Permissions, borrowed from Amazon and Google

J
John Doe
|
Jan 15, 2024
|
5 min read
#react
#javascript
#frontend

Access control is the least forgiving system in a platform. Every other subsystem can earn partial credit. A slow query is still a query, a rough UI still renders, but a permission model that is almost right is wrong, and wrong here means someone saw something that was not theirs. Giant Context, the website platform I build, is multitenant, which raises the stakes again. Organizations that must never see each other share one database, one API, and one codebase. The structure is public. The plumbing stays home on purpose.

The identity half, briefly

A user system answers two questions, who are you and what may you do. Permissions bind to the who, so that half comes first. Authentication here is rented. Google Cloud's Identity Platform stores the credentials, runs sign-in and MFA, and issues a signed token when a person proves who they are. Identity is owned. The platform keeps its own users table, resolves every verified token to an internal user id at the API, and binds memberships, grants, and audit history to that internal id. The coupling is one column. Swap the provider and nothing downstream notices.

One rule crosses from identity into everything below. The token carries who, never what. Providers offer custom claims that stamp roles into the token at sign-in. Remove a contractor's access on Friday and a claims-based system honors their Monday token until it expires. Resolved from the schema instead, revocation lands on the next request. Authorization lives in tables, where it can change between two requests.

What RBAC is for

Role-based access control answers the second question, what may you do, and its core move is indirection. Nobody grants a permission to a person. Permissions are named capabilities, roles are bundles of permissions, and people hold roles. The indirection is the entire value. When capabilities attach to people directly, every audit means reconstructing who was given what, and every departure means tracking down each grant one at a time. When they attach to roles, the questions an auditor actually asks (who can do this, what can she do, what happens when he leaves) become lookups.

The model is decades old and the biggest multitenant systems on earth run on descendants of it. That is precisely why I did not invent one.

Who does this well

For two years I ran production on Azure, and I have run this platform's stack on Google Cloud since its first commit. Both clouds, and Amazon's, converge on the same skeleton with different vocabulary:

AWS
Google Cloud
Azure
Here

Permission

s3:GetObject

storage.objects.get

provider action strings

email:read

Bundle

policy

role

role definition

role

Grant

policy attachment

binding

assignment

role grant

Scope

resource / account

org → folder → project

mgmt group → subscription → group

org → project → app

Inheritance

via resource paths

down the hierarchy

down the scopes

down the ladder

Across any row, the differences are spelling. Permissions are namespaced strings. Roles bundle them. Grants bind a role to a principal at a scope, and scopes form a hierarchy that inherits downward. Three companies with different histories and enormous security teams arrived at the same shape. Convergence like that is evidence.

Three companies with different histories and enormous security teams arrived at the same shape. Convergence like that is evidence.

So when it came time to build this platform's model, the decision was to adopt it outright rather than improvise. The action grammar here is Amazon's, exactly. A permission is service:action, so crm:delete and email:read read the way s3:GetObject reads. The scoping is Google's. Grants live on a resource ladder that runs organization, then project, then app, and a role granted at a rung covers the rungs below it. I had operated inside Azure's version for two years without ever seeing its internals, so I built what I could infer from the outside and measured it against the standard the outside implied. The standard was secure by default, multitenant at every layer, and granular down to the app.

The structure, concretely

Four tables carry the entire model, and the migration that created them describes itself:

The RBAC tables, from the migration

-- Creates permissions, roles, role_permissions, user_roles tables-- Permissions table (auto-synced from OpenAPI x-permissions)CREATE TABLE permissions ( ... );-- Roles table (admin-managed)CREATE TABLE roles ( ... );-- Role-Permission junction tableCREATE TABLE role_permissions ( ... );-- User-Role junction table (users can have multiple roles)CREATE TABLE user_roles ( ... );

Textbook RBAC, unmodified. Permissions, bundles, grants, and the people they belong to. The first comment carries the design. The permissions table is not maintained by anyone. It is auto-synced from the API specification.

Declared once, derived everywhere

This codebase generates its client surface from the API's description of itself, and the permission model rides the same machinery. Each operation in the API carries its permission requirement as part of its OpenAPI description. From that single declaration, the pieces derive.

The permission catalog is extracted from the routes into a spec file, and a sync step keeps the two from drifting:

One permission, extracted from the routes

{  "name": "email:read",  "routes": [    "GET  .../apps/email/{appId}/emails",    "GET  .../apps/email/{appId}/emails/{emailId}",    "GET  .../apps/email/{appId}/templates"  ]}

The backend enforces what the spec declares. The frontend consumes the same declaration, because each console page exports its requirements in the same permission language, so the page that shows a thing and the endpoint that serves it are gated by the same string from the same source. There is no second vocabulary to fall out of agreement, on either side of the wire. One set of names everywhere, generated rather than remembered.

No conditional grant checks, anywhere. Elements, pages, and routes all derive from the generated files. I don't write permission conditionals. I have my grants, and things are tagged accordingly.

That is the developer experience the whole structure buys. There is no hand-written if-may-they in this codebase. A surface declares what it is, the tags carry the requirement, and the machinery gates it.

And the agreement is tested by machine. Among the generated test families is an RBAC suite that exercises the declared permissions against the enforced ones, so a route whose declaration and enforcement disagree fails in CI rather than in production. I do not audit the permission model by reading it. The suite audits it on every push.

I do not audit the permission model by reading it. The suite audits it on every push.

Multitenancy is the reason for all of it

The ladder earns its keep the moment two customers exist. Organizations are hard walls. Nothing grants across them, and no role, however elevated inside its organization, reaches into another. Below the wall, the ladder makes delegation granular. An organization role for the people who run the company. A project role for the people who work a given site. An app-level grant for someone who should touch the email tool and nothing else. Granularity is what lets real teams map their actual trust onto the platform instead of rounding everyone up to admin.

Machine callers get no separate treatment. API access rides the same permission model as a person holding a role, which means the answer to who can do this never has an asterisk for automation.

I borrowed rather than innovated. I consider that the most defensible security decision in the codebase. The model is the one Amazon, Google, and Microsoft converged on. The grammar is Amazon's, the hierarchy is Google's, the discipline of running it came from two years inside Azure. What this codebase adds is not a new idea about access control. It is the wiring, spec-declared, generated to both sides, synced into the database, and tested for agreement on every push. If you are building multitenant software, that is the part worth copying. Take the shape from the giants. Then make your own system incapable of disagreeing with itself.

Have questions?

If you're interested in my work or Giant Context, contact me!

Contact Jesse
Legal
Privacy Policy
Terms of Service
Cookie Policy

© 2026 Giant Context. All rights reserved.

Permissions, borrowed from Amazon and Google