Team Docs Sharing: Roles, Link Sharing and Restricted Folders
MediumAI-assisted coding · AI-Assisted Feature Work
Team Docs Sharing: Roles, Link Sharing and Restricted Folders
You've joined the team that owns the sharing model behind a team-docs product (think Google Drive for a company). Items form a tree of folders and docs, people and groups get roles on items, and access flows down the tree. The service is small, but support keeps filing "why can (or can't) this person open that doc?" tickets, and three pieces of work are queued.
Codebase
| File | What it does |
|---|---|
docshare/api.py | run(operations) entry point and the Dispatcher that routes each operation |
docshare/service.py | DocShare: validates arguments, applies changes, keeps the cache honest |
docshare/access.py | AccessResolver: effective role per (user, item), with a cache and invalidation hooks |
docshare/tree.py | ItemTree: folders and docs, move, walk_up, subtree |
docshare/directory.py | Directory: users, groups and group membership |
docshare/grants.py | GrantTable: direct grants (item, principal) -> role |
docshare/roles.py | The role ladder, role_rank, and which role each action needs |
docshare/errors.py | ShareError and every error code |
docshare/bulk.py | Fixture helpers for the load tests (known-good) |
tests/ | Existing end-to-end tests (one of them currently fails) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh DocShare and returns one result per operation. Errors are returned as "error: <code>" strings, never raised. Names (users, groups, items) are non-empty, case-sensitive strings. A wrong number of arguments, or an argument of the wrong type, returns "error: invalid arguments" (checked before anything else); an unknown operation returns "error: unknown operation".
The model. Users and groups share one namespace (a name is either a user or a group). A group's members are users and/or other groups, so groups nest. Everyone is in one org. Roles, strongest first: owner > editor > commenter > viewer; "none" means no access. An item is a "folder" or a "doc"; only folders can contain items; several top-level items (parent null) may exist.
| Operation | Arguments | Returns | Errors (checked in this order) |
|---|---|---|---|
add_user / add_group | name | true | name taken |
add_member | group, member | true | group not found, member not found, membership cycle |
remove_member | group, member | true | group not found, not a member |
add_item | item, kind, parent | true | item exists, invalid kind, parent not found, parent not a folder |
move_item | item, parent | true | item not found, parent not found, parent not a folder, move cycle |
grant | item, principal, role | true | item not found, principal not found, invalid role |
revoke | item, principal | true | item not found, no such grant |
role | user, item | role string | user not found, item not found |
can | user, item, action | true/false | user not found, item not found, invalid action |
add_itemwithparent = nullcreates a top-level item.move_itemwithparent = nullmakes the item top-level; moving an item under itself or under one of its own descendants is amove cycle.grantgives a user or a group a role on an item; granting again to the same (item, principal) replaces the earlier role.revokeremoves it. A principal is a user or a group.add_member/remove_memberaccept a user or a group asmember. Adding someone who is already a member is a no-op that returnstrue.canactions:viewneeds at least viewer,commentcommenter,editeditor,shareowner.role/cantake a user; a group name isuser not found.- Every operation named
bulk_*(bulk_users,bulk_chain,bulk_nest) is a fixture helper the load tests use to build big structures from one operation; they are known-good. Trees may be up to 5,000 levels deep, groups may nest up to 600 deep, and there may be about a thousand users.
Operations that are not built yet (set_link, set_restricted, explain, audit) return "error: not supported".
Part 1 — Make the access check right, then add link sharing
Goal: role(user, item) must return the strongest role the user holds on the item through any direct or inherited grant, via any group they are in at any nesting depth. Today two things are wrong, and group cycles and share links do not exist yet.
Rules
- Inheritance. A grant on a folder applies to everything below it, at any depth.
- Effective role. The user's effective role on an item is the strongest role among *all* grants that apply: every grant, on the item itself or on any ancestor, whose principal is the user or a group the user belongs to. Group membership is transitive: if
engis inplatformandanais ineng, a grant toplatformreachesana. No applicable grant means"none". A weaker grant that is closer to the item never hides a stronger one that is further away. - Membership cycles.
add_member(group, member)returnserror: membership cycleif it would make a group a member of itself, directly or through nested groups. The check comes after the two not-found checks and a rejected call changes nothing. - Link sharing.
["set_link", item, role]turns on "anyone in the org with the link" for the item, withroleeither"viewer"or"commenter";role = nullturns the link off. Anything else iserror: invalid role(item not foundis checked first). While on, *every user in the org* — including users created later — holds that role on the item and everything below it, exactly like a grant would, so rule 2 applies: a link never lowers anyone's role, it only adds a candidate. Setting a link again replaces the previous link role.
Example
[["add_user", "ana"], ["add_group", "eng"], ["add_group", "platform"],
["add_member", "platform", "eng"], ["add_member", "eng", "ana"],
["add_item", "drive", "folder", null], ["add_item", "plan", "doc", "drive"],
["grant", "drive", "platform", "editor"], ["grant", "plan", "ana", "viewer"],
["role", "ana", "plan"], ["add_member", "eng", "platform"],
["add_user", "ben"], ["set_link", "drive", "viewer"], ["role", "ben", "plan"]]returns [true, true, true, true, true, true, true, true, true, "editor", "error: membership cycle", true, true, "viewer"].
Part 2 — Restricted folders and moving things around
Goal: a folder (or doc) can be marked restricted so that access from above stops at it, and move_item must change inherited access immediately. Support also reports that after moving a folder into a different drive, people keep seeing the old drive's access (and lose the new one's) until something unrelated happens to refresh it.
Rules
["set_restricted", item, flag]with a booleanflag(item not foundfirst; a non-boolean flag iserror: invalid arguments) turns restriction on or off. Items start unrestricted.- A restricted item cuts off everything above it: grants and share links set on its ancestors (strictly above it) do not apply to it or to anything below it. Grants and links set on the restricted item itself, or on items below it, apply as usual. Group membership is unaffected.
- Restrictions nest: for a given item, only the grants and links on the item and its ancestors up to and including the nearest restricted one (the item itself counts) apply.
- Rule 2 of Part 1 still decides the effective role among the grants that apply.
- The restricted flag, the item's own grants and its own link travel with the item when it moves.
- After
move_item, everyrole/cananswer for the moved item and everything below it must reflect its new ancestors immediately: inheritance from the old parent chain is gone, inheritance from the new one is in effect (subject to restrictions on the way).
Example
[["add_user", "ana"], ["add_user", "ben"], ["add_item", "drive", "folder", null],
["add_item", "hr", "folder", "drive"], ["add_item", "offer", "doc", "hr"],
["add_item", "notes", "folder", "drive"], ["add_item", "memo", "doc", "notes"],
["grant", "drive", "ana", "editor"], ["grant", "hr", "ben", "viewer"],
["role", "ana", "offer"], ["set_restricted", "hr", true], ["role", "ana", "offer"], ["role", "ben", "offer"],
["add_item", "wiki", "folder", null], ["grant", "wiki", "ana", "commenter"],
["role", "ana", "memo"], ["move_item", "notes", "wiki"], ["role", "ana", "memo"]]returns [true, true, true, true, true, true, true, true, true, "editor", true, "none", "viewer", true, true, "editor", true, "commenter"]. Ana's editor grant on drive stops at the restricted hr; Ben's grant on hr itself still works. Moving notes out of drive into wiki swaps which grant reaches memo.
Part 3 — Explain it, audit it, and let owners back in
Goal: support needs to see *why* someone has access, and who has access at all. Add explain and audit. A new product rule also changes Part 2: restricting a folder is locking owners out of their own drives, so owners must be able to see through restrictions.
Rules
- Owner override (changes Part 2 rule 2). A grant with role
ownerset on a *proper ancestor* of the item (to the user or to a group the user is in) applies even if a restricted item sits between. Nothing else passes through a restriction: editor/commenter/viewer grants and share links above a restricted item still do not apply. This affectsrole,can,explainandauditalike. Owner grants on the item itself, or within the restricted scope, work as before. ["explain", user, item](user not found,item not found, in that order) returns a string for the winning grant. Among all grants that apply to the user on the item (rule 1 included), the winner is chosen by, in order: strongest role; then the nearest to the item (the item itself is distance 0, its parent 1, ...); then source — a grant to the user themself beats a grant to a group, which beats the share link; then the principal name in plain string order (only matters between two groups). The winner's role always equals whatrolereturns.- The string formats are exactly:
- direct:
"editor: ana on specs" - via a group:
"editor: ana > eng > platform on specs". This is the membership chain from the user up to the group holding the grant (each name is a member of the next). If several chains exist, use the one with the fewest hops, and among those the one that compares smallest name by name from left to right. - share link:
"viewer: link on drive" - an owner grant that only applies because of rule 1 gets the suffix
" (owner override)", e.g."owner: ben on drive (owner override)". - nothing applies:
"none". "on X" names the item the winning grant or link is set on.
["audit", item](item not found) returns[[user, role], ...]for every user whose effective role on the item is not"none", strongest role first, ties by user name in plain string order. A share link in scope gives every user in the org at least its role.- Hidden tests build thousands of levels, hundreds of users and deeply nested groups with the
bulk_*helpers, then callauditandexplainrepeatedly. Askingrolefor every user in the org on every audit is too slow.
Example
[["add_user", "ana"], ["add_user", "ben"], ["add_group", "eng"], ["add_group", "platform"],
["add_member", "platform", "eng"], ["add_member", "eng", "ana"],
["add_item", "drive", "folder", null], ["add_item", "specs", "folder", "drive"], ["add_item", "plan", "doc", "specs"],
["grant", "drive", "platform", "editor"], ["grant", "drive", "ben", "owner"], ["set_link", "drive", "viewer"],
["explain", "ana", "plan"], ["audit", "plan"],
["set_restricted", "specs", true], ["explain", "ben", "plan"], ["explain", "ana", "plan"], ["audit", "plan"]]returns [..., "editor: ana > eng > platform on drive", [["ben", "owner"], ["ana", "editor"]], true, "owner: ben on drive (owner override)", "none", [["ben", "owner"]]] (the first twelve results are true).
Notes
tree.py,grants.py,roles.py,errors.pyandbulk.pyare known-good.- You may use the AI assistant freely; you are expected to verify what it produces.
- Later parts must not break earlier ones except where a rule says it changes one; hidden tests re-check earlier behaviour after later features.
Example 1
- Input:
- [["add_user","ana"],["add_group","eng"],["add_group","platform"],["add_member","platform","eng"],["add_member","eng","ana"],["add_item","drive","folder",null],["add_item","plan","doc","drive"],["grant","drive","platform","editor"],["grant","plan","ana","viewer"],["role","ana","plan"],["add_member","eng","platform"],["add_user","ben"],["set_link","drive","viewer"],["role","ben","plan"]]
- Output:
- [true,true,true,true,true,true,true,true,true,"editor","error: membership cycle",true,true,"viewer"]
- Explanation
- Ana reaches the platform grant through eng, and the stronger editor role on the folder beats her viewer grant on the doc. Putting platform inside eng would create a cycle. The link gives the later-created Ben viewer.
Example 2
- Input:
- [... drive(ana editor) / hr(ben viewer) / offer, notes / memo ..., ["set_restricted","hr",true],["role","ana","offer"],["role","ben","offer"],["move_item","notes","wiki"],["role","ana","memo"]]
- Output:
- [..., true, "none", "viewer", true, "commenter"]
- Explanation
- Restricting hr cuts off Ana's grant on drive but Ben's grant on hr itself still applies. Moving notes into wiki swaps the grant that reaches memo.
Example 3
- Input:
- [... platform editor + ben owner + viewer link on drive ..., ["explain","ana","plan"],["audit","plan"],["set_restricted","specs",true],["explain","ben","plan"],["explain","ana","plan"],["audit","plan"]]
- Output:
- [..., "editor: ana > eng > platform on drive", [["ben","owner"],["ana","editor"]], true, "owner: ben on drive (owner override)", "none", [["ben","owner"]]]
- Explanation
- explain names the winning grant and the membership chain. After restricting specs only the owner grant on drive still reaches plan, and it is labelled as an override.
Guidelines
- Python 3 standard library only
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Trees can be 5,000 levels deep and groups can nest 600 deep: do not rely on recursion
- Part 3 inputs have hundreds of users; auditing must not resolve every user from scratch