|
| 1 | +# Patch Documentation: Immutable & Container-Specific Permissions |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +- **cs3org/cs3apis#272**: MERGED (4 Jun 2026) |
| 6 | +- **cs3org/go-cs3apis**: Regenerated (c3fdb0aa5e9e) |
| 7 | +- **opencloud-eu/reva#674**: go-cs3apis update PR (Housekeeping, waiting for review) |
| 8 | +- **cs3org/reva#5628**: Open, ACL encoding |
| 9 | +- **opencloud-eu/opencloud#2841**: EditorLitePlus role (independent, ready) |
| 10 | +- Feature branch ready, compiles with new go-cs3apis. |
| 11 | + |
| 12 | +## Overview |
| 13 | + |
| 14 | +Two features spanning 4 repositories: |
| 15 | + |
| 16 | +1. **Container-specific permissions** (`DeleteContainer`, `MoveContainer`) — separate file and directory delete/move operations |
| 17 | +2. **Immutable attribute** (freeze/protect) — persistent xattr to protect resources from modification |
| 18 | + |
| 19 | +Reference: [cs3org/cs3apis#272](https://github.com/cs3org/cs3apis/pull/272) |
| 20 | + |
| 21 | +## Repositories and Branches |
| 22 | + |
| 23 | +| Repo | Branch | PR | Purpose | |
| 24 | +|------|--------|-----|---------| |
| 25 | +| `flash7777/reva` | `chore/update-go-cs3apis` | [opencloud-eu/reva#674](https://github.com/opencloud-eu/reva/pull/674) | go-cs3apis update + stubs (Housekeeping) | |
| 26 | +| `flash7777/reva` | `feature/immutable-decomposedfs` | Pending (after #674) | decomposedfs, roles, ACL, WebDAV, tests | |
| 27 | +| `flash7777/opencloud` | `feature/immutable-and-container-permissions` | Pending | Graph API actions | |
| 28 | +| `flash7777/opencloud` | `feature/editor-light-role` | [opencloud-eu/opencloud#2841](https://github.com/opencloud-eu/opencloud/pull/2841) | EditorLitePlus role | |
| 29 | +| `flash7777/web` | `feature/immutable-ui` | Pending | Frontend: Resource model, icons, sidebar | |
| 30 | + |
| 31 | +## Release Strategy |
| 32 | + |
| 33 | +``` |
| 34 | +cs3org/cs3apis#272 MERGED ✓ |
| 35 | + ↓ |
| 36 | +cs3org/go-cs3apis regenerated ✓ (c3fdb0aa5e9e) |
| 37 | + ↓ |
| 38 | +PR 1: opencloud-eu/reva#674 — go-cs3apis update + stubs (waiting for review) |
| 39 | + ↓ |
| 40 | +PR 2: opencloud-eu/reva — feature/immutable-decomposedfs (rebase on #674, then submit) |
| 41 | + ↓ |
| 42 | +PR 3: opencloud-eu/opencloud — Graph API actions (after reva release with PR 2) |
| 43 | + ↓ |
| 44 | +PR 4: opencloud-eu/web — Frontend (independent, can merge anytime) |
| 45 | +``` |
| 46 | + |
| 47 | +Note: `flash7777/reva` is a fork of cs3org/reva (same GitHub fork network as |
| 48 | +opencloud-eu/reva), which allows cross-repo PRs against opencloud-eu/reva. |
| 49 | +`flash7777/reva-eu` is a standalone repo (cannot create PRs). |
| 50 | + |
| 51 | +## What was changed — by layer |
| 52 | + |
| 53 | +### Layer 1: CS3 Proto Spec |
| 54 | + |
| 55 | +**cs3org/cs3apis#272** — APPROVED |
| 56 | + |
| 57 | +`ResourcePermissions` new fields: |
| 58 | +- `delete_container` (21) — can delete containers |
| 59 | +- `move_container` (22) — can move/rename containers |
| 60 | +- `set_immutable_file` (23) — can freeze files (irreversible) |
| 61 | +- `set_immutable_container` (24) — can protect/unprotect containers |
| 62 | + |
| 63 | +`ResourceInfo` new field: |
| 64 | +- `immutable` (20) — resource is frozen/protected |
| 65 | + |
| 66 | +`ProviderAPI` new RPCs: |
| 67 | +- `SetImmutable` / `UnsetImmutable` |
| 68 | + |
| 69 | +Immutable semantics documented in `docs/proposals/immutable-overview.md`: |
| 70 | +- File (freeze): content fixed, irreversible |
| 71 | +- Container (protect): structure fixed, reversible by manager/admin |
| 72 | +- Self vs. parent rule: object is immutable if own OR parent attribute is set |
| 73 | + |
| 74 | +### Layer 2: Go Bindings |
| 75 | + |
| 76 | +**cs3org/go-cs3apis** — will be regenerated by CERN after cs3apis#272 merge. |
| 77 | + |
| 78 | +`flash7777/go-cs3apis` was used for local testing only. The reva branch |
| 79 | +has **no** `replace` directive — it references the upstream go-cs3apis and |
| 80 | +will compile once the fields are available. |
| 81 | + |
| 82 | +### Layer 3: Reva Storage (opencloud-eu/reva) |
| 83 | + |
| 84 | +#### Roles (`pkg/conversions/role.go`) |
| 85 | + |
| 86 | +| Role | DeleteContainer | MoveContainer | SetImmutableFile | SetImmutableContainer | |
| 87 | +|------|:-:|:-:|:-:|:-:| |
| 88 | +| Viewer | - | - | - | - | |
| 89 | +| EditorLite | - | - | - | - | |
| 90 | +| Editor | **true** | **true** | - | - | |
| 91 | +| SpaceEditor | **true** | **true** | - | - | |
| 92 | +| SpaceEditorWithoutVersions | **true** | **true** | - | - | |
| 93 | +| Coowner | **true** | **true** | **true** | **true** | |
| 94 | +| Manager | **true** | **true** | **true** | **true** | |
| 95 | + |
| 96 | +**Breaking change:** Directory delete/move requires explicit `DeleteContainer`/`MoveContainer`. |
| 97 | + |
| 98 | +#### Storage Interface (`pkg/storage/storage.go`) |
| 99 | + |
| 100 | +New methods: `SetImmutable(ctx, ref)`, `UnsetImmutable(ctx, ref)` |
| 101 | + |
| 102 | +#### decomposedfs Implementation |
| 103 | + |
| 104 | +**xattr:** `user.oc.immutable` (`prefixes.go`) |
| 105 | + |
| 106 | +**Node methods** (`node/node.go`): |
| 107 | +- `ImmutableState` enum: `ImmutableNone` (0), `ImmutableProtected` (1), `ImmutableFrozen` (2) |
| 108 | +- `IsImmutable()` — check self |
| 109 | +- `GetImmutableState()` — effective state (self + parent) |
| 110 | +- `FreezeFile()` — irreversible, rejects dirs |
| 111 | +- `ProtectContainer()` — rejects files |
| 112 | +- `UnprotectContainer()` — rejects files |
| 113 | + |
| 114 | +**Handler checks** (`decomposedfs.go`): |
| 115 | +- `Delete`: `DeleteContainer` check + immutable self/parent |
| 116 | +- `Move`: `MoveContainer` check + immutable source/target |
| 117 | +- `CreateDir`: parent immutable check |
| 118 | +- `SetImmutable()`: permission check (`SetImmutableFile`/`SetImmutableContainer`) |
| 119 | +- `UnsetImmutable()`: rejects files, checks permission |
| 120 | + |
| 121 | +**Upload** (`upload.go`): |
| 122 | +- Frozen file → no overwrite |
| 123 | +- Protected parent → no new file, no modification |
| 124 | + |
| 125 | +**Stat** (`node.go` `AsResourceInfo()`): |
| 126 | +- `ResourceInfo.Immutable` set from xattr |
| 127 | +- `immutable-state` in Opaque: `frozen` or `protected` |
| 128 | + |
| 129 | +#### ACL Encoding (`pkg/storage/utils/grants/grants.go`) |
| 130 | + |
| 131 | +New flags: `+dc`/`!dc`, `+mc`/`!mc` |
| 132 | +Fix: strip container flags before parsing `!d` to avoid substring collision. |
| 133 | + |
| 134 | +#### WebDAV PROPFIND (`internal/http/services/owncloud/ocdav/propfind/`) |
| 135 | + |
| 136 | +- `oc:permissions`: strip `D` (delete) and `NV` (rename/move) when effectively immutable |
| 137 | +- `oc:immutable`: new property — `frozen`, `protected`, or NotFound |
| 138 | +- Both allprops and named property requests supported |
| 139 | + |
| 140 | +#### Legacy drivers |
| 141 | + |
| 142 | +All non-decomposedfs drivers get `NotSupported` stubs. |
| 143 | + |
| 144 | +### Layer 4: Graph API (opencloud-eu/opencloud) |
| 145 | + |
| 146 | +New LibreGraph actions: |
| 147 | +- `libre.graph/driveItem/container/delete` |
| 148 | +- `libre.graph/driveItem/container/move` |
| 149 | +- `libre.graph/driveItem/immutable/file/set` |
| 150 | +- `libre.graph/driveItem/immutable/container/set` |
| 151 | + |
| 152 | +Bidirectional mapping in `conversion.go`. |
| 153 | +Vendor patches for go-cs3apis and reva role.go (temporary until dependency update). |
| 154 | + |
| 155 | +### Layer 5: Web Frontend (opencloud-eu/web) |
| 156 | + |
| 157 | +- `DavProperty.Immutable`: new WebDAV property, added to Default request list |
| 158 | +- `Resource.immutableState`: `'frozen' | 'protected' | undefined` |
| 159 | +- `canBeDeleted()` / `canRename()`: return `false` when immutableState is set |
| 160 | +- `FileDetails.vue` sidebar: shield icon (filled = frozen, outline = protected) with label |
| 161 | + |
| 162 | +### Layer 6: EditorLitePlus Role (independent) |
| 163 | + |
| 164 | +**opencloud-eu/opencloud#2841** — new sharing role: edit without delete. |
| 165 | +Uses only existing CS3 permissions, no dependency on cs3apis#272. |
| 166 | + |
| 167 | +## Tests |
| 168 | + |
| 169 | +| Suite | Tests | Status | |
| 170 | +|-------|:-----:|:------:| |
| 171 | +| Node (IsImmutable, GetImmutableState, Freeze, Protect, Unprotect, Parent rule) | 6 | PASS | |
| 172 | +| Handler (Delete/Move/CreateDir frozen/protected, Upload protected, SetImmutable permissions) | 7 | PASS | |
| 173 | +| Grants ACL (+dc/!dc/+mc/!mc encoding, substring collision) | 8 | PASS | |
| 174 | +| Existing tests (recycle, delete) updated with DeleteContainer | 7 fixes | PASS | |
| 175 | +| Total new/modified tests | **28** | **All PASS** | |
| 176 | + |
| 177 | +`posix/tree` failure is pre-existing on main. |
| 178 | + |
| 179 | +## Concepts: Lock vs. Protected vs. Frozen |
| 180 | + |
| 181 | +| | Lock | Protected | Frozen | |
| 182 | +|---|---|---|---| |
| 183 | +| Purpose | Collaborative editing | Structure protection (parent) | Content protection (self) | |
| 184 | +| Duration | Temporary (expires) | Until manager removes | **Permanent** (files) | |
| 185 | +| Set by | Any user with write access | Manager/Admin | Manager/Admin | |
| 186 | +| Affects | Parallel writes | Delete/move/rename of object | All modifications | |
| 187 | +| Reversible | Yes (expires or unlock) | Yes (UnprotectContainer) | **No** (files) / Yes (containers) | |
| 188 | +| Icon | 🔒 Lock | 🛡️ Shield outline | 🛡️ Shield filled | |
| 189 | +| WebDAV | `d:lockdiscovery` | `oc:immutable="protected"` | `oc:immutable="frozen"` | |
| 190 | + |
| 191 | +## Migration |
| 192 | + |
| 193 | +No data migration needed. The `user.oc.immutable` xattr is only set explicitly via `SetImmutable()`. Existing deployments are unaffected. |
| 194 | + |
| 195 | +`DeleteContainer`/`MoveContainer` are added to all existing roles that have `Delete`/`Move`. No grants break — OpenCloud uses Unified Roles exclusively, not manual permission grants. |
| 196 | + |
| 197 | +## Dependency chain |
| 198 | + |
| 199 | +``` |
| 200 | +cs3org/cs3apis#272 → cs3org/go-cs3apis → opencloud-eu/reva → opencloud-eu/opencloud |
| 201 | + → opencloud-eu/web |
| 202 | +``` |
| 203 | + |
| 204 | +No personal forks in any dependency. The flash7777/* forks were used for |
| 205 | +development and local testing only. PRs will be created against the |
| 206 | +upstream repos once go-cs3apis has the new fields. |
0 commit comments