"""API dependencies using Dishka.""" from typing import Annotated, Any from dishka.integrations.fastapi import FromDishka from fastapi import Depends, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from app.application import ( CreatePostUseCase, DeletePostUseCase, GetPostUseCase, ListPostsUseCase, PublishPostUseCase, UpdatePostUseCase, ) from app.domain.exceptions import ForbiddenException, UnauthorizedException from app.domain.roles import Role, get_effective_role from app.infrastructure.auth import KeycloakAuthClient, TokenInfo # Use case dependencies - injected via Dishka CreatePostDep = FromDishka[CreatePostUseCase] GetPostDep = FromDishka[GetPostUseCase] UpdatePostDep = FromDishka[UpdatePostUseCase] DeletePostDep = FromDishka[DeletePostUseCase] ListPostsDep = FromDishka[ListPostsUseCase] PublishPostDep = FromDishka[PublishPostUseCase] # Security scheme security = HTTPBearer(auto_error=False) def get_keycloak_client(request: Request) -> KeycloakAuthClient: """Get Keycloak client from DI container via request state.""" client: KeycloakAuthClient = request.state.dishka_container.get(KeycloakAuthClient) return client async def get_current_token_info( credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)], request: Request, ) -> TokenInfo: """Validate token and return token info from Keycloak.""" if not credentials: raise UnauthorizedException("Authentication required") keycloak_client = get_keycloak_client(request) token = credentials.credentials token_info = await keycloak_client.introspect_token(token) if not token_info.is_valid: raise UnauthorizedException("Invalid or expired token") return token_info async def get_current_user_id( token_info: Annotated[TokenInfo, Depends(get_current_token_info)], ) -> str: """Get current user ID from validated token.""" return token_info.user_id CurrentUserDep = Annotated[str, Depends(get_current_user_id)] TokenInfoDep = Annotated[TokenInfo, Depends(get_current_token_info)] # Optional auth - doesn't require authentication but provides user info if available async def get_optional_token_info( credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)], request: Request, ) -> TokenInfo | None: """Get token info if valid token provided, otherwise None (guest).""" if not credentials: return None keycloak_client = get_keycloak_client(request) token = credentials.credentials token_info = await keycloak_client.introspect_token(token) if token_info.is_valid: return token_info return None OptionalTokenInfoDep = Annotated[TokenInfo | None, Depends(get_optional_token_info)] async def get_optional_user_id( token_info: OptionalTokenInfoDep, ) -> str | None: """Get current user ID if token is valid, otherwise None.""" if token_info: return token_info.user_id return None OptionalUserDep = Annotated[str | None, Depends(get_optional_user_id)] def get_current_role(token_info: OptionalTokenInfoDep) -> Role: """Get effective role from token info. Returns GUEST if no valid token provided. """ if token_info and token_info.roles: return get_effective_role(token_info.roles) return Role.GUEST CurrentRoleDep = Annotated[Role, Depends(get_current_role)] def require_roles(allowed_roles: list[Role]) -> Any: """Create dependency that checks if user has one of the allowed roles.""" async def check_role(role: CurrentRoleDep) -> Role: if role not in allowed_roles: raise ForbiddenException( f"Access denied. Required roles: {[r.value for r in allowed_roles]}" ) return role return Depends(check_role) # Predefined role requirements RequireAdmin = require_roles([Role.ADMIN]) RequireUser = require_roles([Role.USER, Role.ADMIN]) RequireAny = require_roles([Role.GUEST, Role.USER, Role.ADMIN])