Files
ss-tools/backend/src/services/auth_service.py
busya a3c7c402b7 fix(llm): add fetch-models endpoint, fix SQL Lab INSERT (client_id truncation, sync mode, target_column, timestamp normalization)
- Add POST /api/llm/providers/fetch-models route with LLMClient.fetch_models()
- Add target_column to TranslationJob model/schema/service/orchestrator
- Fix SQL Lab execute: truncate client_id to 11 chars (varchar(11))
- Switch SQL Lab to sync mode (runAsync: false) — no Celery workers
- Fix polling: unwrap nested result from Superset query API
- Fix ClickHouse timestamp: normalize float timestamps to YYYY-MM-DD
2026-05-13 20:06:15 +03:00

143 lines
6.5 KiB
Python

# #region auth_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, credential, session, jwt]
# @BRIEF Orchestrates credential authentication and ADFS JIT user provisioning.
# @LAYER: Domain
# @RELATION DEPENDS_ON -> [AuthRepository]
# @RELATION DEPENDS_ON -> [verify_password]
# @RELATION DEPENDS_ON -> [create_access_token]
# @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]
from typing import Dict, Any, Optional, List
from datetime import datetime
from sqlalchemy.orm import Session
from ..core.auth.repository import AuthRepository
from ..core.auth.security import verify_password
from ..core.auth.jwt import create_access_token
from ..core.auth.logger import log_security_event
from ..models.auth import User, Role
from ..core.logger import belief_scope
# #region AuthService [C:3] [TYPE Class]
# @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) -> Optional[User]:
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.utcnow()
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] ->[create_access_token]
# @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.utcnow()
self.db.commit()
self.db.refresh(user)
return user
# endregion AuthService.provision_adfs_user
# #endregion AuthService
# #endregion auth_service