Repository navigation
feat(s3/bucket): allow tagging, attribute and bucket configuration reads in policies - #86
Merged
Merged
Conversation
…ads in policies Hilt is giving each S3 operation the gateway serves its own permission instead of letting unrecognized subresources fall through to the bare bucket or object operation. A principal could reach these through s3:ListBucket, s3:GetObject and the plain object writes, so a policy has to be able to grant them by name: object tagging, GetObjectAttributes, and reads of the bucket's ACL, CORS, location, object lock, tagging and versioning configuration. The configuration writes stay out, since they change the bucket for every principal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/ingot
that referenced
this pull request
Oct 20, 2026
…ries naming a version Hilt now authorizes DeleteObjects on s3:DeleteObject, as S3 does, where it used to classify the request as CreateBucket. S3 also requires s3:DeleteObjectVersion for an entry that names a version, and those entries are in the body, where Hilt cannot see them. The IAM service now carries the key's action sets on the request (reqscope.ActionsKey, the same per-key cache the fast path reads), and DeleteObjects fails such an entry with AccessDenied unless the key holds the permission on the bucket. Bumps hilt to the classification that mirrors the gateway's router (fil-forge/hilt#102) and libforge to the matching policy actions (fil-forge/libforge#86). The itest key carries the new permissions behind INGOT_ITEST_ROUTE_PERMS until the published hilt image knows them, and TestForgeRoutePermissions covers the refusals through the real hilt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The focused permission additions and validation tests match the intended scope, with no blocking issues identified.
No open issues 🎉
What changed in this PR
Expands bucket-policy permissions to support Hilt’s operation-specific authorization while keeping bucket configuration writes excluded.
Changes:
- Allows object tagging, object attributes, and six bucket configuration reads.
- Classifies configuration reads as bucket-ARN actions.
- Adds tests for ARN validation and rejected configuration writes.
| File | Description |
|---|---|
commands/s3/bucket/policy_test.go |
Tests resource restrictions and excluded configuration writes. |
commands/s3/bucket/policy_json.go |
Extends the action allowlist and bucket-action classification. |
Review effort: Balanced
alanshaw
added a commit
to fil-forge/hilt
that referenced
this pull request
Oct 20, 2026
classifyRequest refuses a query naming two subresources or an unsupported one, and classifies the supported subresources with no operation of their own as the plain bucket or object operation. Those include writes: PUT /b?versioning and ?object-lock ran on a CreateBucket authorization, which skips the bucket scope check, so a key holding s3:CreateBucket could change any bucket's versioning or object lock. Each supported subresource now selects its operation per method, as the gateway's router does (bucketRoutes, objectRoutes): bucket location, ACL, CORS, policy status, tagging, versioning and object lock configuration, and object tagging and attributes, each with its own S3 permission. A route the gateway has that Hilt does not authorize (PUT ?cors, PUT ?acl, object ACLs, uploadId alone on a PUT) is refused, as is a POST naming no route. A subresource the router does not route on for the method is the plain operation there, as in the router, and a HEAD is HeadBucket or HeadObject. The bucket configuration writes and the policy status are service-key only, like the policy and encryption. The new reads and the object tagging writes are policy actions (libforge pinned to fil-forge/libforge#86). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/hilt
that referenced
this pull request
Oct 20, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/ingot
that referenced
this pull request
Oct 20, 2026
Hilt moves to the hilt#102 head that pins the same libforge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/ingot
that referenced
this pull request
Oct 20, 2026
…ries naming a version Hilt now authorizes DeleteObjects on s3:DeleteObject, as S3 does, where it used to classify the request as CreateBucket. S3 also requires s3:DeleteObjectVersion for an entry that names a version, and those entries are in the body, where Hilt cannot see them. The IAM service now carries the key's action sets on the request (reqscope.ActionsKey, the same per-key cache the fast path reads), and DeleteObjects fails such an entry with AccessDenied unless the key holds the permission on the bucket. Bumps hilt to the classification that mirrors the gateway's router (fil-forge/hilt#102) and libforge to the matching policy actions (fil-forge/libforge#86). The itest key carries the new permissions behind INGOT_ITEST_ROUTE_PERMS until the published hilt image knows them, and TestForgeRoutePermissions covers the refusals through the real hilt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/ingot
that referenced
this pull request
Oct 20, 2026
Hilt moves to the hilt#102 head that pins the same libforge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/hilt
that referenced
this pull request
Oct 20, 2026
classifyRequest refuses a query naming two subresources or an unsupported one, and classifies the supported subresources with no operation of their own as the plain bucket or object operation. Those include writes: PUT /b?versioning and ?object-lock ran on a CreateBucket authorization, which skips the bucket scope check, so a key holding s3:CreateBucket could change any bucket's versioning or object lock. Each supported subresource now selects its operation per method, as the gateway's router does (bucketRoutes, objectRoutes): bucket location, ACL, CORS, policy status, tagging, versioning and object lock configuration, and object tagging and attributes, each with its own S3 permission. A route the gateway has that Hilt does not authorize (PUT ?cors, PUT ?acl, object ACLs, uploadId alone on a PUT) is refused, as is a POST naming no route. A subresource the router does not route on for the method is the plain operation there, as in the router, and a HEAD is HeadBucket or HeadObject. The bucket configuration writes and the policy status are service-key only, like the policy and encryption. The new reads and the object tagging writes are policy actions (libforge pinned to fil-forge/libforge#86). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/hilt
that referenced
this pull request
Oct 20, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/hilt
that referenced
this pull request
Oct 20, 2026
…102) Stacked on #101. Pins libforge to its merge of fil-forge/libforge#86. #101 refuses a query naming two subresources (`ConflictingQuery`) or an unsupported one, and classifies DeleteObjects. It classifies the remaining supported subresources as the plain bucket or object operation. Those include writes: `PUT /b?versioning` and `?object-lock` run on a **CreateBucket** authorization, which skips the bucket scope check, so a key holding `s3:CreateBucket` can change any bucket's versioning or object lock. This PR gives each of them its own operation and permission. ### What changes Each supported subresource now selects its operation per method, as the gateway's router does (`bucketRoutes`, `objectRoutes`, alongside #101's `subresources` vocabulary): | Operation | Permission | |---|---| | GetBucketLocation / Acl / Cors | `s3:GetBucketLocation` / `s3:GetBucketAcl` / `s3:GetBucketCORS` | | GetBucketPolicyStatus | `s3:GetBucketPolicyStatus` | | Get / Put / DeleteBucketTagging | `s3:GetBucketTagging` / `s3:PutBucketTagging` (both writes) | | Get / PutBucketVersioning | `s3:GetBucketVersioning` / `s3:PutBucketVersioning` | | Get / PutObjectLockConfiguration | `s3:GetBucketObjectLockConfiguration` / `s3:PutBucketObjectLockConfiguration` | | Get / Put / DeleteObjectTagging | `s3:GetObjectTagging` / `s3:PutObjectTagging` / `s3:DeleteObjectTagging` | | GetObjectAttributes | `s3:GetObjectAttributes` | The bucket configuration writes and the policy status are service-key only, like the policy and encryption. The other new permissions are policy actions (fil-forge/libforge#86, merged). Object tagging writes are catalog writes; the bucket configuration lives in Ingot's registry and needs no Forge commands. Routing follows the router per method: - A route the gateway has that Hilt does not authorize is refused with `UnsupportedOperation`: `PUT`/`DELETE ?cors`, `PUT ?acl`, object ACLs, `?uploads`/`?versions` on an object, and `uploadId` on a PUT without a `partNumber`. - A POST naming no route is refused. On a bucket it is a browser form upload, authenticated by a signed policy in the body that Hilt doesn't verify; #101 classifies it as CreateBucket. - A supported subresource the router doesn't route on for the method is the plain operation there, as in the router (`PUT /b?uploads` creates the bucket). A HEAD is HeadBucket or HeadObject whatever its subresource. - Other query parameters are ignored, as #101 has it. ### Behaviour changes to know about - Existing service keys don't hold the new permissions, so they lose these operations until they're re-issued with them. FilOne's console sets explicit permission lists on the keys it issues and will need the new names. - Principal policies that list actions (instead of `s3:*`) need the new reads added for principals to keep reading bucket configuration, tags and attributes. - `POST /b/k?partNumber&uploadId` is now CompleteMultipartUpload, as the router routes it (same permission as the PutObject it was before). ### Tests `go test ./...` and `go vet -tags itest ./itest` pass. `TestClassifyRequest` covers each new operation, the router's fall-through cases, and the refused routes. `TestRoutesAddressExistingBuckets` checks that every routed operation is scope-checked against its bucket and routes only on a supported subresource. I haven't run the IAM itest scenarios against the change. fil-forge/ingot#287 pins this branch and runs its route-permission scenario against it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
alanshaw
added a commit
to fil-forge/ingot
that referenced
this pull request
Oct 20, 2026
…ries naming a version (#287) Stacked on #286, which maps hilt#101's `ConflictingQuery` refusal to S3's 400. Pins hilt to the merge of fil-forge/hilt#102 (`cd26388`, which includes hilt#101), replacing #286's pin, and libforge to its merge of fil-forge/libforge#86. Hilt used to classify `POST /b?delete` (DeleteObjects) as CreateBucket. CreateBucket skips the bucket scope check, so a key holding `s3:CreateBucket` could batch-delete in any bucket of its tenant, permanently deleting named versions included. Ingot did no permission check per entry. hilt#101 authorizes DeleteObjects on `s3:DeleteObject`, as S3 does. S3 also requires `s3:DeleteObjectVersion` for each entry that names a version. Those entries are in the body, where Hilt can't see them, so Ingot now checks them: the IAM service puts the key's action sets on the request (`reqscope.ActionsKey`, the same per-key cache the local fast path reads), and `DeleteObjects` fails each such entry with AccessDenied, leaving the version in place, unless the key holds the permission on the bucket. Entries without a version are unaffected. A request with no action set refuses version entries. The pin bump also brings in hilt#101's and hilt#102's classification on the fast path (`RequirementsFor`), so bucket configuration, object tagging and attribute requests are checked against their own permissions. ### Tests - `TestVersioning_DeleteObjectsNeedsVersionPermission`: refused without the permission and with no action set; a mixed batch refuses the version entry and deletes the rest. - `TestForgeRoutePermissions` (itest), through the real hilt: - a key holding only `s3:CreateBucket` is refused PutBucketVersioning and DeleteObjects on an existing bucket - a key without `s3:DeleteObjectVersion` gets AccessDenied for the version entry while the other entry is deleted, and a key with it deletes the version Each denial is asserted twice, so the fast path is covered. - The itest key (`hiltAllPermissions`) carries the encryption and route permissions; the published hilt image knows them, so the `INGOT_ITEST_ENCRYPTION_PERMS` and `INGOT_ITEST_ROUTE_PERMS` guards are gone and both permission scenarios run in CI. `make test` passes. Against the published hilt `:main` (`cd26388`), with no override, as CI runs, these pass: `TestForgeVersity` (the S3 conformance suite), `TestForgeScenarios`, `TestForgeRoutePermissions` and `TestForgeBucketEncryptionPermissions`. Merge #286 first; this PR's pin replaces its pin. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A bucket policy can now grant these by name: object tagging (
s3:GetObjectTagging,s3:PutObjectTagging,s3:DeleteObjectTagging),s3:GetObjectAttributes, and reads of the bucket's configuration (s3:GetBucketAcl,s3:GetBucketCORS,s3:GetBucketLocation,s3:GetBucketObjectLockConfiguration,s3:GetBucketTagging,s3:GetBucketVersioning). The configuration reads are bucket actions, so they take the bucket ARN; the rest take the objects ARN.Hilt is changing to refuse any request with a subresource it doesn't classify, and to give each operation the gateway serves its own permission. Before that change, a principal reached these operations through
s3:ListBucket,s3:GetObjectand the plain object writes, so policies need to be able to grant them or principals lose access.The configuration writes (
s3:PutBucketVersioning,s3:PutBucketObjectLockConfiguration,s3:PutBucketTagging) stay out of the allowlist, as the encryption configuration does: each changes the bucket for every principal. Service keys can hold them.Policies using
s3:*pick up the new actions automatically. Policies that list actions need the new ones added.🤖 Generated with Claude Code