Add fine-grained bucket access: paths, actions, conditions, raw
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:
@@ -45,6 +45,72 @@ type BucketAccessSpec struct {
|
||||
// dedicated user it creates (UserRef empty). Defaults to "<name>-rgw".
|
||||
// +optional
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
|
||||
// Paths optionally scopes object-level access to these key prefixes within
|
||||
// the bucket; each becomes the resource "<bucket>/<prefix>*". Empty grants
|
||||
// the whole bucket. The bucket-level ListBucket action always applies to the
|
||||
// whole bucket. Ignored when RawStatements is set.
|
||||
// +optional
|
||||
Paths []string `json:"paths,omitempty"`
|
||||
|
||||
// Actions optionally overrides the S3 actions granted by Level. When set,
|
||||
// exactly these actions are granted, on the bucket and its (optionally
|
||||
// prefixed) objects. Ignored when RawStatements is set.
|
||||
// +optional
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
|
||||
// Conditions optionally restricts when the grant applies (e.g. source IPs,
|
||||
// TLS required). Ignored when RawStatements is set.
|
||||
// +optional
|
||||
Conditions *AccessConditions `json:"conditions,omitempty"`
|
||||
|
||||
// RawStatements is an escape hatch for arbitrary S3 policy statements, merged
|
||||
// into the bucket policy for this grant's principal. When set, Level,
|
||||
// Actions, Paths and Conditions on this object are ignored; the operator only
|
||||
// fills in the Principal (this grant's user) when a statement omits one.
|
||||
// +optional
|
||||
RawStatements []PolicyStatement `json:"rawStatements,omitempty"`
|
||||
}
|
||||
|
||||
// AccessConditions restricts when a grant applies. Each field maps to an S3
|
||||
// policy condition and, when several are set, all must hold (they are AND'd).
|
||||
type AccessConditions struct {
|
||||
// SourceIPs restricts the grant to requests from these CIDRs (or single
|
||||
// addresses), via the S3 aws:SourceIp condition.
|
||||
// +optional
|
||||
SourceIPs []string `json:"sourceIPs,omitempty"`
|
||||
|
||||
// SecureTransportOnly requires the request to use TLS, via the S3
|
||||
// aws:SecureTransport condition.
|
||||
// +optional
|
||||
SecureTransportOnly bool `json:"secureTransportOnly,omitempty"`
|
||||
}
|
||||
|
||||
// PolicyStatement is a raw S3 bucket-policy statement, exposed for grants that
|
||||
// need control beyond Level/Actions/Paths/Conditions.
|
||||
type PolicyStatement struct {
|
||||
// Sid is an optional statement id. The operator derives one when empty.
|
||||
// +optional
|
||||
Sid string `json:"sid,omitempty"`
|
||||
|
||||
// Effect is Allow or Deny. Defaults to Allow.
|
||||
// +kubebuilder:validation:Enum=Allow;Deny
|
||||
// +kubebuilder:default=Allow
|
||||
// +optional
|
||||
Effect string `json:"effect,omitempty"`
|
||||
|
||||
// Actions are the S3 actions the statement covers (e.g. s3:GetObject).
|
||||
Actions []string `json:"actions"`
|
||||
|
||||
// Resources are S3 resource ARNs, or bucket-relative key prefixes when they
|
||||
// do not start with "arn:". Empty means the whole bucket and its objects.
|
||||
// +optional
|
||||
Resources []string `json:"resources,omitempty"`
|
||||
|
||||
// Conditions is the raw S3 condition block: operator -> condition key ->
|
||||
// values, e.g. {"IpAddress": {"aws:SourceIp": ["10.0.0.0/8"]}}.
|
||||
// +optional
|
||||
Conditions map[string]map[string][]string `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// BucketAccessStatus reports observed grant state.
|
||||
|
||||
@@ -9,6 +9,26 @@ import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AccessConditions) DeepCopyInto(out *AccessConditions) {
|
||||
*out = *in
|
||||
if in.SourceIPs != nil {
|
||||
in, out := &in.SourceIPs, &out.SourceIPs
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConditions.
|
||||
func (in *AccessConditions) DeepCopy() *AccessConditions {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AccessConditions)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Bucket) DeepCopyInto(out *Bucket) {
|
||||
*out = *in
|
||||
@@ -41,7 +61,7 @@ func (in *BucketAccess) DeepCopyInto(out *BucketAccess) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
@@ -98,6 +118,28 @@ func (in *BucketAccessList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BucketAccessSpec) DeepCopyInto(out *BucketAccessSpec) {
|
||||
*out = *in
|
||||
if in.Paths != nil {
|
||||
in, out := &in.Paths, &out.Paths
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Actions != nil {
|
||||
in, out := &in.Actions, &out.Actions
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = new(AccessConditions)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.RawStatements != nil {
|
||||
in, out := &in.RawStatements, &out.RawStatements
|
||||
*out = make([]PolicyStatement, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BucketAccessSpec.
|
||||
@@ -349,6 +391,58 @@ func (in *ObjectStoreUserStatus) DeepCopy() *ObjectStoreUserStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PolicyStatement) DeepCopyInto(out *PolicyStatement) {
|
||||
*out = *in
|
||||
if in.Actions != nil {
|
||||
in, out := &in.Actions, &out.Actions
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Resources != nil {
|
||||
in, out := &in.Resources, &out.Resources
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make(map[string]map[string][]string, len(*in))
|
||||
for key, val := range *in {
|
||||
var outVal map[string][]string
|
||||
if val == nil {
|
||||
(*out)[key] = nil
|
||||
} else {
|
||||
inVal := (*in)[key]
|
||||
in, out := &inVal, &outVal
|
||||
*out = make(map[string][]string, len(*in))
|
||||
for key, val := range *in {
|
||||
var outVal []string
|
||||
if val == nil {
|
||||
(*out)[key] = nil
|
||||
} else {
|
||||
inVal := (*in)[key]
|
||||
in, out := &inVal, &outVal
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
(*out)[key] = outVal
|
||||
}
|
||||
}
|
||||
(*out)[key] = outVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyStatement.
|
||||
func (in *PolicyStatement) DeepCopy() *PolicyStatement {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PolicyStatement)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Quota) DeepCopyInto(out *Quota) {
|
||||
*out = *in
|
||||
|
||||
Reference in New Issue
Block a user