patches: add v1.11.0 owner-scoped access patch and its leil-rig proof

0003-owner-scoped-access.patch changes authorization, applied after the
de-brand and dark-theme patches:

- a bucket's ACL and policy may name only the owner's keys (accounts on
  the owner's UserID), whoever writes them, root included;
- public access is read-only: all-users READ, and "*" allowed only
  s3:GetObject and s3:ListBucket;
- the bucket policy never denies the owner's key;
- DeleteBucket, PutBucketVersioning, PutObjectLockConfiguration and
  PutBucketOwnershipControls are reserved to root and admins;
- an ACL with the all-users READ grant can be written back unchanged.

docs/owner-scoped-access.md has the rules, what v1.11.0 already did,
and the leil-rig proof procedure with its 2026-11-10 result: 63 of 63
on the patched build, 29 of 63 failing on stock v1.11.0.
tests/owner-scope/ holds the proof script and a stdlib-only SigV4
request helper. README and the pipeline doc list the patch and how to
re-verify it after a rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
57_WolveandClaude Opus 5.5 committed 2026-11-10 23:50:47 -06:00
1 parent 1984762186
commit 152ea01107
6 files changed
+1963 -6

No files matched your search

+18 -1
View File
@@ -110,7 +110,24 @@ When upstream releases `vX.Y.Z`:
VGW_REPO_URL="/path/to/local/versitygw" VGW_TAG=vX.Y.Z bash ci/run.sh
```
A clean `git am` plus a populated `src/dist/` means the series is good.
A clean `git am` plus a populated `src/dist/` means the web UI patches
are good. `0003-owner-scoped-access.patch` changes Go code in `auth/`, so
a clean apply is not enough for it: run its unit tests in the patched
tree, on Linux (on Windows several upstream packages fail regardless),
```bash
go test ./auth/ ./s3api/...
```
and its proof test on leil-rig — see
[owner-scoped-access.md](owner-scoped-access.md#proving-the-patch-is-in-on-leil-rig).
If upstream has reworked `auth/access-control.go` or `auth/acl.go`,
re-read that document's rules against the new code rather than only
resolving the conflict.
`src/` may hold someone's local work tree; `ci/clone-source.sh` deletes
it. Point `VGW_SRC_DIR` somewhere fresh (any `*-src/` name is
gitignored) when in doubt.
## Retiring a version
+134
View File
@@ -0,0 +1,134 @@
# Owner-scoped bucket access (`0003-owner-scoped-access.patch`)
`patches/v1.11.0/0003-owner-scoped-access.patch` applies after the de-brand and dark-theme patches. Unlike them it
changes the gateway's behaviour, not its look: who may be named in a bucket's ACL and policy, what the public may be
given, and which bucket settings only the gateway operator may change.
| | |
|---|---|
| Touches | `auth/acl.go`, `auth/access-control.go`, `auth/owner_scope.go` (new), `s3api/controllers/bucket-put.go`, and tests |
| Base | upstream `v1.11.0` (`e42ae7ab`) + `0001` + `0002` |
| Unit tests | `go test ./auth/ ./s3api/...` (table-driven; `owner_scope_test.go` in `auth` and `s3api/controllers`) |
| Proof test | [`tests/owner-scope/proof.sh`](../tests/owner-scope/proof.sh), on leil-rig, below |
## The rules
A bucket belongs to the account its ACL names as owner — the key in the `user.acl` xattr — and to the person behind
that account, who may hold several keys: every key whose IAM account carries the owner account's `UserID`. Sharing a
bucket means handing someone one of those keys, never naming somebody else's.
| Rule | Where | Refused with |
|---|---|---|
| **Only the owner's keys.** Every account named by `PutBucketPolicy`, `PutBucketAcl` (body and `x-amz-grant-*` headers) and `CreateBucket`'s grant headers — so also `versitygw admin create-bucket --grant-*` — must have the owner's `UserID`. The owner is looked up in IAM too: an owner unknown to IAM, or with `UserID` 0, can name no other key. The check is on the document, not the caller: root is refused too. | `auth.UpdateACL`, `auth.VerifyPolicyOwnerScope` | `403 AccessDenied` *"&lt;key&gt; is not an account of the bucket owner &lt;owner&gt;"* |
| **Public means read.** `all-users` may only be granted `READ`; a policy may `Allow` `"*"` only `s3:GetObject` and `s3:ListBucket`. Canned `public-read-write`, any other `all-users` grant and any other public `Allow` — wildcards such as `s3:Get*` included — are refused. A `Deny` for `"*"` grants nothing and is accepted. | same | `403 AccessDenied` *"public access is read-only: …"* |
| **A policy adds to the ACL.** An `Allow` from either is enough; a policy `Deny` wins — except that the bucket owner's key is never denied by the bucket policy. The owner's *other* keys get no exemption. | `auth.verifyResourceAccess` | — |
| **Gateway root/admin only:** `DeleteBucket`, `PutBucketVersioning`, `PutObjectLockConfiguration`, `PutBucketOwnershipControls` (and `DeleteBucketOwnershipControls`, which authorizes as it). Refused to the owner, to grantees, to anything a policy grants and to public requests, before any ACL or policy is read. | `auth.verifyAccessGates` | `403 AccessDenied` *"s3:DeleteBucket is reserved to the gateway's root and admin accounts"* |
| An ACL holding the `all-users` `READ` grant, exactly as `GetBucketAcl` returns it, can be written back with `PutBucketAcl`. Upstream looks `all-users` up as an account and answers `500`. | `auth.UpdateACL` | — |
### What v1.11.0 already did, and what is new
The request was written against a checkout of upstream `main` at `4dc0debf`. That checkout is **older** than
v1.11.0, not newer — `git describe` gives `v1.8.0-78-g4dc0debf` and v1.11.0 is not in its history — and v1.11.0 has
already reworked the code it describes:
- *"`verifyResourceAccess` skips the ACL once a policy exists"* — true at `4dc0debf`, not in v1.11.0. There a policy
statement that does not match falls back to the ACL, and a policy `Deny` wins, so "a policy adds to the ACL" was
already the behaviour. The proof test's control run below confirms it: a key named only in a policy works on stock
v1.11.0.
- *"`verifyBucketPolicy` gives the owner no exemption"* — v1.11.0 exempts the owner only for reading, replacing and
deleting the policy itself (`isBucketPolicyAction`). A `Deny` naming the owner, or `"*"`, still locks the owner
out of everything else. 0003 widens the exemption to every action and removes `isBucketPolicyAction`.
- `PutBucketOwnershipControls`, `PutBucketVersioning` and `PutObjectLockConfiguration` live in
`s3api/controllers/bucket-put.go` as stated; `DeleteBucket` and `DeleteBucketOwnershipControls` are in
`bucket-delete.go`. The gate is in `auth`, so the controllers are unchanged.
### Edges worth knowing
- **ARN principals** (the standalone IAM service, `PrincipalResolver`) skip the per-principal `UserID` check: that
backend fixes every bucket's owner to root and gives every identity root's configured `UserID`, so the rule holds
by construction. The public read-only rule still applies.
- **The check is at write time.** A policy or ACL stored before this build keeps working as stored. The
root/admin-only gate is the exception: it is evaluated per request, so a stored grant of those four actions stops
working at once.
- **An unknown grantee** still gets upstream's error (`accounts does not exist: …`, a `500`); 0003 only adds refusals.
- **Writing `user.acl` directly** (leil-cluster's `leilfs-user` appends same-UID grantees with `setfattr`) bypasses
the gateway and so this check. Today it only ever adds the user's own keys, which the rule allows.
- **Bucket creation** still lets a `userplus` creator set object lock and object ownership on its own new bucket;
only changing them afterwards is reserved.
## Proving the patch is in (on leil-rig)
`--version` reads `1.11.0` with or without the patch, so the proof is behaviour. `tests/owner-scope/proof.sh` starts
a throwaway gateway from a given binary on `127.0.0.1:7390`, with its own internal IAM and posix root under
`/var/tmp/s3gw-owner-scope`, drives every rule through signed S3 requests (`tests/owner-scope/s3req.py`, Python
standard library only, so nothing is installed), and deletes the lot again. Nothing installed is touched — no
package, unit or config — so it does not disturb whichever `versitygw` the rig is pinned to.
It creates four accounts through the admin API, all on leil-cluster's reserved test band:
| Key | UID | Role | Is |
|---|---|---|---|
| `po-owner` | 74990 | userplus | owner of bucket `proof` (`create-bucket --owner po-owner --object-ownership BucketOwnerPreferred --grant-full-control po-full`) |
| `po-full` | 74990 | user | a `FULL_CONTROL` grantee — one of the owner's keys |
| `po-obj` | 74990 | user | in no ACL; named in one policy statement allowing `s3:GetObject`/`PutObject`/`DeleteObject` on `shared/report.txt` |
| `po-foreign` | 74991 | user | somebody else's key |
Take the lease first, and give it back when done (`rig-lease.sh` is in `../leil-rig`):
```bash
RIG_LEASE_WHO="<you>: s3-gateway 0003 proof" ./bin/rig-lease.sh claim --why "s3-gateway 0003 proof on leil-test-1" --minutes 60
```
Build, then copy the binary and the test to the test node (it runs no cluster service, and has `python3`):
```bash
VGW_SRC_DIR=build-src VGW_TAG=v1.11.0 bash ci/run.sh
cd ../leil-rig && source bin/rig-lib.sh
on leil-test-1 'install -d -o leil -g leil /var/tmp/s3gw-0003'
rig_scp ../s3-gateway/build-src/dist/versitygw_linux_amd64_v1/versitygw leil-test-1 /var/tmp/s3gw-0003/versitygw
tar -czf /tmp/owner-scope.tgz -C ../s3-gateway/tests owner-scope
rig_scp /tmp/owner-scope.tgz leil-test-1 /var/tmp/s3gw-0003/owner-scope.tgz
on leil-test-1 'cd /var/tmp/s3gw-0003 && chmod 0755 versitygw && tar -xzf owner-scope.tgz && bash owner-scope/proof.sh $PWD/versitygw'
```
(`VGW_SRC_DIR` keeps `ci/run.sh` away from `src/`, which it would `rm -rf`.) Every line must read `PASS` and the last
`N passed, 0 failed`; the exit status says the same. `KEEP=1` leaves the gateway directory and its log behind,
`PROOF_PORT` moves it off 7390. What it covers, in order:
| Checks | Shows |
|---|---|
| A1–A6 | a foreign key refused as a grantee — by `admin create-bucket --grant-full-control` as root (and no bucket made), by `PutBucketAcl` grant header as the owner and as root, by `PutBucketAcl` body as root — while the owner's own key is granted |
| B1–B2 | a foreign key refused as a policy principal, as the owner and as root |
| C1–C8 | `"*"` `s3:PutObject`, canned `public-read-write` and an `all-users` `WRITE` body refused; a policy allowing `"*"` `s3:GetObject` + `s3:ListBucket` accepted, after which anonymous `GetObject` and `ListBucket` work and anonymous `PutObject` does not |
| D1–D4 | the owner key still writes, reads, deletes and reads the ACL under that public-read-only policy |
| E1–E8 | the owner key still writes, reads, lists and deletes under a policy denying `po-owner` `s3:*` and `"*"` `s3:PutObject`, while the same `Deny` beats `po-full`'s `FULL_CONTROL` grant; the owner then deletes the policy |
| F1–F12 | `po-obj` puts, gets and deletes `shared/report.txt` and nothing else: no other object, no listing, no ACL or policy read or write |
| G0–G11 | with the owner's policy granting `po-full` all four, `po-full` is refused ownership controls (put and delete), versioning, object lock and `DeleteBucket`; so is the owner; `po-full`'s object grant still works, and root changes ownership controls |
| H1–H5 | after canned `public-read`, the ACL `GetBucketAcl` returns is written back unchanged and still grants `all-users` `READ` |
| Z1 | root deletes the bucket |
Release the lease and clean up: `on leil-test-1 'rm -rf /var/tmp/s3gw-0003'`, then `./bin/rig-lease.sh release`.
### Result on leil-rig, 2026-11-10: **PASSED**
Run on `leil-test-1` (AlmaLinux 9.8, kernel 5.14.0-687.53.1, Python 3.9.25) under the lease, from a `ci/run.sh`
build of this series:
| | |
|---|---|
| Build | `VGW_TAG=v1.11.0 bash ci/run.sh` (fresh clone of `v1.11.0`, 0001–0003 by `git am --3way`, GoReleaser); `--version` 1.11.0, `Build` `98b6c69b` |
| `versitygw` (linux amd64) sha256 | `4e06d35b746602ce4c1de4a3f3c307c4648faa2f058625f72305a8848002bfd4` |
| `versitygw_1.11.0_linux_amd64.rpm` sha256 | `e4b0e77ecf003e6ec1d57757d841941ded0bcbd9175e668a6794c0a1c6bf33ba` (built locally; not published, not pinned) |
| `proof.sh` | **63 passed, 0 failed** |
| Unit tests on the node (go1.27.3 linux/amd64) | `go test` of every package except `plugins/`: 35 of 35 ok, among them `auth` and `s3api/...` |
**Control: stock v1.11.0** (0001 + 0002 only, same script, same node): **34 passed, 29 failed.** Root and the owner
named `po-foreign` as grantee and as principal (A1–A5, B1–B2); `"*"` `PutObject` and `public-read-write` were
accepted, and anonymous `PutObject` then succeeded (C1, C2, C8); an `all-users` `WRITE` body and the `GetBucketAcl`
round trip answered `500` (C3, H3); the owner's own `Deny` locked it out of put, get, list and delete (E2–E5); with
the bucket public-read-write, `po-obj` reached objects beyond its one (F4–F7); and the policy grant let `po-full` —
and the owner — change ownership controls, enable object lock, and reach versioning and `DeleteBucket` (G1–G8, which
failed only on "versioning not configured" and "bucket not empty"). Stock v1.11.0 passed F1–F3 and F8–F12 on its own:
the single-object key already worked there; 0003 keeps it working.
Run this test again before pinning any build that carries 0003, in leil-rig or leil-cluster.