All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 18s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 24s
Migration 005, Registry-Sync, Placeholder-Validation, capability-geschuetzte API, Tests und Abschlussbericht v0.1. Co-authored-by: Cursor <cursoragent@cursor.com>
80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
"""Central capability resolution and FastAPI enforcement dependencies."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any, Callable
|
|
|
|
from fastapi import Depends, HTTPException
|
|
|
|
from rights_registry import load_grants_for_roles
|
|
from services.audit import log_audit
|
|
from tenant_context import TenantContext, get_tenant_context, require_tenant_context
|
|
|
|
CAPABILITY_ENFORCE_ENV = "CAPABILITY_ENFORCE"
|
|
|
|
|
|
def capability_enforcement_mode() -> str:
|
|
raw = os.getenv(CAPABILITY_ENFORCE_ENV, "probe").strip().lower()
|
|
return "enforce" if raw in ("1", "true", "enforce", "yes") else "probe"
|
|
|
|
|
|
def resolve_capabilities(*, portal_role: str, tenant_role: str | None) -> frozenset[str]:
|
|
return frozenset(load_grants_for_roles(portal_role=portal_role, tenant_role=tenant_role))
|
|
|
|
|
|
def has_capability(ctx: TenantContext, capability_key: str) -> bool:
|
|
return capability_key in ctx.capabilities
|
|
|
|
|
|
def check_capability(ctx: TenantContext, capability_key: str) -> dict[str, Any]:
|
|
allowed = has_capability(ctx, capability_key)
|
|
return {
|
|
"capability_key": capability_key,
|
|
"allowed": allowed,
|
|
"reason": None if allowed else "missing_grant",
|
|
}
|
|
|
|
|
|
def require_capability(capability_key: str) -> Callable[..., TenantContext]:
|
|
"""FastAPI dependency — blocks in enforce mode, probes in probe mode."""
|
|
|
|
def _dependency(ctx: TenantContext = Depends(require_tenant_context)) -> TenantContext:
|
|
return _enforce_capability(ctx, capability_key)
|
|
|
|
return _dependency
|
|
|
|
|
|
def require_capability_ctx(capability_key: str) -> Callable[..., TenantContext]:
|
|
"""Like require_capability but allows portal context without active tenant."""
|
|
|
|
def _dependency(ctx: TenantContext = Depends(get_tenant_context)) -> TenantContext:
|
|
return _enforce_capability(ctx, capability_key)
|
|
|
|
return _dependency
|
|
|
|
|
|
def _enforce_capability(ctx: TenantContext, capability_key: str) -> TenantContext:
|
|
result = check_capability(ctx, capability_key)
|
|
if result["allowed"]:
|
|
return ctx
|
|
|
|
log_audit(
|
|
"capability.denied",
|
|
user_id=ctx.user_id,
|
|
tenant_id=ctx.tenant_id,
|
|
details={
|
|
"capability_key": capability_key,
|
|
"mode": capability_enforcement_mode(),
|
|
"portal_role": ctx.portal_role,
|
|
"tenant_role": ctx.tenant_role,
|
|
},
|
|
)
|
|
|
|
if capability_enforcement_mode() == "enforce":
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=f"Capability fehlt: {capability_key}",
|
|
)
|
|
return ctx
|