""" Keycloak Authentication - Models, Config, and Exceptions. """ import os import logging from typing import Optional, Dict, Any, List from dataclasses import dataclass logger = logging.getLogger(__name__) @dataclass class KeycloakConfig: """Keycloak connection configuration.""" server_url: str realm: str client_id: str client_secret: Optional[str] = None verify_ssl: bool = True @property def issuer_url(self) -> str: return f"{self.server_url}/realms/{self.realm}" @property def jwks_url(self) -> str: return f"{self.issuer_url}/protocol/openid-connect/certs" @property def token_url(self) -> str: return f"{self.issuer_url}/protocol/openid-connect/token" @property def userinfo_url(self) -> str: return f"{self.issuer_url}/protocol/openid-connect/userinfo" @dataclass class KeycloakUser: """User information extracted from Keycloak token.""" user_id: str email: str email_verified: bool name: Optional[str] given_name: Optional[str] family_name: Optional[str] realm_roles: List[str] client_roles: Dict[str, List[str]] groups: List[str] tenant_id: Optional[str] raw_claims: Dict[str, Any] def has_realm_role(self, role: str) -> bool: return role in self.realm_roles def has_client_role(self, client_id: str, role: str) -> bool: client_roles = self.client_roles.get(client_id, []) return role in client_roles def is_admin(self) -> bool: return self.has_realm_role("admin") or self.has_realm_role("schul_admin") def is_teacher(self) -> bool: return self.has_realm_role("teacher") or self.has_realm_role("lehrer") class KeycloakAuthError(Exception): """Base exception for Keycloak authentication errors.""" pass class TokenExpiredError(KeycloakAuthError): """Token has expired.""" pass class TokenInvalidError(KeycloakAuthError): """Token is invalid.""" pass class KeycloakConfigError(KeycloakAuthError): """Keycloak configuration error.""" pass def get_keycloak_config_from_env() -> Optional[KeycloakConfig]: """Create KeycloakConfig from environment variables.""" 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" )