# #region auth_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, credential, session, jwt] # @defgroup Services Module group. # @BRIEF Orchestrates credential authentication and ADFS JIT user provisioning. # @LAYER Domain # @RELATION DEPENDS_ON -> [AuthRepository] # @RELATION DEPENDS_ON -> [verify_password] # @RELATION DEPENDS_ON -> [Auth.Jwt.CreateAccessToken] # @RELATION DEPENDS_ON -> [User] # @RELATION DEPENDS_ON -> [Role] # @INVARIANT Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles. # @PRE Core auth models and security utilities available. # @POST User identity verified and session tokens issued according to role scopes. # @SIDE_EFFECT Writes last login timestamps and JIT-provisions external users. # @DATA_CONTRACT [Credentials | ADFSClaims] -> [UserEntity | SessionToken] # @RATIONALE Thin service layer between API routes and AuthRepository — encapsulates credential # verification (password hashing), ADFS JIT provisioning, and session creation into # a single orchestration class. This keeps route handlers free of auth business logic. # @REJECTED Putting credential verification logic directly in API route handlers was rejected — # it would be duplicated across local login, ADFS callback, and API-key auth paths. # Authentication was kept separate from authorization (permission checks in middleware) # to allow independent testing of each concern. from datetime import UTC, datetime from typing import Any from sqlalchemy.orm import Session from ..core.auth.jwt import create_access_token from ..core.auth.logger import log_security_event from ..core.auth.repository import AuthRepository from ..core.auth.security import verify_password from ..core.logger import belief_scope from ..models.auth import User # #region AuthService [C:3] [TYPE Class] # @defgroup Services Module group. # @BRIEF Provides high-level authentication services. # @RELATION DEPENDS_ON -> [AuthRepository] # @RELATION DEPENDS_ON -> [User] # @RELATION DEPENDS_ON -> [Role] class AuthService: # region AuthService_init [TYPE Function] # @PURPOSE: Initializes the authentication service with repository access over an active DB session. # @PRE db is a valid SQLAlchemy Session instance bound to the auth persistence context. # @POST self.repo is initialized and ready for auth user/role CRUD operations. # @SIDE_EFFECT Allocates AuthRepository and binds it to the provided Session. # @DATA_CONTRACT Input(Session) -> Model(AuthRepository) # @PARAM db (Session) - SQLAlchemy session. def __init__(self, db: Session): self.db = db self.repo = AuthRepository(db) # endregion AuthService_init # region AuthService.authenticate_user [TYPE Function] # @PURPOSE: Validates credentials and account state for local username/password authentication. # @PRE username and password are non-empty credential inputs. # @POST Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None. # @SIDE_EFFECT Persists last_login update for successful authentications via repository. # @DATA_CONTRACT Input(str username, str password) -> Output(User | None) # @RELATION DEPENDS_ON ->[AuthRepository] # @RELATION CALLS ->[verify_password] # @RELATION DEPENDS_ON ->[User] # @PARAM username (str) - The username. # @PARAM password (str) - The plain password. # @RETURN Optional[User] - The authenticated user or None. def authenticate_user(self, username: str, password: str) -> User | None: with belief_scope("auth.authenticate_user"): user = self.repo.get_user_by_username(username) if not user or not user.is_active: return None if not verify_password(password, user.password_hash): return None # Update last login user.last_login = datetime.now(UTC) self.db.commit() self.db.refresh(user) return user # endregion AuthService.authenticate_user # region AuthService.create_session [TYPE Function] # @PURPOSE: Issues an access token payload for an already authenticated user. # @PRE user is a valid User entity containing username and iterable roles with role.name values. # @POST Returns session dict with non-empty access_token and token_type='bearer'. # @SIDE_EFFECT Generates signed JWT via auth JWT provider. # @DATA_CONTRACT Input(User) -> Output(Dict[str, str]{access_token, token_type}) # @RELATION CALLS -> [Auth.Jwt.CreateAccessToken] # @RELATION DEPENDS_ON ->[User] # @RELATION DEPENDS_ON ->[Role] # @PARAM user (User) - The authenticated user. # @RETURN Dict[str, str] - Session data. def create_session(self, user: User) -> dict[str, str]: with belief_scope("auth.create_session"): roles = [role.name for role in user.roles] access_token = create_access_token( data={"sub": user.username, "scopes": roles} ) return {"access_token": access_token, "token_type": "bearer"} # endregion AuthService.create_session # region AuthService.provision_adfs_user [TYPE Function] # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings. # @PRE user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent. # @POST Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state. # @SIDE_EFFECT May insert new User, mutate user.roles, commit transaction, and refresh ORM state. # @DATA_CONTRACT Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted) # @RELATION DEPENDS_ON ->[AuthRepository] # @RELATION DEPENDS_ON ->[User] # @RELATION DEPENDS_ON ->[Role] # @PARAM user_info (Dict[str, Any]) - Claims from ADFS token. # @RETURN User - The provisioned user. def provision_adfs_user(self, user_info: dict[str, Any]) -> User: with belief_scope("auth.provision_adfs_user"): username = user_info.get("upn") or user_info.get("email") email = user_info.get("email") groups = user_info.get("groups", []) user = self.repo.get_user_by_username(username) if not user: user = User( username=username, email=email, full_name=user_info.get("name"), auth_source="ADFS", is_active=True, is_ad_user=True, ) self.db.add(user) log_security_event("USER_PROVISIONED", username, {"source": "ADFS"}) # Sync roles from AD groups mapped_roles = self.repo.get_roles_by_ad_groups(groups) user.roles = mapped_roles user.last_login = datetime.now(UTC) self.db.commit() self.db.refresh(user) return user # endregion AuthService.provision_adfs_user # #endregion AuthService # #endregion auth_service