"""FastAPI dependencies: auth, db, tenant context, RBAC.""" from __future__ import annotations import uuid from typing import Any import redis.asyncio as aioredis from fastapi import Depends, HTTPException, Request, status from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings from app.core.auth import get_redis, get_session_data, refresh_session_ttl from app.core.db import get_db, set_tenant_context async def get_redis_dep() -> aioredis.Redis: """FastAPI dependency for Redis client.""" return get_redis() async def get_current_user( request: Request, db: AsyncSession = Depends(get_db), redis: aioredis.Redis = Depends(get_redis_dep), ) -> dict[str, Any]: """Get the current authenticated user from session cookie or internal headers. Returns session data dict with user_id, tenant_id, email, name, role, and resolved permissions from Redis cache. Supports internal calls via X-Internal-Call: true header with X-Tenant-Id and X-User-Id headers (for AI tool API access). """ settings = get_settings() # Check for internal call (AI tool access) if request.headers.get("X-Internal-Call") == "true": tenant_id_str = request.headers.get("X-Tenant-Id", "") user_id_str = request.headers.get("X-User-Id", "") if tenant_id_str and user_id_str: try: tenant_id = uuid.UUID(tenant_id_str) user_id = uuid.UUID(user_id_str) await set_tenant_context(db, tenant_id) from app.core.permissions import get_cached_permissions resolved = await get_cached_permissions(db, redis, user_id, tenant_id) return { "user_id": user_id_str, "tenant_id": tenant_id_str, "permissions": resolved.get("permissions", []), "denied_permissions": resolved.get("denied", []), "field_permissions": resolved.get("field_permissions", {}), "is_system_admin": resolved.get("is_system_admin", False), "is_active": True, } except (ValueError, Exception): pass # Fall through to session cookie auth session_id = request.cookies.get(settings.session_cookie_name) if not session_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={"detail": "Not authenticated", "code": "not_authenticated"}, ) session_data = await get_session_data(redis, session_id) if session_data is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={"detail": "Session expired or invalid", "code": "session_invalid"}, ) # Sliding session: extend TTL on each authenticated request await refresh_session_ttl(redis, session_id) if not session_data.get("is_active", True): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={"detail": "User account deactivated", "code": "user_inactive"}, ) # Set RLS tenant context tenant_id = uuid.UUID(session_data["tenant_id"]) await set_tenant_context(db, tenant_id) # Load resolved permissions from cache (or DB on miss) from app.core.permissions import get_cached_permissions user_id = uuid.UUID(session_data["user_id"]) resolved = await get_cached_permissions(db, redis, user_id, tenant_id) session_data["permissions"] = resolved.get("permissions", []) session_data["denied_permissions"] = resolved.get("denied", []) session_data["field_permissions"] = resolved.get("field_permissions", {}) session_data["is_system_admin"] = resolved.get("is_system_admin", False) return session_data async def require_admin( current_user: dict[str, Any] = Depends(get_current_user), ) -> dict[str, Any]: """Require admin role (legacy + new permission system).""" if current_user.get("is_system_admin") or current_user.get("role") == "admin": return current_user # Also check via permission system from app.core.permissions import check_permission if check_permission(current_user, "*:*"): return current_user raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Admin access required", "code": "forbidden"}, ) async def require_write( current_user: dict[str, Any] = Depends(get_current_user), ) -> dict[str, Any]: """Require write permission (admin, editor, or custom role with write perms).""" if current_user.get("is_system_admin"): return current_user role = current_user.get("role", "viewer") if role in ("admin", "editor"): return current_user # Check via permission system for custom roles from app.core.permissions import check_permission if check_permission(current_user, "*:write") or check_permission(current_user, "*:create"): return current_user raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Write access required", "code": "forbidden"}, ) def require_permission(permission: str): """FastAPI dependency factory: require a specific permission. Usage: @router.get("/contacts", dependencies=[Depends(require_permission("contacts:read"))]) """ async def _check( current_user: dict[str, Any] = Depends(get_current_user), ) -> dict[str, Any]: if current_user.get("is_system_admin"): return current_user from app.core.permissions import check_permission if check_permission(current_user, permission): return current_user raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={ "detail": f"Permission '{permission}' required", "code": "forbidden", }, ) return _check def require_field_access(module: str, field: str, default: str = "read"): """FastAPI dependency factory: require field-level access. Usage: @router.get("/contacts/{id}", dependencies=[Depends(require_field_access("contacts", "annual_revenue"))]) """ async def _check( current_user: dict[str, Any] = Depends(get_current_user), ) -> dict[str, Any]: if current_user.get("is_system_admin"): return current_user from app.core.permissions import check_field_access access = check_field_access(current_user, module, field, default) if access == "hidden": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={ "detail": f"Field '{field}' is hidden", "code": "field_hidden", }, ) return current_user return _check async def get_tenant_id( current_user: dict[str, Any] = Depends(get_current_user), ) -> uuid.UUID: """Extract tenant_id from current user session.""" return uuid.UUID(current_user["tenant_id"]) async def get_current_user_id( current_user: dict[str, Any] = Depends(get_current_user), ) -> uuid.UUID: """Extract user_id from current user session.""" return uuid.UUID(current_user["user_id"])