""" FastAPI Authentication Dependencies and Factory Functions. Provides: - get_keycloak_config_from_env(): Create config from env vars - get_authenticator(): Create HybridAuthenticator instance - get_auth(): Global authenticator singleton - get_current_user(): FastAPI dependency for authentication - require_role(): FastAPI dependency factory for role-based access """ import os import logging from typing import Optional, Dict, Any from fastapi import Request, HTTPException, Depends from .keycloak_auth import ( KeycloakConfig, KeycloakConfigError, HybridAuthenticator, TokenExpiredError, TokenInvalidError, ) logger = logging.getLogger(__name__) # ============================================= # FACTORY FUNCTIONS # ============================================= def get_keycloak_config_from_env() -> Optional[KeycloakConfig]: """ Create KeycloakConfig from environment variables. Required env vars: - KEYCLOAK_SERVER_URL: e.g., https://keycloak.breakpilot.app - KEYCLOAK_REALM: e.g., breakpilot - KEYCLOAK_CLIENT_ID: e.g., breakpilot-backend Optional: - KEYCLOAK_CLIENT_SECRET: For confidential clients - KEYCLOAK_VERIFY_SSL: Default true """ server_url = os.environ.get("KEYCLOAK_SERVER_URL") realm = os.environ.get("KEYCLOAK_REALM") client_id = os.environ.get("KEYCLOAK_CLIENT_ID") if not all([server_url, realm, client_id]): logger.info("Keycloak not configured, using local JWT only") return None return KeycloakConfig( server_url=server_url, realm=realm, client_id=client_id, client_secret=os.environ.get("KEYCLOAK_CLIENT_SECRET"), verify_ssl=os.environ.get("KEYCLOAK_VERIFY_SSL", "true").lower() == "true" ) def get_authenticator() -> HybridAuthenticator: """ Get configured authenticator instance. Uses environment variables to determine configuration. """ keycloak_config = get_keycloak_config_from_env() # JWT_SECRET is required - no default fallback in production jwt_secret = os.environ.get("JWT_SECRET") environment = os.environ.get("ENVIRONMENT", "development") if not jwt_secret and environment == "production": raise KeycloakConfigError( "JWT_SECRET environment variable is required in production" ) return HybridAuthenticator( keycloak_config=keycloak_config, local_jwt_secret=jwt_secret, environment=environment ) # ============================================= # FASTAPI DEPENDENCY # ============================================= # Global authenticator instance (lazy-initialized) _authenticator: Optional[HybridAuthenticator] = None def get_auth() -> HybridAuthenticator: """Get or create global authenticator.""" global _authenticator if _authenticator is None: _authenticator = get_authenticator() return _authenticator async def get_current_user(request: Request) -> Dict[str, Any]: """ FastAPI dependency to get current authenticated user. Usage: @app.get("/api/protected") async def protected_endpoint(user: dict = Depends(get_current_user)): return {"user_id": user["user_id"]} """ auth_header = request.headers.get("authorization", "") if not auth_header.startswith("Bearer "): # Check for development mode environment = os.environ.get("ENVIRONMENT", "development") if environment == "development": # Return demo user in development without token return { "user_id": "10000000-0000-0000-0000-000000000024", "email": "demo@breakpilot.app", "role": "admin", "realm_roles": ["admin"], "tenant_id": "a0000000-0000-0000-0000-000000000001", "auth_method": "development_bypass" } raise HTTPException(status_code=401, detail="Missing authorization header") token = auth_header.split(" ")[1] try: auth = get_auth() return await auth.validate_token(token) except TokenExpiredError: raise HTTPException(status_code=401, detail="Token expired") except TokenInvalidError as e: raise HTTPException(status_code=401, detail=str(e)) except Exception as e: logger.error(f"Authentication failed: {e}") raise HTTPException(status_code=401, detail="Authentication failed") async def require_role(required_role: str): """ FastAPI dependency factory for role-based access. Usage: @app.get("/api/admin-only") async def admin_endpoint(user: dict = Depends(require_role("admin"))): return {"message": "Admin access granted"} """ async def role_checker(user: dict = Depends(get_current_user)) -> dict: user_role = user.get("role", "user") realm_roles = user.get("realm_roles", []) if user_role == required_role or required_role in realm_roles: return user raise HTTPException( status_code=403, detail=f"Role '{required_role}' required" ) return role_checker