Authorization Models
ACL (Access Control List): per-resource list of who can do what. Simple but does not scale — updating access for a role change requires touching every resource. RBAC (Role-Based Access Control): users are assigned roles; roles have permissions; permissions are (resource, action) pairs. Scalable for enterprise: changing a role’s permissions updates access for all users with that role. ABAC (Attribute-Based Access Control): policies evaluate attributes of the subject (user department, clearance), resource (classification, owner), action, and environment (time of day, IP range). Most expressive but more complex to implement and audit. ReBAC (Relationship-Based Access Control): Google Zanzibar model — permissions are derived from relationships in a graph. “User A can view document D if A is a viewer of D, or A is a member of a group that is a viewer of D.”
RBAC Data Model
-- Core RBAC tables
CREATE TABLE users (user_id UUID PRIMARY KEY, email TEXT, ...);
CREATE TABLE roles (role_id UUID PRIMARY KEY, name TEXT, description TEXT);
CREATE TABLE permissions (
perm_id UUID PRIMARY KEY,
resource_type TEXT, -- "document", "project", "billing"
resource_id UUID, -- NULL = applies to all resources of this type
action TEXT -- "read", "write", "delete", "admin"
);
CREATE TABLE role_permissions (role_id UUID, perm_id UUID, PRIMARY KEY (role_id, perm_id));
CREATE TABLE user_roles (
user_id UUID, role_id UUID,
scope_type TEXT, -- "global", "organization", "project"
scope_id UUID, -- NULL for global
PRIMARY KEY (user_id, role_id, scope_type, COALESCE(scope_id, '00000000-0000-0000-0000-000000000000'::UUID))
);
Scoped roles: a user can be ADMIN of project A but VIEWER of project B. The scope_type and scope_id columns capture this. Authorization check: does user U have permission to perform action A on resource R of type T?
SELECT 1
FROM user_roles ur
JOIN role_permissions rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.perm_id = p.perm_id
WHERE ur.user_id = $user_id
AND p.action = $action
AND p.resource_type = $resource_type
AND (p.resource_id IS NULL OR p.resource_id = $resource_id)
AND (ur.scope_type = 'global'
OR (ur.scope_type = $scope_type AND ur.scope_id = $scope_id))
LIMIT 1;
Policy Decision Point (PDP) and Policy Enforcement Point (PEP)
The PDP evaluates authorization requests and returns ALLOW/DENY. The PEP intercepts requests at service boundaries and calls the PDP. Separation of concerns: services contain no authorization logic — they delegate to the PDP. Implementation choices: Centralized PDP: a dedicated authorization service (Open Policy Agent, AWS IAM, custom). Simple operationally but adds network latency to every authorization check. Cache at the PEP: store allow/deny decisions in a local cache with short TTL (< 60s) to avoid per-request network calls. Embedded PDP: the policy engine runs as a sidecar or in-process. Eliminates network latency; policy updates are pushed to all instances. Open Policy Agent (OPA) supports this with bundle distribution.
Google Zanzibar-Style ReBAC
Zanzibar stores tuples: (object, relation, user). Example: (doc:readme, viewer, user:alice) or (doc:readme, viewer, group:eng#member). Authorization check: “Can user:alice view doc:readme?” resolves by checking all viewer tuples for doc:readme, recursively expanding group memberships. The result is a graph traversal. Scale challenges: the tuple store has billions of rows (Google handles trillions). Key optimizations: Zookies (consistency tokens): each write returns a token encoding the write timestamp. Subsequent reads include the token and are guaranteed to see that write (solves the “new enemy” problem where a permission removal is not yet visible). Namespace configs: schema definitions for each relation type, enabling query optimization. Leopard index: precomputed group-membership expansions to avoid deep recursive traversals at query time. Open-source implementations: Ory Keto, SpiceDB, Authzed.
See also: Atlassian Interview Prep
See also: Databricks Interview Prep
See also: Stripe Interview Prep