[split-required] Split final 43 files (500-668 LOC) to complete refactoring
klausur-service (11 files): - cv_gutter_repair, ocr_pipeline_regression, upload_api - ocr_pipeline_sessions, smart_spell, nru_worksheet_generator - ocr_pipeline_overlays, mail/aggregator, zeugnis_api - cv_syllable_detect, self_rag backend-lehrer (17 files): - classroom_engine/suggestions, generators/quiz_generator - worksheets_api, llm_gateway/comparison, state_engine_api - classroom/models (→ 4 submodules), services/file_processor - alerts_agent/api/wizard+digests+routes, content_generators/pdf - classroom/routes/sessions, llm_gateway/inference - classroom_engine/analytics, auth/keycloak_auth - alerts_agent/processing/rule_engine, ai_processor/print_versions agent-core (5 files): - brain/memory_store, brain/knowledge_graph, brain/context_manager - orchestrator/supervisor, sessions/session_manager admin-lehrer (5 components): - GridOverlay, StepGridReview, DevOpsPipelineSidebar - DataFlowDiagram, sbom/wizard/page website (2 files): - DependencyMap, lehrer/abitur-archiv Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
104
backend-lehrer/auth/keycloak_models.py
Normal file
104
backend-lehrer/auth/keycloak_models.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
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"
|
||||
)
|
||||
Reference in New Issue
Block a user