Access Control Matrix: Role-Based (RBAC) & Attribute-Based (ABAC) Paradigms

Reading Progress48%

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 exploreGET /api/v1/system-status/status_pages returns published pages (plus platform-status) via platform FORJD credentials.
  • Authenticated reads/writes — resolve deml_account_id → forjd_tenant_id → secret_ref and 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.