feat(permissions): ABAC integration, principals caching, cache version validation
- Integrate ABAC policies into apply_visibility_filter() (allow/deny with priority) - Add field whitelist (ABAC_ALLOWED_FIELDS) for build_sql_condition() security - Add request-level ContextVar for user principals (group_ids, role_id) - Set principals in deps.py (session + bearer auth) - Use ContextVar in visibility.py and permission_resolver.py (N+1 fix) - Add version validation to get_cached_visible_ids() (cache strategy unification) - Deactivate delegation route (parked — not integrated into resolve_permissions) - Add 7 ABAC integration tests All 70 tests pass (7 ABAC + 63 existing). No regressions.
This commit is contained in:
+31
-3
@@ -96,20 +96,31 @@ async def get_current_user(
|
||||
is_admin = session_data.get("is_system_admin", False)
|
||||
await set_user_context(db, user_id, group_ids, is_admin)
|
||||
|
||||
# Check membership status (P1.7: suspended membership should not be usable)
|
||||
# Check membership status and load role_id (P1.7: suspended membership should not be usable)
|
||||
from app.models.user import UserTenant
|
||||
membership_q = await db.execute(
|
||||
select(UserTenant.status)
|
||||
select(UserTenant.status, UserTenant.role_id)
|
||||
.where(UserTenant.user_id == user_id)
|
||||
.where(UserTenant.tenant_id == tenant_id)
|
||||
)
|
||||
membership_status = membership_q.scalar_one_or_none()
|
||||
membership_row = membership_q.first()
|
||||
membership_status = membership_row[0] if membership_row else None
|
||||
role_id = membership_row[1] if membership_row else None
|
||||
if membership_status is not None and membership_status != "active":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={"detail": f"Mitgliedschaft ist {membership_status}, Zugriff verweigert", "code": "membership_suspended"},
|
||||
)
|
||||
|
||||
# Cache user principals for this request — avoids N+1 queries in visibility.py
|
||||
from app.core.principals import UserPrincipals, set_principals
|
||||
set_principals(UserPrincipals(
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
group_ids=group_ids,
|
||||
role_id=role_id,
|
||||
))
|
||||
|
||||
# Load resolved permissions from cache (or DB on miss)
|
||||
from app.core.permissions import get_cached_permissions
|
||||
|
||||
@@ -165,6 +176,23 @@ async def get_current_user_bearer(
|
||||
is_admin = user_data.get("is_system_admin", False)
|
||||
await set_user_context(db, user_id, group_ids, is_admin)
|
||||
|
||||
# Load role_id and cache user principals for this request
|
||||
from app.models.user import UserTenant
|
||||
membership_q = await db.execute(
|
||||
select(UserTenant.role_id)
|
||||
.where(UserTenant.user_id == user_id)
|
||||
.where(UserTenant.tenant_id == tenant_id)
|
||||
)
|
||||
role_id = membership_q.scalar_one_or_none()
|
||||
|
||||
from app.core.principals import UserPrincipals, set_principals
|
||||
set_principals(UserPrincipals(
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
group_ids=group_ids,
|
||||
role_id=role_id,
|
||||
))
|
||||
|
||||
# Load resolved permissions
|
||||
from app.core.permissions import get_cached_permissions
|
||||
redis = get_redis()
|
||||
|
||||
Reference in New Issue
Block a user