from __future__ import annotations from datetime import datetime import bcrypt from sqlalchemy import select from app.db.engine import get_session from app.db.models import User from app.security.audit import log_event from app.security.ldap_client import LdapClient from app.security.ldap_sync import sync_user_ldap_roles def hash_password(password: str) -> str: return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") def verify_password(password: str, hashed: str) -> bool: if not hashed: return False try: return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8")) except Exception: return False def get_user_by_username(session, username: str) -> User | None: return session.scalar(select(User).where(User.username == username)) def authenticate_local(username: str, password: str, request=None) -> int | None: db = get_session() try: user = get_user_by_username(db, username) if not user or not user.is_active or user.is_ldap: if user: log_event(db, action="login.failed", target=username, detail={"reason": "local_denied"}, request=request) return None if not verify_password(password, user.password_hash): log_event(db, action="login.failed", target=username, detail={"reason": "password"}, request=request) return None user.last_login_at = datetime.utcnow() db.add(user) db.commit() log_event(db, action="login.success", target=username, detail={"provider": "local"}, request=request, actor=user) return int(user.id) finally: db.close() def authenticate_ldap(username: str, password: str, request=None) -> int | None: client = LdapClient() result = client.authenticate(username, password) if not result: db = get_session() try: log_event(db, action="login.failed", target=username, detail={"reason": "ldap"}, request=request) finally: db.close() return None user_dn = result["user_dn"] db = get_session() try: user = get_user_by_username(db, username) if not user: user = User(username=username, is_active=True, is_superuser=False, is_ldap=True, password_hash="") db.add(user) db.commit() db.refresh(user) user.is_ldap = True user.is_active = True user.last_login_at = datetime.utcnow() sync_user_ldap_roles(session=db, user=user, username=username, user_dn=user_dn) db.add(user) db.commit() log_event(db, action="login.success", target=username, detail={"provider": "ldap"}, request=request, actor=user) return int(user.id) finally: db.close() def authenticate(username: str, password: str, request=None) -> int | None: user_id = authenticate_local(username, password, request=request) if user_id: return user_id return authenticate_ldap(username, password, request=request)