Punit1's picture
Initial commit
939c0c0
Raw History Blame Contribute Delete
5.04 kB
"""
JWT Security, Password Hashing, and RBAC utilities.
"""
from __future__ import annotations
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional
import structlog
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings
logger = structlog.get_logger(__name__)
import bcrypt
import passlib.handlers.bcrypt
def _fixed_calc_checksum(self, secret):
if isinstance(secret, str):
secret = secret.encode("utf-8")
secret = secret[:72]
secret, ident = self._prepare_digest_args(secret)
config = self._get_config(ident)
if isinstance(config, str):
config = config.encode("ascii")
raw_hash = bcrypt.hashpw(secret, config).decode("utf-8")
return raw_hash[29:]
passlib.handlers.bcrypt._BcryptBackend._calc_checksum = _fixed_calc_checksum
passlib.handlers.bcrypt._BcryptBackend._lacks_20_support = True
passlib.handlers.bcrypt._BcryptBackend._workrounds_initialized = True
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# ── Role Definitions ─────────────────────────────────────────
class UserRole(str, Enum):
ADMIN = "admin"
HR = "hr"
FINANCE = "finance"
EMPLOYEE = "employee"
# Role hierarchy: what each role can access
ROLE_PERMISSIONS: dict[UserRole, list[str]] = {
UserRole.ADMIN: [
"chat:all", "documents:all", "sql:all",
"admin:all", "reports:all", "users:manage",
],
UserRole.FINANCE: [
"chat:basic", "sql:finance", "documents:finance",
"reports:finance", "analytics:finance",
],
UserRole.HR: [
"chat:basic", "documents:hr", "reports:hr",
"analytics:hr",
],
UserRole.EMPLOYEE: [
"chat:basic", "documents:public",
],
}
# ── Password Utilities ───────────────────────────────────────
def hash_password(password: str) -> str:
pwd_clean = password.encode("utf-8")[:72].decode("utf-8", errors="ignore")
return pwd_context.hash(pwd_clean)
def verify_password(plain_password: str, hashed_password: str) -> bool:
pwd_clean = plain_password.encode("utf-8")[:72].decode("utf-8", errors="ignore")
return pwd_context.verify(pwd_clean, hashed_password)
# ── JWT Utilities ────────────────────────────────────────────
def create_access_token(
user_id: str,
tenant_id: str,
role: UserRole,
email: str,
expires_delta: Optional[timedelta] = None,
) -> str:
expire = datetime.utcnow() + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
payload = {
"sub": user_id,
"tenant_id": tenant_id,
"role": role.value,
"email": email,
"type": "access",
"exp": expire,
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
def create_refresh_token(user_id: str, tenant_id: str) -> str:
expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
payload = {
"sub": user_id,
"tenant_id": tenant_id,
"type": "refresh",
"exp": expire,
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
def decode_token(token: str) -> dict:
"""Decode and validate a JWT token. Raises JWTError on failure."""
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
# ── Permission Checks ────────────────────────────────────────
def has_permission(role: UserRole, permission: str) -> bool:
"""Check if a role has a specific permission."""
role_perms = ROLE_PERMISSIONS.get(role, [])
# Check exact permission or wildcard (e.g., "admin:all" grants everything)
return (
permission in role_perms
or any(
perm.endswith(":all") and perm.split(":")[0] == permission.split(":")[0]
for perm in role_perms
)
or "admin:all" in role_perms
)
def require_roles(*roles: UserRole):
"""FastAPI dependency factory: require specific roles."""
from typing import Any
from fastapi import Depends, HTTPException, status
from app.core.dependencies import get_current_active_user
async def role_checker(current_user: Any = Depends(get_current_active_user)):
if current_user.role not in roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"error": "insufficient_permissions",
"message": f"Required role(s): {[r.value for r in roles]}",
"your_role": current_user.role,
},
)
return current_user
return role_checker