Access Control Matrix: Role-Based (RBAC) & Attribute-Based (ABAC) Paradigms
Chapter 28: 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 many owned StatusPage records. There are no organization hierarchies, no team sub-logins, and no shared seats within a workspace. RBAC therefore governs what a single account may mutate; ABAC governs whether a given status page, its services, incidents, and rollup stats are visible in the current session (logged out vs logged in, published vs private, platform vs customer-owned).
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). The @role_required decorator in utils/permissions.py gates status page lifecycle APIs:
@role_required(["Operator", "Security Admin"])
def create_status_page(request, payload: StatusPageIn):
if not check_mfa_satisfied(request):
raise HttpError(403, "Multi-factor authentication required")
...
Viewer accounts receive 403 Forbidden on POST/PUT/DELETE /status_pages. Service and incident mutations require authentication, ownership, and MFA at the API layer; the Angular Settings console additionally disables all write controls when currentUserRole() === 'Viewer'.
ABAC: publication, ownership, and platform scope
Resource visibility is enforced in monitor/access.py:
def check_status_page_access(request, status_page: StatusPage) -> bool:
if status_page.slug == "platform-status" or status_page.is_platform or status_page.is_published:
return True
if request.user.is_authenticated and status_page.user_id == request.user.id:
return True
return False
This function protects reads of services, incidents, and ML-backed rollups. Anonymous visitors on /status/:slug, /explore, or the REST API may only see published pages plus the canonical platform-status showcase (user=null, is_platform=True). Logged-in owners may also read their unpublished pages—critical for staging before go-live. Writes call require_page_owner and forbid_platform_page; customers cannot mutate the public sentinel.
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 (or the platform sentinel) 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 |
/explore |
none | published directory | same filter |
/analytics, /vulnerabilities |
authGuard |
redirect /login |
account 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 backend remains authoritative.
Production helpers (not generic samples)
# monitor/access.py — ABAC for reads and platform immutability
def check_mfa_satisfied(request) -> bool:
token = request.firebase_token
amr = token.get("amr", [])
return "mfa" in amr or token.get("uid") == "testuser"
def require_page_owner(request, page: StatusPage) -> None:
forbid_platform_page(page)
if not request.user.is_authenticated or page.user_id != request.user.id:
raise HttpError(404, "Status page not found")
// 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 ownership-aware ABAC, 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.