Platrium Docs
ArchitectureAuthorization

How Authorization Works

Capabilities, roles, grants and one tiny recursive query. That's the whole trick!

"Can Alice download this file?" sounds simple, until you add groups, orgs, public links, nested folders, shared drives and a few thousand tenants. Platrium answers it with a small, boring model that lives in plain SQL. Let's walk through it!

Everything here lives in engine/internal/authz (the model and the Authorizer interface) and engine/internal/authz/sqlauthz (the SQL engine). Enforcement happens in fsops, so no resolver or handler can forget to check.

Capabilities Are Bits

A capability is one thing you may do to an item. Each one is a single bit in a 64-bit integer, so a set of capabilities is just a number and "is this allowed?" is one bitwise AND.

CapabilityBitValueLets you
LIST01See that an item exists, list a folder
VIEW12Read metadata and preview
DOWNLOAD24Download or copy out the content
COMMENT38Comment (once comments exist)
CREATE8256Upload or create inside a folder
EDIT9512Rename, replace content
DELETE101024Delete permanently
MOVE112048Move within the drive
TRASH124096Send to trash
MOVE_OUT138192Move or copy out of the drive
SHARE1665536Give others access (up to your own)
MANAGE17131072Break inheritance, manage settings
DELETE_DRIVE18262144Delete a whole drive

Some capabilities imply others (EDIT implies VIEW, which implies LIST). Grants are normalized when written, so checks never apply rules.

Grants

A grant says: this subject gets these capabilities on this item (and everything under it).

FieldMeaning
resource_idThe file, folder or drive root
subject_type / subject_idUSER, GROUP, TENANT (the whole org) or PUBLIC (anyone)
capsThe capability snapshot
roleA display label
expires_atOptional expiry
tenant_id, drive_idFor isolation, and so a drive's grants delete in one statement

One grant per subject per item, so sharing again just replaces it.

The Check

No Yes Yes No Yes No Principaluser + groups Item inmy tenant? Not found Owner of aprivate drive? All capabilities Ancestor chain, thenmatching grants(2 queries) OR thecapabilities Has what theoperation needs? Forbidden

Two queries do the work: one recursive CTE walks the ancestors up to the drive root, marking every ancestor above a restriction as cut, and one indexed lookup loads the unexpired grants that match your user, groups, tenant or PUBLIC. On a cut ancestor only grants that carry MANAGE count; everywhere else every grant does. CapsMany runs the whole thing for many items at once, which is how folder listings stay fast.

An item you can't see at all is reported as not found, never "forbidden". That way nobody can probe which IDs exist. "Forbidden" only appears when you can see the item but lack the capability.

Inheritance

Grants flow down the tree. A grant on a folder covers everything inside it. To lock a subfolder down, set inherit_perms = false on it: ancestors' grants stop applying there, and only grants on the item itself (and below) count.

There's one exception, and it's what keeps a drive administrable: a grant that carries MANAGE (a Drive Admin's) keeps applying through a restriction. Restricting hides a folder from members, editors and public links, never from the drive's admins. So restricting needs no extra grant to "keep you in", and resuming inheritance leaves nothing behind.

There are no deny rules. Access only ever adds up, which keeps checks order-independent and easy to explain.

Groups: Flatten The Small Side

Groups can nest. We keep the direct memberships in group_members and a flattened group_closures table (every group → every user beneath it), rebuilt in the same transaction as each membership change. Resolving "which groups is Alice in?" is then one indexed lookup, however deep the nesting.

What Enforces It

Every fsops operation requires a capability on the item(s) involved:

OperationNeeds
List a folder, count, breadcrumbs, change feedLIST on the folder
Item or file metadataVIEW
DownloadDOWNLOAD
Upload, new folderCREATE on the parent
RenameEDIT
MoveMOVE on the item and CREATE on the destination (same drive only)
CopyDOWNLOAD on the source and CREATE on the destination

Listings, counts, change feeds and breadcrumbs also hide what you can't see. A breadcrumb starts at the topmost folder you can open, so someone shared a deep folder never learns the names above it.

Changing Access: The Rules

Reading access has one choke point (CapsMany). Writing access has one too: the rules in authz.CheckChange.

An item's access is just the set of grants on it, and every write is a change to that set: share, revoke, set general access, restrict, create a drive. So a writer does the same thing every time. It works out what the grants will be, hands the before and after to CheckChange, and writes only if the rules agree.

lock the item's drive load grants as they are (Before) build grants as they will be (After) Change(actor, item facts, Before, After) ok, or the first rule that says no write Writer (Grant, Revoke, ...) Drive lock authz.CheckChange Storage
RuleIn plain wordsError
no-self-editNobody changes their own access to an item, up or down. You can't make yourself a viewer of your own file or remove the grant you were given. Someone else has to.ErrInvalid
role-offeredA grant only carries a role its item and its kind offer: no Drive Admin on a file, no Editor for the public, no expiry where a level can't have one.ErrInvalid
public-allowedA public grant is refused when the tenant has turned public sharing off.ErrForbidden
no-escalationNobody hands out more capabilities than they hold.ErrForbidden
never-orphanA shared drive keeps at least one permanent manager (a user or a group). Removing, demoting or end-dating the last one is refused.ErrInvalid

Where It Lives

The rules are in package authz, with no database access, and they take plain types: a Change, the item's ItemFacts (is it a drive root, is it in a shared drive, does it inherit) and the grants. The adapter fills in the facts and does the locking and writing. That is what keeps them engine-neutral. An OpenFGA engine runs the same rules, and the shared scenarios in authztest check that every engine agrees. See scaling and OpenFGA.

Code Layout

engine/internal/authz/
├── capability.go   the bit registry, implied rules, Verbs()/Parse
├── role.go         built-in roles and their wording per context
├── level.go        the general-access levels (restricted, organization, public)
├── model.go        Principal, Grant, Subject, GeneralAccess
├── rules.go        the change rules and CheckChange (no storage imports)
├── authz.go        the Authorizer interface (reads AND writes)
├── authztest/      scenarios every engine must pass
└── sqlauthz/       the SQL engine: check, grants, groups, shared, general,
                    and mutate.go (lock, load, check) used by every writer

The Authorizer interface owns writes as well as reads. Each engine stores its own data, so there is never a dual-write to keep in sync. That's what makes swapping engines possible. More on that in scaling and OpenFGA.

On this page