|
| 1 | +# ADR: Nested Shared Folders |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +Draft |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +Support sharing a folder that is already inside another shared folder without |
| 10 | +turning the child folder into a restrictive boundary by default. Nested folder |
| 11 | +shares are additive: parent access still applies, and the child share can add |
| 12 | +new users or higher rights. A future `limited_access` mode can make a child |
| 13 | +folder restrictive, but that is not part of the first implementation. |
| 14 | + |
| 15 | +## Context |
| 16 | + |
| 17 | +Today, shared drives are treated as isolated top-level sharing boundaries. The |
| 18 | +backend prevents creating a shared drive from a folder or file that is already |
| 19 | +shared, is inside another shared drive, or contains another shared drive. This |
| 20 | +keeps permission checks simple because a file belongs to a single effective |
| 21 | +shared-drive scope. |
| 22 | + |
| 23 | +The nested shared folders user story changes that model. A parent folder can be |
| 24 | +shared with one set of users, and a subfolder can be shared with additional |
| 25 | +users. Users who have access through the parent should keep access to the child |
| 26 | +folder; users added directly on the child should see only that child folder when |
| 27 | +they do not have access to the parent. |
| 28 | + |
| 29 | +This is different from a Google-style limited access folder. Limited access is a |
| 30 | +separate feature where parent access stops at the child folder. We want the data |
| 31 | +model and permission resolver to leave room for that future mode, but the first |
| 32 | +iteration should implement normal nested sharing as additive inheritance. |
| 33 | + |
| 34 | +### Endpoint Impact And Permission Check Complexity |
| 35 | + |
| 36 | +| UI journey | Endpoint called | What to check and why | Complexity | |
| 37 | +|---|---|---|---| |
| 38 | +| Create a shared folder from a folder inside another shared folder | `POST /sharings/drives` | Check the caller can read/share the selected root and can manage sharing in the parent effective scope, because the child share adds a new direct access path. | **Medium**: current anti-nesting validation must change, and inherited access must be derived. | |
| 39 | +| Create a file share inside a shared folder | existing file sharing / sharing creation endpoints | Check the caller can read/share the file in its effective scope. The file share adds access but does not become a boundary. | **Low to Medium**: additive file shares already fit the model, but nested containment checks must not treat them as drive boundaries. | |
| 40 | +| Create a new shared folder by name | `POST /sharings/drives` | Check `POST` on the parent folder, because a real VFS folder is created before it is shared. | **Low**: mostly existing folder-create permission. | |
| 41 | +| Open parent folder containing shared child folders | `GET /sharings/drives/:id/:file-id` | Check effective `GET` through parent and additive child scopes. Parent users can still open child content unless future `limited_access` blocks inheritance. | **Medium**: listing may need to annotate child shared roots and inherited/direct access. | |
| 42 | +| Open child shared folder directly | `GET /sharings/drives/:child-id/:file-id` | Check effective `GET` for the child scope. Direct child recipients can enter through the child share even if they cannot see the parent. | **Medium**: route checks must accept direct child access and inherited parent access where applicable. | |
| 43 | +| Rename/move item inside one effective scope | `PATCH /sharings/drives/:id/:file-id` | Check effective write on the item and write/create on the destination parent. | **Medium**: source and destination may have different additive scopes. | |
| 44 | +| Move item from parent shared folder into child shared folder | `PATCH /sharings/drives/:parent-id/:file-id` or `POST /sharings/drives/move` | Check write/remove on source and write/create on destination; effective access after the move is parent plus child additive scopes. | **Medium**: destination effective access must be recomputed. | |
| 45 | +| Move item from child shared folder back to parent | `PATCH /sharings/drives/:child-id/:file-id` or `POST /sharings/drives/move` | Check write/remove on child source and write/create on parent destination; preserve direct child access only if product requires it. | **Medium**: moving can change inherited access. | |
| 46 | +| Move shared folder into another shared folder | `POST /sharings/drives/move` | Compute effective users before the move, then merge with destination inherited users and keep the higher permission. | **High**: must materialize missing direct grants to avoid losing pre-move access. | |
| 47 | +| Copy/move folder containing nested shared roots | `POST /sharings/drives/move` | Find nested shared roots under the moved folder and apply merge/preserve rules for each impacted share. | **High**: proportional to nested shared roots and needs clear copy-vs-move semantics. | |
| 48 | +| Download file through parent or child shared route | `POST /sharings/drives/:id/downloads`, then `GET /sharings/drives/:id/downloads/:secret/:name` | Check effective `GET` for the requested file and verify the route is a valid access path for that user. | **Medium**: existing broad route checks need effective-scope validation. | |
| 49 | +| Download archive from a shared folder | `POST /sharings/drives/:id/archive` | Check effective `GET` for archive entries. For v1 additive sharing, inherited parent access can include child content. | **Medium to High**: archive construction still walks content and must account for nested shared roots. | |
| 50 | +| Create share-by-link inside nested shared folder | `POST /sharings/drives/:id/permissions` | Check effective `GET` on target and link-creation rights in the effective scope. | **Medium**: current containment checks must become effective-access checks. | |
| 51 | +| List share-by-link permissions | `GET /sharings/drives/:id/permissions?ids=...` | Check each requested ID is visible through effective access. | **Medium**: batch validation needed for all IDs. | |
| 52 | +| Edit/remove share-by-link | `PATCH /sharings/drives/:id/permissions/:perm-id` or `DELETE /sharings/drives/:id/permissions/:perm-id` | Check the permission doc belongs to a scope the caller can manage. | **Medium**: direct and inherited managers may differ. | |
| 53 | +| Watch live changes in parent shared folder | `GET /sharings/drives/:id/_changes` or `GET /sharings/drives/:id/realtime` | Send events only when the user has effective access to the changed item. | **High**: path-prefix filtering must be replaced or supplemented by effective-access filtering. | |
| 54 | +| Show shared folders in sidebar / shared section | `GET /sharings/drives` | Return direct shares and enough metadata to distinguish inherited users from direct recipients. | **Medium**: UI needs both "Shared with me" and "Shared by me" behavior from the user story. | |
| 55 | +| Delete/trash shared parent folder containing nested shared folders | `DELETE /sharings/drives/:id/:file-id` or `DELETE /files/:file-id` | Find nested shared roots and decide whether deletion removes them for everyone or requires preserving direct shares. | **High**: destructive operation with revocation and orphaning risk. | |
| 56 | +| Parent access removed while child direct access remains | `GET /sharings/drives`, child routes | Recompute effective access so the user loses parent-derived access but keeps direct child access. | **Medium**: requires derived inheritance instead of copied inherited members. | |
| 57 | +| Background sync/replication | internal `io.cozy.shared` updates | Track sharing refs according to effective additive scopes and preserve direct child access. | **High**: fan-out, locking, and replication semantics become more complex. | |
| 58 | + |
| 59 | +## Models Considered |
| 60 | + |
| 61 | +- Yandex Disk avoids nested shared folders entirely. |
| 62 | +- Google Drive normally uses inherited, additive sharing. It supports |
| 63 | + restrictive child folders only through a special limited access feature. |
| 64 | +- SharePoint, OneDrive, and Nextcloud are closer to a full ACL model, where |
| 65 | + folders or files can have inherited and overridden permissions. |
| 66 | + |
| 67 | +For Twake, the first implementation should follow the normal additive behavior: |
| 68 | +share downward by inheritance, add access on child folders, and defer |
| 69 | +restrictive limited access until the product explicitly needs that mode. |
| 70 | + |
| 71 | +## Decision |
| 72 | + |
| 73 | +Nested folder sharing will be additive by default. |
| 74 | + |
| 75 | +An `io.cozy.sharings` document remains the sharing scope and member list. The |
| 76 | +shared root remains marked on `io.cozy.files` with `referenced_by`. For nested |
| 77 | +folder shares, effective access is derived from all applicable additive sharing |
| 78 | +scopes on the path. |
| 79 | + |
| 80 | +Rules: |
| 81 | + |
| 82 | +- Parent folder access applies to child folders unless a future |
| 83 | + `limited_access` mode explicitly blocks inheritance. |
| 84 | +- Child folder sharing can add users or grant higher rights. |
| 85 | +- If the same user has access through parent and child scopes, the higher right |
| 86 | + wins. |
| 87 | +- File shares are additive access only. They do not create restrictive |
| 88 | + boundaries. |
| 89 | +- Limited access is reserved as an explicit access mode for a later iteration. |
| 90 | + |
| 91 | +## Solution |
| 92 | + |
| 93 | +Add an explicit access mode to sharing documents: |
| 94 | + |
| 95 | +```text |
| 96 | +io.cozy.sharings |
| 97 | +- access_mode: additive | limited_access |
| 98 | +``` |
| 99 | + |
| 100 | +The default value is `additive` for existing and newly created nested folder |
| 101 | +shares. `limited_access` is reserved for the future feature and should not be |
| 102 | +enabled by this first implementation. |
| 103 | + |
| 104 | +The permission resolver should distinguish between sharing scopes and |
| 105 | +restrictive boundaries: |
| 106 | + |
| 107 | +```text |
| 108 | +effectiveAccessScopes(file_or_folder) |
| 109 | + returns additive parent and child sharing scopes that apply to the item |
| 110 | +
|
| 111 | +nearestRestrictiveBoundary(file_or_folder) |
| 112 | + reserved for future limited_access mode |
| 113 | +
|
| 114 | +childSharedRootsUnder(folder) |
| 115 | + returns nested shared roots impacted by recursive operations |
| 116 | +``` |
| 117 | + |
| 118 | +For v1, `effectiveAccessScopes` is the main permission check. A user can access |
| 119 | +an item when they are a member of any applicable additive scope. The final |
| 120 | +permission is the highest permission found across those scopes. |
| 121 | + |
| 122 | +Example: |
| 123 | + |
| 124 | +```text |
| 125 | +Folder A shared with Alice as editor |
| 126 | + Folder B shared with Bob as viewer |
| 127 | +``` |
| 128 | + |
| 129 | +Effective access to `Folder B`: |
| 130 | + |
| 131 | +```text |
| 132 | +Alice: editor, inherited from Folder A |
| 133 | +Bob: viewer, direct access from Folder B |
| 134 | +``` |
| 135 | + |
| 136 | +Bob should see `Folder B` in "Shared with me" without seeing `Folder A`. The |
| 137 | +owner/editor should see both `Folder A` and `Folder B` in "Shared by me" as |
| 138 | +separate shared items. |
| 139 | + |
| 140 | +## Implementation Notes |
| 141 | + |
| 142 | +Creation: |
| 143 | + |
| 144 | +- Allow creating a folder share inside an existing shared folder. |
| 145 | +- Reject or keep out of scope any operation that tries to create a restrictive |
| 146 | + nested folder by default. |
| 147 | +- Store `access_mode = additive` explicitly or treat missing `access_mode` as |
| 148 | + `additive` for backwards compatibility. |
| 149 | + |
| 150 | +Permission checks: |
| 151 | + |
| 152 | +- Replace single-scope checks with an effective access resolver for nested |
| 153 | + folder content. |
| 154 | +- Show inherited users separately from direct users in the sharing dialog when |
| 155 | + the UI needs to explain why a user has access. |
| 156 | +- Do not copy inherited parent members into every child sharing during normal |
| 157 | + updates. Derive inherited access from the parent chain. |
| 158 | + |
| 159 | +Move: |
| 160 | + |
| 161 | +- Moving a shared folder into another shared folder should merge permissions. |
| 162 | +- Users who had effective access to the moved folder before the move must keep |
| 163 | + access after the move. |
| 164 | +- Users who have access to the destination folder gain access through the new |
| 165 | + parent inheritance. |
| 166 | +- If the same user exists in both sources with different rights, keep the higher |
| 167 | + right. |
| 168 | +- To preserve pre-move access when parent inheritance changes, compute the |
| 169 | + moved folder's effective access before the move and materialize missing users |
| 170 | + as direct grants on the moved folder sharing. |
| 171 | + |
| 172 | +Future limited access: |
| 173 | + |
| 174 | +- `limited_access` will be the mode that turns a child folder into a hard |
| 175 | + boundary. |
| 176 | +- When a `limited_access` boundary is encountered, parent scopes above it should |
| 177 | + stop applying. |
| 178 | +- Move, delete, archive, download, realtime, and sync flows will need additional |
| 179 | + boundary checks before enabling that mode. |
| 180 | + |
| 181 | +## Consequences |
| 182 | + |
| 183 | +This approach matches the user story for nested shared folders and avoids a full |
| 184 | +ACL model. It keeps file shares additive and keeps inherited parent access |
| 185 | +derived instead of copied. |
| 186 | + |
| 187 | +The tradeoff is that permission checks become more complex than the current |
| 188 | +single shared-drive scope. The backend needs shared helpers for effective access |
| 189 | +resolution and for finding nested shared roots impacted by recursive operations. |
| 190 | + |
| 191 | +Limited access remains possible later, but it must be implemented as an |
| 192 | +explicit mode with additional checks instead of being the default behavior of |
| 193 | +nested sharing. |
0 commit comments