A workspace product has pages nested arbitrarily deep, with permissions inheritable and overridable at any level, shared with individuals, groups and guests. How should authorization be designed so checks stay fast and correct?
Show the full answer Hide the answer
The core difficulty
A permission check must answer "can this user perform this action on this object" for an object whose effective permissions derive from an arbitrarily deep ancestor chain, with overrides at any level, group memberships that are themselves nested, and sharing that can be granted or revoked at any moment.
Doing that naively means walking the tree on every check, which is a variable number of lookups on the hottest path in the product.
The design
1. A dedicated authorization model, not permission columns. Relationships expressed explicitly —
page:X parent page:Y, user:A editor page:Y, group:G member user:A — with inheritance and group
expansion as rules over the relationship graph. This is the relationship-based model popularised by
large-scale internal authorization systems, and it exists because ad-hoc permission columns cannot express
inheritance with overrides.
2. Materialise effective permissions, so a check is a lookup rather than a traversal. The trade is that a permission change high in the tree must propagate to descendants, which is a fan-out write.
3. Bound the propagation. Materialise lazily for cold subtrees and eagerly for hot ones, or materialise to a depth and traverse the remainder. A permission change at the root of a large workspace must not be a synchronous operation.
4. Deny wins, and revocation is immediate. Grants may be eventually consistent; revocations must not. This asymmetry is the single most important correctness property: a user who loses access must lose it now, while a user who gains access can wait a second. Design the caching around that asymmetry rather than treating both directions the same.
5. Check at the data boundary, not only at the API boundary. Every path that returns content — search, export, notifications, previews, link unfurling — must apply the same check. Search results and notification previews are the classic leak paths, because they are built by a different team from a different index.
6. Cache with a version token per subtree. A permission change bumps the version, invalidating cached decisions for that subtree without enumerating them.
The failure modes to design against
- Search leaking content because the index was built before a permission change and filtering happens on the index rather than on current truth.
- Public link sharing bypassing checks, becoming an unauthenticated path into the permission model.
- Guest access inheriting more than intended when a page is moved into a different part of the tree — a move changes effective permissions for everything beneath it.
- Group membership changes not propagating, so a departed employee retains access.
- Permission checks that fail open under load or dependency failure. Authorization must fail closed, which means it needs availability commensurate with the whole product.
The one-line principle
Grants may be eventually consistent; revocations may not. Almost every serious authorization incident in collaborative products traces back to violating that asymmetry somewhere in a cache or an index.