chore: update semantic contracts and git merge handling
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
# [DEF:backend.src.services.auth_service:Module]
|
||||
#
|
||||
# @COMPLEXITY: 5
|
||||
# @SEMANTICS: auth, service, business-logic, login, jwt, adfs, jit-provisioning
|
||||
# @PURPOSE: Orchestrates credential authentication and ADFS JIT user provisioning.
|
||||
@@ -9,28 +8,29 @@
|
||||
# @RELATION: [DEPENDS_ON] ->[backend.src.core.auth.jwt.create_access_token]
|
||||
# @RELATION: [DEPENDS_ON] ->[backend.src.models.auth.User]
|
||||
# @RELATION: [DEPENDS_ON] ->[backend.src.models.auth.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]
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models.auth import User, Role
|
||||
|
||||
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
|
||||
# [/SECTION]
|
||||
|
||||
|
||||
# [DEF:AuthService:Class]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Provides high-level authentication services.
|
||||
class AuthService:
|
||||
# [DEF:__init__:Function]
|
||||
# [DEF:AuthService.__init__:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @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.
|
||||
@@ -39,10 +39,11 @@ class AuthService:
|
||||
# @DATA_CONTRACT: Input(Session) -> Model(AuthRepository)
|
||||
# @PARAM: db (Session) - SQLAlchemy session.
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.repo = AuthRepository(db)
|
||||
# [/DEF:__init__:Function]
|
||||
# [/DEF:AuthService.__init__:Function]
|
||||
|
||||
# [DEF:authenticate_user:Function]
|
||||
# [DEF:AuthService.authenticate_user:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Validates credentials and account state for local username/password authentication.
|
||||
# @PRE: username and password are non-empty credential inputs.
|
||||
@@ -52,23 +53,24 @@ class AuthService:
|
||||
# @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):
|
||||
with belief_scope("AuthService.authenticate_user"):
|
||||
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:
|
||||
if not user or not user.is_active:
|
||||
return None
|
||||
|
||||
if not user.is_active:
|
||||
return None
|
||||
|
||||
if not user.password_hash or not verify_password(password, user.password_hash):
|
||||
if not verify_password(password, user.password_hash):
|
||||
return None
|
||||
|
||||
self.repo.update_last_login(user)
|
||||
# Update last login
|
||||
user.last_login = datetime.utcnow()
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
|
||||
return user
|
||||
# [/DEF:authenticate_user:Function]
|
||||
# [/DEF:AuthService.authenticate_user:Function]
|
||||
|
||||
# [DEF:create_session:Function]
|
||||
# [DEF:AuthService.create_session:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @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.
|
||||
@@ -77,24 +79,16 @@ class AuthService:
|
||||
# @DATA_CONTRACT: Input(User) -> Output(Dict[str, str]{access_token, token_type})
|
||||
# @PARAM: user (User) - The authenticated user.
|
||||
# @RETURN: Dict[str, str] - Session data.
|
||||
def create_session(self, user) -> Dict[str, str]:
|
||||
with belief_scope("AuthService.create_session"):
|
||||
# Collect role names for scopes
|
||||
scopes = [role.name for role in user.roles]
|
||||
|
||||
token_data = {
|
||||
"sub": user.username,
|
||||
"scopes": scopes
|
||||
}
|
||||
|
||||
access_token = create_access_token(data=token_data)
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer"
|
||||
}
|
||||
# [/DEF:create_session:Function]
|
||||
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"}
|
||||
# [/DEF:AuthService.create_session:Function]
|
||||
|
||||
# [DEF:provision_adfs_user:Function]
|
||||
# [DEF:AuthService.provision_adfs_user:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @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.
|
||||
@@ -104,32 +98,34 @@ class AuthService:
|
||||
# @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("AuthService.provision_adfs_user"):
|
||||
with belief_scope("auth.provision_adfs_user"):
|
||||
username = user_info.get("upn") or user_info.get("email")
|
||||
email = user_info.get("email")
|
||||
ad_groups = user_info.get("groups", [])
|
||||
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_active=True,
|
||||
is_ad_user=True
|
||||
)
|
||||
self.repo.db.add(user)
|
||||
|
||||
# Update roles based on group mappings
|
||||
from ..models.auth import ADGroupMapping
|
||||
mapped_roles = self.repo.db.query(Role).join(ADGroupMapping).filter(
|
||||
ADGroupMapping.ad_group.in_(ad_groups)
|
||||
).all()
|
||||
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
|
||||
self.repo.db.commit()
|
||||
self.repo.db.refresh(user)
|
||||
|
||||
user.last_login = datetime.utcnow()
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
|
||||
return user
|
||||
# [/DEF:provision_adfs_user:Function]
|
||||
# [/DEF:AuthService.provision_adfs_user:Function]
|
||||
# [/DEF:AuthService:Class]
|
||||
|
||||
# [/DEF:backend.src.services.auth_service:Module]
|
||||
Reference in New Issue
Block a user