Skip to content

feat(rpc): read and write bucket policies over /s3/bucket/policy - #89

Open
pyropy wants to merge 9 commits into
srdjan/feat/iam-tenant-removal-revokesfrom
srdjan/feat/iam-policy-command
Open

pyropy wants to merge 9 commits into
srdjan/feat/iam-tenant-removal-revokesfrom
srdjan/feat/iam-policy-command

Conversation

@pyropy

@pyropy pyropy commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Serves S3 GetBucketPolicy, PutBucketPolicy and DeleteBucketPolicy as one Hilt command, /s3/bucket/policy, reached by service keys only. The PUT body must match the signed payload hash, and If-Match / If-None-Match are optional signed preconditions.

  • ?policy classified into three operations with their own permissions
  • pkg/rpc/service/bucket/policy.go: operation, body hash check, preconditions
  • Handler, wiring and failure mapping; client.Policy for Ingot
  • sigv4.PayloadHash and a test presign option
  • The operation acts on the authorized bucket's DID; an empty precondition is invalid
  • Deployments must delegate /s3/bucket/policy from Hilt to Ingot (feat(stack): delegate /s3/bucket/policy from hilt to ingot smelt#65 does it for local stacks)
  • A principal's policy list carries no per-item ETag; a caller reads it from GetBucketPolicy's ETag header
  • A bucket deleted between authorization and a policy write answers UnknownBucket
  • Temporary pin: libforge to feat(s3): add the /s3/bucket/policy command libforge#80; re-pin before merge

🤖 Generated with Claude Code

@pyropy

pyropy commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T09:39:29.037230Z 6a2d7a7 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f08be2a440

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/rpc/service/bucket/policy.go
Comment thread pkg/rpc/service/bucket/policy.go Outdated
Comment thread pkg/rpc/service/bucket/policy.go
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from 37026f0 to 7d3b768 Compare September 29, 2026 15:22
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from 7d3b768 to 99ed840 Compare September 30, 2026 15:15
pyropy added a commit to fil-forge/ingot that referenced this pull request Sep 30, 2026
…t/policy

GET, PUT and DELETE on /{bucket}?policy are answered ahead of the S3
route table: the signed request and, on a PUT, the body are forwarded to
Hilt as /s3/bucket/policy, which authenticates and authorizes them. The
routes sit ahead of versitygw's controllers because its PutBucketPolicy
validates the body as an AWS-shaped document, which a Forge policy is
not, and its backend seam carries neither preconditions nor an ETag. The
bucket authority maps Hilt's policy rejections; the routes render them
as NoSuchBucketPolicy, MalformedPolicy, PreconditionFailed,
InvalidRequest and OperationAborted, and a CreateBucket's refused
x-bucket-policy header now renders as MalformedPolicy too, the RFC's one
mapping. The in-memory store answers NoSuchBucketPolicy and NotImplemented.

Temporary pins to hilt branch srdjan/feat/iam-policy-command
(fil-forge/hilt#89) and libforge branch
srdjan/feat/s3-bucket-policy-command (fil-forge/libforge#80); re-pin
before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from 99ed840 to 93b2c13 Compare September 30, 2026 15:35
@pyropy

pyropy commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 93b2c1320d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from 93b2c13 to a30b36c Compare September 30, 2026 16:13
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from a30b36c to 275bf5a Compare September 30, 2026 16:14
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch 2 times, most recently from 103f0f7 to dc4d3b3 Compare September 30, 2026 16:55
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from dc4d3b3 to d7d19a1 Compare September 30, 2026 16:58
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from d7d19a1 to 7f48010 Compare September 30, 2026 17:03
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 1, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 1, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 1, 2026
…t/policy

GET, PUT and DELETE on /{bucket}?policy are answered ahead of the S3
route table: the signed request and, on a PUT, the body are forwarded to
Hilt as /s3/bucket/policy, which authenticates and authorizes them. The
routes sit ahead of versitygw's controllers because its PutBucketPolicy
validates the body as an AWS-shaped document, which a Forge policy is
not, and its backend seam carries neither preconditions nor an ETag. The
bucket authority maps Hilt's policy rejections; the routes render them
as NoSuchBucketPolicy, MalformedPolicy, PreconditionFailed,
InvalidRequest and OperationAborted, and a CreateBucket's refused
x-bucket-policy header now renders as MalformedPolicy too, the RFC's one
mapping. The in-memory store answers NoSuchBucketPolicy and NotImplemented.

Temporary pins to hilt branch srdjan/feat/iam-policy-command
(fil-forge/hilt#89) and libforge branch
srdjan/feat/s3-bucket-policy-command (fil-forge/libforge#80); re-pin
before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 1, 2026
Picks up the principal removal that strips its policies before locking the
principal, and the policy store lock-order fixes. Temporary pin to a draft
branch; re-pin to hilt main before this leaves draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 1, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 1, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 1, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-one/fil-one that referenced this pull request Oct 1, 2026
The storage system serves bucket policies as the S3 operations
GetBucketPolicy, PutBucketPolicy and DeleteBucketPolicy (fil-one/RFC#30,
fil-forge/hilt#89), so the iam arm's policy methods sign those with the
tenant's console key instead of calling management-API routes. The
preconditions ride as signed If-Match / If-None-Match headers and the
ETag comes back in a header, both through command middleware. The S3
error codes map onto the policy errors the routes and the fanout already
handle; the console key gains the three policy permissions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pyropy

pyropy commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a2d7a76f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/rpc/service/bucket/policy.go
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 2, 2026
…t/policy

GET, PUT and DELETE on /{bucket}?policy are answered ahead of the S3
route table: the signed request and, on a PUT, the body are forwarded to
Hilt as /s3/bucket/policy, which authenticates and authorizes them. The
routes sit ahead of versitygw's controllers because its PutBucketPolicy
validates the body as an AWS-shaped document, which a Forge policy is
not, and its backend seam carries neither preconditions nor an ETag. The
bucket authority maps Hilt's policy rejections; the routes render them
as NoSuchBucketPolicy, MalformedPolicy, PreconditionFailed,
InvalidRequest and OperationAborted, and a CreateBucket's refused
x-bucket-policy header now renders as MalformedPolicy too, the RFC's one
mapping. The in-memory store answers NoSuchBucketPolicy and NotImplemented.

Temporary pins to hilt branch srdjan/feat/iam-policy-command
(fil-forge/hilt#89) and libforge branch
srdjan/feat/s3-bucket-policy-command (fil-forge/libforge#80); re-pin
before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 2, 2026
Picks up the principal removal that strips its policies before locking the
principal, and the policy store lock-order fixes. Temporary pin to a draft
branch; re-pin to hilt main before this leaves draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Policy PUT can race bucket deletion and leave grants unrevoked, while the dependency remains temporarily pinned to an open PR.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Adds /s3/bucket/policy RPC support for forwarding S3 bucket-policy operations through Hilt.

Changes:

  • Implements authenticated GET, PUT, and DELETE policy handling.
  • Adds SigV4 payload hashing and signed preconditions.
  • Wires the RPC route, client API, failure mappings, and tests.
File Description
AGENTS.md Documents the policy RPC.
go.mod Updates libforge dependency.
go.sum Updates dependency checksums.
pkg/​api/​service/​bucketpolicy/​service.go Exposes resolved-bucket policy removal.
pkg/​client/​client.go Adds the policy RPC client method.
pkg/​fx/​rpc.go Registers the new route.
pkg/​fx/​rpc_test.go Verifies route grouping.
pkg/​rpc/​failure.go Maps policy failures.
pkg/​rpc/​failure_test.go Tests failure mapping.
pkg/​rpc/​policy.go Adds the RPC handler.
pkg/​rpc/​rpc_test.go Verifies command registration.
pkg/​rpc/​service/​auth/​auth.go Restricts policy operations to service keys.
pkg/​rpc/​service/​auth/​operation.go Classifies policy requests and permissions.
pkg/​rpc/​service/​auth/​operation_test.go Tests policy classification.
pkg/​rpc/​service/​bucket/​policy.go Implements policy operations.
pkg/​rpc/​service/​bucket/​policy_test.go Tests policy behavior.
pkg/​sigv4/​sign.go Adds payload-hash presigning.
pkg/​sigv4/​sigv4.go Exposes verified payload hashes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/rpc/service/bucket/policy.go
Comment thread go.mod Outdated
github.com/docker/docker v28.5.2+incompatible
github.com/exaring/otelpgx v0.12.0
github.com/fil-forge/libforge v0.0.0-20260914100934-63f5b20252fe
github.com/fil-forge/libforge v0.0.0-20260928131613-0060aa6cb75a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Known and tracked: the pin is temporary while libforge#80 is open, and the re-pin to the merged revision follows its merge. The merge order is libforge #80, swarf #22, then this stack.

Comment thread pkg/rpc/service/auth/operation.go Outdated
Comment thread pkg/client/client.go
pyropy and others added 7 commits October 5, 2026 17:00
Ingot forwards S3 GetBucketPolicy, PutBucketPolicy and DeleteBucketPolicy
requests as one command; the request's method selects the operation. A
service key holding s3:GetBucketPolicy, s3:PutBucketPolicy or
s3:DeleteBucketPolicy reaches it, a principal-bound key is refused. A
PUT's body must hash to the request's signed payload hash, so nothing on
the path can replace the document. If-Match and If-None-Match: * are
optional signed headers; without one the write is unconditional, as on
AWS. The policy service's rejections (PolicyNotFound, InvalidPrecondition,
PreconditionFailed, ConcurrentChange) become receipt failures for Ingot to
render.

Temporary pin to libforge branch srdjan/feat/s3-bucket-policy-command
(fil-forge/libforge#80), re-pin before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uthorized bucket

A write-locked tenant was refused nothing: the authorizer let it through
for reads and left writes to handlers that never checked. It now refuses
uploads, bucket creation and PutBucketPolicy for such a tenant, keeping
reads, listings and deletes, as the management API's TenantStatus
contract says. The refusal is OperationNotPermitted, which Ingot already
renders as AccessDenied.

The bucket policy operation acts on the bucket the authorizer resolved,
by DID, instead of looking it up again by name, and a present but empty
If-Match or If-None-Match is an invalid precondition rather than an
unconditional write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#52 enforces write-locked in the authorizer and revokes the cached write
grants, so this stack no longer carries its own guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n, and test the policy client method

GET and HEAD shared the policy branch, so HEAD /bucket?policy was classified
as GetBucketPolicy and the policy handler answered it with the document; the
operation is GET-only and a HEAD now classifies as the plain bucket operation.
The client's Policy method gains the test its neighbours have: the forwarded
request and body reach the handler and the ETag and policy come back, and a
failure receipt is an error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e header

CreateBucket no longer carries a policy (RFC 30, 113d4ded), so the failure
mapping's comment names the one source left, a PutBucketPolicy body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
0060aa6 predates libforge #80's rebase onto main and lacks
SampleItem.UploadCount, which hilt main uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pyropy

pyropy commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

/forge-perf

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

forge-perf

Commit ee884580feca95731c91ae2ad6eac86a66993452
Image ghcr.io/fil-forge/hilt:pr-89-ee88458 (sha256:15100044cd80)
Pairs 1 (main, branch)
State Done: within noise
Pairing exp-hilt-pr89-ee884580feca-37331920218
Logs workflow run
Run Role Class Median p5 Traces
main-20261005t152922z main valid (few_windows, nic_allowance_exceeded, traced) 3.67 GB/s 3.50 GB/s Grafana
main-20261005t154638z branch valid (few_windows, nic_allowance_exceeded, traced) 3.76 GB/s 3.55 GB/s Grafana
Median p5
Branch against main +2.6% +1.2%
Noise band ±7.9% ±4.7%

Verdict: within noise, judged on the median against its noise band. Positive deltas mean the branch ingests faster.

Main runs the current main set; branch swaps in this commit's image. The runs table on the forge-perf page lists each run.

Bucket-policy ETags travel in the HTTP ETag header; a list cannot carry
one per item, so PrincipalPolicy drops the etag field. Callers that need
a policy's ETag read it with S3 GetBucketPolicy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
// Delete removes the bucket's policy. ifMatch must equal the current ETag.
// Every principal the policy reached loses its access, so each one's keys lose
// their delegations over the bucket inside the store's transaction.
func (s *Service) Delete(ctx context.Context, externalID, bucketName, ifMatch string) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete doesn't seem to be called by anything? Can we just have a Delete or a Remove method please?

Comment thread pkg/client/client.go
// Policy invokes /s3/bucket/policy with a forwarded GetBucketPolicy,
// PutBucketPolicy or DeleteBucketPolicy request; body is the policy document
// on a PUT. It returns no delegations.
func (c *Client) Policy(ctx context.Context, req s3.Request, body []byte, opts ...MethodOption) (*s3bkt.PolicyOK, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this BucketPolicy?

Suggested change
func (c *Client) Policy(ctx context.Context, req s3.Request, body []byte, opts ...MethodOption) (*s3bkt.PolicyOK, error) {
func (c *Client) BucketPolicy(ctx context.Context, req s3.Request, body []byte, opts ...MethodOption) (*s3bkt.PolicyOK, error) {

}
var opts []bucketpolicysvc.WriteOption
if unconditional {
opts = append(opts, bucketpolicysvc.Unconditional())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, typically I'd expect a functional option, something like bucketpolicysvc.WithPrecondition(ifMatch) that takes the ifMatch parameter (since it's optional) and it wouldn't be passed to Write.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants