Access Control Matrix: Role-Based (RBAC) & Attribute-Based (ABAC) Paradigms
Chapter 29: Access Control Matrix: Role-Based (RBAC) & Attribute-Based (ABAC) Paradigms
Architecting a scalable SaaS observability platform requires authorization that matches how customers actually use the product. The DEML Platform uses a User + Sites model: one Firebase Authentication login maps to one Django User, one UserProfile.account_id, and one FORJD tenant that may own many status pages. There are no organization hierarchies, no team sub-logins, and no shared seats within a workspace. Status pages, services, incidents, and uptime projections are FORJD-owned and reached through the Django BFF (backend/forjd/). RBAC governs what a single DEML account may mutate via that BFF; FORJD enforces tenant binding, scopes, and published-page visibility for anonymous explore/status reads.
RBAC: one role per login
UserProfile.role is exactly one of Viewer, Operator, or Security Admin. On first Firebase login, middleware provisions a profile—defaulting to Operator (or Security Admin for the platform bootstrap account). Django role gates and backend/forjd/ action policy run before proxying status lifecycle calls to FORJD:
# Conceptual BFF gate — stable DEML path adapts to FORJD /api/v1/status/pages
@require_forjd_action("status.admin") # Operator / Security Admin
async def status_pages_list_proxy(request):
if request.method in ("POST", "PUT", "PATCH", "DELETE") and not check_mfa_satisfied(request):
return JsonResponse({"detail": "Multi-factor authentication required"}, status=403)
return await proxy_to_forjd(request, "GET|POST", "/api/v1/status/pages")
Viewer accounts receive 403 Forbidden on mutating /api/v1/system-status/status_pages verbs. Service and incident mutations require authentication, mapped-tenant ownership, and MFA at the BFF; the Angular Settings console additionally disables all write controls when currentUserRole() === 'Viewer'.
ABAC: publication, tenant binding, and platform scope
Resource visibility is enforced by the Django BFF adapters plus FORJD tenant binding and publication rules—not by a local StatusPage ORM:
- Anonymous explore —
GET /api/v1/system-status/status_pagesreturns published pages (plusplatform-status) via platform FORJD credentials. - Authenticated reads/writes — resolve
deml_account_id → forjd_tenant_id → secret_refand fail closed on mismatch; unpublished pages stay private to the owning tenant. - Platform sentinel — customers cannot mutate
platform-status.
MFA is ABAC on the session token: check_mfa_satisfied requires "mfa" in the Firebase JWT amr claim before any state change. Machine clients use a separate ABAC path—API keys on /api/v1/ingest and /api/v1/predict resolve to UserProfile.account_id and the mapped FORJD tenant via hashed tokens, not hardcoded hostnames.
Frontend routing mirrors backend intent
| Route | Guard | Anonymous | Logged-in |
|---|---|---|---|
/status, /status/:slug |
none | published + platform-status |
+ own unpublished (via BFF) |
/explore |
none | published directory | same filter |
/analytics, /vulnerabilities |
authGuard |
redirect /login |
account / FORJD data |
/settings |
none (UI RBAC) | loads; mutations need login + non-Viewer | full console if Operator+ |
authGuard only checks authentication—it does not replace server-side RBAC/ABAC. The Django BFF remains authoritative for session termination; FORJD remains authoritative for status-page data.
Production helpers (not generic samples)
# MFA session gate (simplified; production accepts amr + firebase.sign_in_second_factor)
def check_mfa_satisfied(request) -> bool:
token = request.firebase_token or {}
amr = token.get("amr", [])
if isinstance(amr, str) and "mfa" in amr.lower():
return True
if isinstance(amr, (list, tuple)) and any(str(x).lower() == "mfa" for x in amr):
return True
firebase = token.get("firebase") or {}
if isinstance(firebase, dict) and firebase.get("sign_in_second_factor"):
return True
return token.get("uid") == "testuser"
// guards/auth.guard.ts — login required for sensitive dashboards
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.isAuthenticated() ? true : router.parseUrl("/login");
};
By combining per-account RBAC with publication- and tenant-aware ABAC on the FORJD status APIs, the platform keeps private operational stats off the public internet while still exposing a world-readable platform-status sentinel and customer-published pages—without inventing org charts we do not implement.