Free lesson · GenAI Platform Engineering
Design RBAC model with roles, permissions, and scopes
Define the permission model with platform-admin, team-admin, developer, and viewer roles. Map each role to specific API actions and resource scopes.
Course: AI Developer Platform Engineering · Chapter 7 · RBAC & Access Control
Free to read — no subscription required.
Introduction
Engineers often inherit access systems where every team member shares the same permissions—until a junior developer accidentally deletes a production model endpoint or a billing dashboard leaks to contractors who have no business seeing it. The problem is that ad-hoc permission grants accumulate faster than they are reviewed, and without a deliberate structure the blast radius of any single over-privileged account is the entire platform. This lesson teaches you to design a layered RBAC model built around four explicit roles—platform-admin, team-admin, developer, and viewer—each bound to a resource scope. You'll define permissions as typed verb-resource pairs, encode role inheritance in Python dataclasses, and assemble the result into enforceable policy objects your middleware can evaluate on every API request.
Key Terminology
- Permission — a frozen, hashable pair of an
Actionverb (such asDEPLOYorMANAGE_BILLING) and aResourceType(such asMODELorBILLING), encoded as a@dataclass(frozen=True)so instances can live in Pythonsetobjects and duplicate definitions collapse atimporttime rather than silently diverging at runtime. - ResourceScope — a hierarchical boundary with four levels (
"global","organization","team","project") that constrains where a permission applies; itscontains()method determines whether one scope subsumes another, so a global-scope policy automatically covers every organization, team, and project boundary beneath it. - Role — a named container holding a direct
permissionsset and an optionalinherits_fromparent reference; callingall_permissions()traverses the ancestor chain and returns the transitive union of every permission defined at or above that role in the hierarchy. - Role Inheritance — the mechanism by which a child role's effective permissions are the union of its own
permissionsset and all permissions from its ancestor chain, enforcing a strict superset relationship:platform-admin⊇team-admin⊇developer⊇viewerat every link. - RBACPolicy — the enforceable artifact your middleware evaluates on each API request, binding a
principal_id, aRole, and aResourceScopeinto the single declarative unit that answers "who may do what, and where." - Principal — any entity that can request access (a human user, service account, or CI/CD identity), identified in an
RBACPolicyby itsprincipal_id; because the RBAC model binds capabilities to roles rather than to principals directly, revoking access means removing the policy binding rather than hunting down individual permission grants scattered across the system.
Concepts
Permissions as Typed Verb-Resource Pairs
The atomic unit of access control in this model is always two-dimensional: what action applied to what kind of resource. A flat string like "models:deploy" captures the idea but is fragile — a typo silently creates a permission token that matches nothing, and nothing stops two developers from spelling the same capability two different ways. Encoding both dimensions as enums (Action and ResourceType) means the valid vocabulary is declared in one place and every reference is checked by the interpreter. Combining them into a @dataclass(frozen=True) makes each Permission instance hashable, so the full permission set for a role can be stored as a Python set: duplicate definitions collapse automatically, and set intersection and union operations across roles are native operations rather than manual loops (see Code Walkthrough).
Scope as a Containment Hierarchy
Every permission needs a "where" dimension. A developer who can create models inside Team A should not automatically be able to do so in Team B, even though both belong to the same organization. ResourceScope encodes four levels of hierarchy — "global", "organization", "team", and "project" — and its contains() method makes the containment relationship explicit: a broader scope returns True for any narrower scope nested beneath it. A platform-admin bound at "global" therefore needs exactly one policy to govern the entire platform, while a team-admin requires a separate policy per team. Crucially, scope comparison in middleware reduces to a single scope.contains(request_scope) call rather than a chain of ad-hoc conditional logic, which means the boundary rules are enforced consistently wherever policies are evaluated.
Role Inheritance as Transitive Set Union
The four-role hierarchy is structured so that every role's effective permission set is a strict superset of the role immediately below it — platform-admin holds everything team-admin holds, team-admin holds everything developer holds, and so on down to viewer. Role.all_permissions() implements this by recursively unioning the role's own permissions set with the complete permission set of its inherits_from ancestor. The practical consequence is a blast-radius guarantee: no lower-privilege role can silently acquire a capability its parent lacks. If a refactor adds Action.DEPLOY to developer but forgets to add it to team-admin, the hierarchy's superset invariant is broken and a team-admin unexpectedly lacks a permission a developer holds. Catching that break requires checking all_permissions() across the chain — which is exactly what the Code Walkthrough's verification exercise asks you to do with a developer pointing to a viewer parent (see Code Walkthrough).
Code Walkthrough
Now that you understand the core RBAC building blocks—roles, permissions, resource scopes, and policies—here is how they connect as a concrete four-role hierarchy and typed Python structures.
Four-Role Hierarchy
The diagram below shows how permissions cascade from platform-admin (global scope) through team-admin and developer down to viewer (read-only):
Each arrow is labeled to show whether the child inherits all of its parent's permissions or only read-level permissions. platform-admin connects exclusively to Billing & Quotas and Users & Roles, reflecting its global governance scope. viewer connects only to Models & Endpoints and Datasets & Artifacts—no write, deploy, pipeline, or administrative capabilities.
Modeling the Structures in Python
The following code encodes the hierarchy as Python dataclasses and enums. Action enumerates every verb the platform supports. ResourceType enumerates the object categories those verbs apply to. A Permission is a frozen pairing of one action and one resource type—frozen=True makes it hashable so permissions can be stored in sets and duplicate definitions are caught at import time rather than at runtime. ResourceScope encodes the hierarchical boundary and its contains method determines whether one scope subsumes another. Role holds a permission set and an optional parent reference; all_permissions() walks the inheritance chain and unions every permission transitively. RBACPolicy is the enforceable artifact: it binds a principal, a role, and a scope into the single unit your middleware evaluates on each request.
Code snippetpython
1from dataclasses import dataclass, field 2from enum import Enum 3from typing import Optional 4 5class Action(Enum): 6 CREATE = "create" 7 READ = "read" 8 UPDATE = "update" 9 DELETE = "delete" 10 DEPLOY = "deploy" 11 MANAGE_MEMBERS = "manage_members" 12 MANAGE_BILLING = "manage_billing" 13 VIEW_AUDIT_LOG = "view_audit_log" 14 15class ResourceType(Enum): 16 MODEL = "model" 17 DATASET = "dataset" 18 PIPELINE = "pipeline" 19 DEPLOYMENT = "deployment" 20 TEAM = "team" 21 BILLING = "billing" 22 AUDIT_LOG = "audit_log" 23 24@dataclass(frozen=True) 25class Permission: 26 action: Action 27 resource_type: ResourceType 28 description: str = "" 29 30@dataclass(frozen=True) 31class ResourceScope: 32 level: str # "global", "organization", "team", "project" 33 org_id: Optional[str] = None 34 team_id: Optional[str] = None 35 project_id: Optional[str] = None 36 37 def contains(self, other: "ResourceScope") -> bool: 38 if self.level == "global": 39 return True 40 if self.level == "organization": 41 return other.org_id == self.org_id 42 if self.level == "team": 43 return other.org_id == self.org_id and other.team_id == self.team_id 44 if self.level == "project": 45 return (other.org_id == self.org_id 46 and other.team_id == self.team_id 47 and other.project_id == self.project_id) 48 return False 49 50@dataclass 51class Role: 52 name: str 53 permissions: set[Permission] = field(default_factory=set) 54 inherits_from: Optional["Role"] = None 55 56 def all_permissions(self) -> set[Permission]: 57 perms = set(self.permissions) 58 if self.inherits_from is not None: 59 perms |= self.inherits_from.all_permissions() 60 return perms 61 62@dataclass 63class RBACPolicy: 64 principal_id: str 65 role: Role 66 scope: ResourceScope
The hierarchy enforces a strict superset relationship: every lower role's permissions are a subset of its parent's, so a child role can never hold a capability its parent lacks.
Check that you can instantiate a developer role pointing to a viewer parent, call all_permissions() on the developer role, and confirm the returned set includes both the developer's own permissions and every permission defined on viewer.
Do's and Don'ts
Having just walked through the dataclasses, scope containment, and inheritance chain, the rules below distill those mechanics into the handful of choices that most often decide whether an RBAC model holds up under review.
Do's
- ✓Do mark
Permissionasfrozen=True— thefrozen=Trueflag makesPermissioninstances hashable, which lets you store them in sets and catches duplicate permission definitions atimporttime rather than silently at runtime when an enforcement check may already be in progress. - ✓Do implement
ResourceScope.contains()to gate every policy evaluation — a role's permission set alone is not enough; theRBACPolicymust confirm the request's resource scope falls within the principal's assigned scope before granting access, soplatform-adminat global scope never bleeds into a narrower org or team boundary accidentally. - ✓Do resolve the full inherited permission set via
Role.all_permissions()— becausedeveloperinherits fromviewerandteam-admininherits fromdeveloper, reading onlyrole.permissionssilently drops every transitively inherited capability; callall_permissions()to walk the chain and union every parent's set before evaluating any middleware check.
Don'ts
- ✗Don't store permissions in a list instead of a
set[Permission]— lists allow duplicatePermissionentries and make membership checks O(n) rather than O(1); becausePermissionisfrozen=Trueand therefore hashable, a set is the correct structure and eliminates redundant grants that would otherwise accumulate as roles evolve. - ✗Don't conflate role inheritance with scope widening — a
developerinheriting fromviewermeans it gainsviewer's permission verbs, not a broaderResourceScope; assigning adeveloperrole atteamscope does not grant access to other teams' models or datasets, and thescope.contains()check is what enforces that boundary. - ✗Don't assign
MANAGE_BILLINGorMANAGE_MEMBERSpermissions belowplatform-adminwithout an explicit scope check — theseActionvariants govern global governance resources (ResourceType.BILLING,ResourceType.AUDIT_LOG), and granting them atteamorprojectscope means ateam-admincould inadvertently read or mutate billing data outside their organizational boundary ifResourceScope.contains()is bypassed.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Platform Engineering subscription.
From · cancel anytime · Already a subscriber? Sign in →
More free lessons in AI Developer Platform Engineering
- Ch 4Add request logging with PII redaction pipeline
- Ch 4Monitor gateway latency and token usage with Prometheus
- Ch 6Implement K8s namespace provisioning with quota enforcement
- Ch 6Deploy multi-tenant infrastructure with Helm overrides
- Ch 7Design RBAC model with roles, permissions, and scopesYou are here
- Ch 7Deploy RBAC with policy-as-code validation
- Ch 9Build cost tracking pipeline from gateway metrics