This commit is contained in:
2026-05-10 09:46:07 +12:00
parent cfc193b713
commit 2f2466ecac
81 changed files with 2571 additions and 413 deletions
+14 -7
View File
@@ -2,8 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi import Depends, HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload
@@ -13,14 +12,14 @@ from app.core.access import (
get_user_permissions,
permissions_to_module_map,
)
from app.core.http import ADMIN_AUTH_COOKIE, CLIENT_AUTH_COOKIE, get_bearer_or_cookie_token
from app.core.security_logging import log_security_event
from app.core.security import verify_token
from app.db.session import get_db
from app.models.access import Role, User
from app.models.client_access import ClientFeatureAccess, ClientUser
from app.services.client_access_service import has_access_level, module_access_map
bearer_scheme = HTTPBearer(auto_error=False)
@dataclass(frozen=True)
class AuthSession:
@@ -67,13 +66,16 @@ def _build_internal_auth_session(db: Session, payload: dict) -> AuthSession:
def get_auth_session(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
request: Request,
db: Session = Depends(get_db),
) -> AuthSession:
if credentials is None:
token = get_bearer_or_cookie_token(request, cookie_name=CLIENT_AUTH_COOKIE.name) or get_bearer_or_cookie_token(
request, cookie_name=ADMIN_AUTH_COOKIE.name
)
if token is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
payload = verify_token(credentials.credentials)
payload = verify_token(token)
# Internal Hunter Stock Feeds users get an auth session derived from the
# role/permission tables rather than the client-portal ClientUser tables.
@@ -111,6 +113,7 @@ def require_client_session(session: AuthSession = Depends(get_auth_session)) ->
def require_admin_session(session: AuthSession = Depends(get_auth_session)) -> AuthSession:
if session.role != "admin":
log_security_event("authz.denied", role=session.role, required="admin")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
return session
@@ -143,6 +146,7 @@ def require_client_module_access(module_key: str, minimum_level: str = "view"):
if session.role == "internal":
permissions = session.module_permissions or {}
if not has_access_level(permissions.get(module_key), minimum_level):
log_security_event("authz.denied", role=session.role, module=module_key, access_level=minimum_level)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"{module_key} access is not permitted",
@@ -158,10 +162,12 @@ def require_client_module_access(module_key: str, minimum_level: str = "view"):
)
)
if feature is not None and not feature.enabled:
log_security_event("authz.denied", role=session.role, module=module_key, reason="feature_disabled")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"{module_key} is disabled for this client")
permissions = module_access_map(user)
if not has_access_level(permissions.get(module_key), minimum_level):
log_security_event("authz.denied", role=session.role, module=module_key, access_level=minimum_level)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"{module_key} access is not permitted")
return AuthSession(
@@ -190,6 +196,7 @@ def require_client_access_manager_session(
user = load_current_client_user(db, require_client_session(session))
permissions = module_access_map(user)
if user.role != "superadmin" or not has_access_level(permissions.get("client_access"), "manage"):
log_security_event("authz.denied", role=session.role, module="client_access", access_level="manage")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Superadmin client access is required")
return AuthSession(