Sharing and revoking access
All names, domains, and identifiers in these examples are fictional. With the Riverside festival over, its album is in demand. The news desk wants it for a feature, the festival's organizers want to see the pictures, and a partner agency wants to choose some for distribution. Each request adds a different kind of relationship to the graph from Access through relationships, and each goes wrong in its own way.
Links that anyone can use
The festival's organizers have no accounts at the Gazette, so Maya creates a share link for them instead:
https://photos.example/s/q7Fm2xRv9LcT4wZb8NkP3s
A link like this grants access to whoever holds it. It works like the bearer tokens in Using access tokens: the service cannot tell whether the person presenting it is the organizer Maya sent it to, or someone who received it later. And links travel. One is forwarded to a sponsor, pasted into a group chat whose service fetches a preview, written into a proxy's logs, saved in browser history, or posted on a public page where search engines find it.
A share link therefore has to be designed as a credential:
- Unguessable. The part after
/s/is long and random, not the album ID or a counter, so nobody can find links by trying numbers. The service stores only a hash of it, so a copy of its database contains no working links. - Scoped to viewing. It lets the holder see one album. It never lets them edit, delete, or share further.
- Expiring. It stops working after a set time, such as two weeks after the festival.
- Revocable. Each link is its own record, so Maya can cancel the organizers' link without affecting any other.
In the graph, the link is one more subject: album:4410#viewer@link:lnk-51c2, where anyone who presents the link's secret acts as link:lnk-51c2. Revoking it deletes that one fact.
One choice remains: whether opening the link requires signing in. A link that works only for signed-in Gazette staff is really a share with a group, delivered by URL, and forwarding it outside the newsroom achieves nothing. The organizers need a link that requires no sign-in, and that kind of link should be treated as public to anyone who obtains it.
Removing access in the right order
Two days after the festival, Lena's freelance work on it ends. At 10:00:00 UTC on 16 June, Theo removes Lena's contributor fact from the album, the same removal that Administering roles safely followed through sessions, tokens and caches. At 10:00:02, Maya adds photo 9014, which Raj has asked to keep within the desk while a complaint is reviewed. At 10:00:03, Lena, whose browser still has the album open, refreshes the page.
If every part of the system saw changes in the order they happened, the refresh would be harmless. The removal came first, so Lena would see nothing new. But the photo service runs on many machines. The album's contents come from one database, and the check reads relationships from a copy that updates a few seconds behind. At 10:00:03, that copy has not yet received the removal. The check combines an old fact, Lena is a contributor to album 4410, with new content, photo 9014 is in album 4410, and Lena sees a photo added after Lena's access ended. That is exactly what the removal was meant to prevent.
The fix is to make each check at least as fresh as the most recent change that matters to it. One way is to give every change to the relationships a version number, in order, and have each write return the version it created. When Maya adds photo 9014, the photo is stored with the latest version at that moment. Any check involving the photo must then use relationship data at least that new. A copy that is behind either catches up first or hands the check to one that is current:
10:00:00 remove album:4410#contributor@user:lena -> version 5121
10:00:02 add photo 9014 to album 4410 stored with version 5121
10:00:03 check: can lena view photo 9014?
copy at version 5119 too old for this photo, not used
copy at version 5121 no path from photo 9014 to lena: deny
Versions like these, often handed back to the application as an opaque ordering token, let most checks use fast copies while guaranteeing that no check mixes facts older than the change it depends on. The indexes from the previous lesson need the same care. A list built from an index that has not yet seen the removal would still show Lena the album.
Keeping the graph bounded
Every check is a search, and some shapes of graph make searches expensive or endless. Three cause most of the trouble.
A group can end up containing itself. Sports desk editors already contains the Weekend desk group, so that weekend staff can cover. If someone then adds Sports desk editors to Weekend desk, perhaps so that the editors receive the weekend schedule, each group now contains the other. A search that follows memberships without care goes round forever.
Nesting can run very deep. Groups inside groups inside groups, built up over years of reorganizations, turn one check into a walk through dozens of levels, repeated for every photo on a page.
Fan-out can be enormous. In a large organization, an album shared with an all-staff group of thousands, or a library shared with hundreds of separate groups, can force a single check to expand a great many members or branches before it can answer.
The defenses are limits, set deliberately. A search remembers the points it has visited, so a cycle stops instead of repeating, and the service can refuse a change that would create a cycle when someone tries to make it. A maximum depth, such as ten levels, and a maximum number of points one check may visit keep the work bounded. When a check reaches a limit without finding a path, it denies rather than guessing. A search that was cut short has not shown that a path exists, and allowing the request would be a guess in the most dangerous direction. The denial carries its own reason, limit_reached rather than an ordinary refusal, so that operators can find and flatten the structure that caused it instead of mistaking it for someone who simply lacks access.