Add fine-grained bucket access: paths, actions, conditions, raw
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

The BucketAccess model only offered three coarse levels (read-only/read-write/
full) applied to the whole bucket. Real grants often need to be scoped to a key
prefix, limited to a source network or TLS, restricted to specific actions, or
expressed as an arbitrary S3 statement. RGW (Reef 18.2+/Squid) honours the S3
bucket-policy features to do all of this; expose them on BucketAccess while
keeping the level as the ergonomic default.

- add BucketAccess spec fields: paths (key-prefix scoping), actions (action
  override), conditions (sourceIPs + secureTransportOnly), rawStatements
  (arbitrary S3 statements with the principal injected)
- extend ceph.Grant + BuildBucketPolicy to render prefixed object resources,
  custom-action statements, S3 condition blocks, and raw statements, keeping
  output deterministic (sorted, stable sids)
- translate the new spec fields into grants in the Bucket controller and
  fingerprint grants so distinct fine-grained BucketAccess objects no longer
  collapse on UID+level alone
- regenerate deepcopy + CRDs; add config/samples/04-access-fine-grained.yaml
- cover paths, action override, conditions, raw statements and determinism in
  policy_test.go; document the fields in the README

Claude-Session: https://claude.ai/code/session_016CEncETbf8cvy1PhsHfFHM
This commit is contained in:
2026-07-24 22:48:20 +10:00
parent 466514063a
commit 4b0430f0df
10 changed files with 788 additions and 41 deletions
+38 -2
View File
@@ -2,6 +2,7 @@ package controller
import (
"context"
"encoding/json"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -176,17 +177,52 @@ func (r *BucketReconciler) collectGrants(ctx context.Context, namespace, bucketR
if ba.Status.UID == "" {
continue
}
key := ba.Status.UID + "|" + string(ba.Spec.Level)
g := grantFromAccess(ba.Status.UID, ba)
key := grantKey(g)
if _, dup := seen[key]; dup {
continue
}
seen[key] = struct{}{}
principals[ba.Status.UID] = struct{}{}
grants = append(grants, ceph.Grant{UID: ba.Status.UID, Level: string(ba.Spec.Level)})
grants = append(grants, g)
}
return grants, len(principals), nil
}
// grantFromAccess translates a BucketAccess spec into the ceph grant model,
// carrying the fine-grained scoping (paths, actions, conditions, raw statements).
func grantFromAccess(uid string, ba *v1alpha1.BucketAccess) ceph.Grant {
g := ceph.Grant{
UID: uid,
Level: string(ba.Spec.Level),
Paths: ba.Spec.Paths,
Actions: ba.Spec.Actions,
}
if c := ba.Spec.Conditions; c != nil {
g.Conditions = &ceph.GrantConditions{
SourceIPs: c.SourceIPs,
SecureTransportOnly: c.SecureTransportOnly,
}
}
for _, s := range ba.Spec.RawStatements {
g.Raw = append(g.Raw, ceph.RawStatement{
Sid: s.Sid,
Effect: s.Effect,
Actions: s.Actions,
Resources: s.Resources,
Condition: s.Conditions,
})
}
return g
}
// grantKey is a stable fingerprint of a grant used to collapse duplicate
// BucketAccess objects that would render identical policy statements.
func grantKey(g ceph.Grant) string {
b, _ := json.Marshal(g)
return string(b)
}
func (r *BucketReconciler) pending(ctx context.Context, b *v1alpha1.Bucket, reason, msg string) (ctrl.Result, error) {
b.Status.Phase = "Pending"
b.Status.ObservedGeneration = b.Generation