Fixes from the review of the admin bucket policy UI (#10907)

* admin: treat a missing S3 Tables policy as an empty load, not an error

The bucket/table policy GET relayed the backend's 404 NoSuchPolicy to the
dialog, whose loader treats any non-OK response as a load failure and
keeps Save and Delete blocked. A bucket or table without a policy could
never be given one. Return policy null instead, the same contract
ShowBucketPolicy uses for classic buckets.

* admin: reject policy documents the structured editor would misread

A top-level JSON array passed the object guard (typeof [] is 'object')
and loaded as a zero-statement policy, which the next commit would
rewrite to an empty document. Object elements in Action/Resource were
coerced to '[object Object]' and saved that way on the s3tables surface,
which stores policies verbatim. Both now throw, which routes the
document to the JSON tab like other unrepresentable shapes.

* admin: let the JSON tab save documents the structured editor can't model

Save with the JSON tab active required a round-trip through
policyDocToEditorState, so exactly the documents the dialogs shunt to
'JSON tab only' mode (unrepresentable Effect, Resource+NotResource, and
the like) could never be saved - Delete was the only mutation left.
Invalid JSON still blocks; an unrepresentable document now saves and the
editor state stays marked unparsed.

* admin: pin the policy editor to what each consumer's backend supports

The s3tables evaluator has no NotResource/NotPrincipal fields - it
silently drops them, turning Allow+NotResource into allow-everything and
making Deny+NotPrincipal inert - and it only matches s3tables: actions
against s3tables ARNs, while the editor suggested s3: actions and
arn:aws:s3::: resources. New registerPolicyEditor knobs: allowNegation
hides the Not* modes and routes documents using them to the JSON tab;
resourceSuggestions pins the Resource autocomplete to the open
resource's ARN; the S3 Tables dialogs get an s3tables-only action
datalist. requirePrincipal now also hides NotPrincipal, which
policy_engine.ValidateBucketPolicy always rejects, and the client-side
check requires Principal specifically to match that server rule.

* admin: save S3 Tables policies from a button, not form submission

The multi-input structured editor sits inside a form whose Save button
was type=submit, so Enter in any single-line editor input - accepting an
autocomplete suggestion, say - implicitly submitted whatever half-built
statement the editor held, and the backend stores the document verbatim.
A lone statement with no Principal matches nobody, locking out every
non-owner. Save is now an ordinary button and the form ignores
submission.

* admin: block zero-statement policy saves

Committing the active tab before the emptiness check made 'Policy JSON
is required' dead code: an empty editor serializes to {"Statement":[]},
which the s3tables backend stores verbatim - evaluated default-deny for
every non-owner, while the statement-count column keeps showing 'Not
configured'. All three policy dialogs now refuse a save with no
statements and point at Delete instead. The classic bucket modal only
gained a clearer message; the server already rejected the document.

* admin: guard S3 Tables policy mutations against stale and overlapping requests

The save/delete completions ran against whatever resource the shared
modal happened to show by then: a slow PUT for one bucket would hide the
modal mid-edit of another and misattribute its alerts, a late DELETE
cleared the shared textarea over the newly opened resource with its
loaded flag set, and nothing stopped a double-click from firing two
overlapping mutations. Ported the classic modal's pattern: capture the
target on start, flag the mutation in flight with the buttons disabled,
and only touch the UI when the completion still matches the open
resource. Success now reloads the page, which also keeps the Policy
column's statement count honest.

* admin: confirm before deleting an S3 Tables policy

Delete Policy sat next to Save and fired on a single click; with
default-allow enabled one stray click silently dropped the resource
policy and left the bucket open to every principal. Same confirmation
the classic bucket modal already has.

* admin: let a corrupt stored bucket policy be shown, fixed, and deleted

A stored document the decoder rejects made the policy GET 500, and with
the loaded flag never set the modal blocked both Save and Delete - the
one policy an operator most needs to remove was the one they couldn't,
even though the delete path never reads the document. The GET now
returns the raw bytes alongside a null policy; the dialog hands them to
the JSON tab and unblocks the buttons.

* admin: url-encode the bucket name in the policy API calls

The filer lists any directory under the buckets path, names S3 would
never allow included; one carrying '#' or '%' broke the fetch URL or
addressed a different name than the modal shows.

* admin: drop stale edit-policy responses on the IAM policies page

The same race the bucket and S3 Tables dialogs already guard against:
open one policy's editor while its GET stalls, open another, and the
late response populates the editor under the second policy's name -
Update then saves the first policy's statements over the second.

* admin: warn before a bucket policy save drops unsupported fields

The editor tracks unmodeled top-level keys precisely so
confirmPolicyFieldDiscard can warn before the server's Version+Statement
decode discards them, but only the IAM page called it; the bucket modal
saved a pasted document with e.g. a console-generated Id without a word
while the editor kept displaying the field.

* s3: enforce the bucket policy size cap on both surfaces

The 20KB cap lived only in the admin UI, so a larger policy stored via
the S3 API displayed there but could never be re-saved, desyncing the
two writers the cap comment claimed could not desync. The constant now
lives in policy_engine next to the shared validator and PutBucketPolicy
rejects oversized documents with PolicyTooLarge, matching AWS.

* admin: ship the policy editor's fieldset styles with the editor

The .policy-stmt-* rules that undo Bootstrap's full-width legend reset
stayed behind in policies.templ when the editor markup moved to the
shared script, so the bucket and S3 Tables dialogs rendered Actions/
Resource/Principal as full-width jumbo headings. PolicyDatalists is the
component every consumer already renders once; the styles live there
now.

* s3: mirror bucket policy changes into the IAM store from the metadata subscription

The advanced-IAM path appends the bucket-policy:<bucket> document to
every STS/session evaluation, but only this gateway's own PutBucketPolicy
maintained that mirror - a policy tightened or created through the admin
UI (or another gateway) never reached it, so revoked access stayed live
indefinitely, and the delete side was an unimplemented TODO in any case.
The metadata subscription now diffs the stored policy on every bucket
entry change and updates or removes the mirror, covering all writers and
deletion with one mechanism; IAMManager gains the missing
RemoveBucketPolicy.

* admin: deduplicate the bucket policy write path

Set and Delete carried line-for-line identical filer closures;
bucketPolicyMutation already treats nil as clear-the-key. The shared
helper sits below Set's validation, since ValidatePolicy cannot take the
nil document Delete passes.

* s3: drop ValidateBucketPolicy's re-checks of ValidatePolicy rules

Both callers run ValidatePolicy first, which already enforces the
version and at-least-one-statement rules; the duplicates were dead code
with drifted error text.

* admin: seed a new statement's Resource from the pinned suggestions

A fresh statement on the S3 Tables dialogs started with no resource row
at all; seed it with the broadest pinned ARN the same way cfg.bucket
already seeds the classic modal.

* admin: refuse to save Not* fields the backend would silently drop

Hiding the NotResource/NotPrincipal modes was not enough where negation
is disallowed: the JSON tab accepts any valid document (that is its
job), and a statement's Advanced-fields box can reintroduce the keys, so
an s3tables save could still store fields the evaluator drops - turning
Allow+NotResource into allow-everything. commitPolicyActiveTab now runs
a final document-level check over what would actually be saved; Delete
stays available for cleanup.

* s3: move the IAM bucket policy mirror on a bucket rename

A same-directory rename delivers one event carrying both entries, and
the byte-equality short-circuit skipped the new name's mirror when the
policy was unchanged - while the replayed delete for the old name
removed its mirror, leaving the renamed bucket unmirrored. The mirror
decision is now a pure function that removes the old name and writes the
new one regardless of byte equality, with the rename cases unit tested.

* s3: backfill the IAM bucket policy mirror on lazy bucket loads

The metadata subscription only mirrors changes, so a policy that
predates the IAM integration never reached the bucket-policy:<bucket>
mirror and its grants did not bind on the IAM path until the policy was
next modified. The gateway is deliberately lazy at startup (nothing
lists all buckets), so the backfill hooks the same place a bucket's
policy first becomes known: the cold bucket-config load. EnsureBucketPolicy
writes only when no mirror is stored, so repeat loads cost one cached
read.

* s3: reconcile the bucket policy backfill against concurrent changes

The backfill's check-then-write could race an event-driven mirror update
or removal and re-store bytes that were already stale, with no later
event to heal it. EnsureBucketPolicy now reports whether it wrote, and a
write is reconciled against a fresh authoritative entry read: a changed
policy is re-mirrored, a removed one is removed. Anything changing after
that read fires its own event, which finds the backfill's write already
present and supersedes it. The backfill also carries the entry's raw
bytes rather than a re-marshaled document, so the reconcile can
byte-compare.

* s3: prime the bucket policy mirror before advanced-IAM authorization

The backfill ran from the lazy bucket-config load, but IAM authorization
evaluates the bucket-policy:<bucket> mirror before any handler runs - a
grant carried only by a not-yet-mirrored policy denied forever, and the
denied request never reached the code that would have loaded the bucket.
authorizeWithIAM now primes the bucket config first (an in-memory cache
hit once warm), and the backfill runs synchronously on the cold load so
the very first authorization already sees the mirror.
This commit is contained in:
Chris Lu
2026-08-24 00:52:01 -07:00
committed by GitHub
parent 68ec8ca655
commit 69cc2869ad
30 changed files with 937 additions and 303 deletions
+12
View File
@@ -76,6 +76,11 @@ type IdentityAccessManagement struct {
// Bucket policy engine for evaluating bucket policies
policyEngine *BucketPolicyEngine
// primeBucketForIAM loads a bucket's config (and with it the advanced-IAM
// bucket-policy mirror backfill) before an IAM authorization that will
// evaluate that mirror. Set by NewS3ApiServer; nil in tests.
primeBucketForIAM func(bucket string)
// Cached policy engine for IAM policy fallback evaluation.
// Keyed by policy name, kept in sync by PutPolicy/DeletePolicy.
iamPolicyEngine *policy_engine.PolicyEngine
@@ -2865,6 +2870,13 @@ func (iam *IdentityAccessManagement) AuthorizeObjectDelete(r *http.Request, iden
func (iam *IdentityAccessManagement) authorizeWithIAM(r *http.Request, identity *Identity, action Action, bucket string, object string) s3err.ErrorCode {
ctx := r.Context()
// The evaluation below consults the bucket-policy:<bucket> mirror, so the
// bucket's lazy load (which backfills that mirror) must happen first -
// nothing earlier on a denied request's path would ever trigger it.
if iam.primeBucketForIAM != nil && bucket != "" {
iam.primeBucketForIAM(bucket)
}
// Get session info from request headers
// First check for JWT-based authentication headers (SeaweedFSSessionTokenHeader)
sessionToken := r.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
+1
View File
@@ -184,6 +184,7 @@ func (s3a *S3ApiServer) onCircuitBreakerConfigChange(dir string, oldEntry *filer
func (s3a *S3ApiServer) onBucketMetadataChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
if dir == s3a.option.BucketsPath {
s3a.maintainBucketOwnerIndex(oldEntry, newEntry)
s3a.mirrorBucketPolicyToIAM(oldEntry, newEntry)
if newEntry != nil {
// Update bucket registry (existing functionality)
s3a.bucketRegistry.LoadBucketMetadata(newEntry)
+10 -12
View File
@@ -5,24 +5,22 @@ import (
"strings"
)
// MaxBucketPolicySize mirrors AWS S3's 20 KB bucket-policy limit, enforced
// by both writers (the S3 gateway's PutBucketPolicy and the admin UI) so
// neither surface can store a document the other refuses to manage.
const MaxBucketPolicySize = 20 * 1024
// ValidateBucketPolicy performs bucket-specific policy validation, on top of
// the generic structural checks in ValidatePolicy. It enforces the rules
// that make a policy document valid as an S3 *bucket* policy specifically:
// every statement must name a Principal, and every Resource/NotResource/Action
// must scope to the given bucket.
// the generic structural checks in ValidatePolicy - callers run that first,
// so the version and non-empty-statement rules are not re-checked here. It
// enforces the rules that make a policy document valid as an S3 *bucket*
// policy specifically: every statement must name a Principal, and every
// Resource/NotResource/Action must scope to the given bucket.
//
// This is shared between the S3 gateway's PutBucketPolicy handler
// (weed/s3api/s3api_bucket_policy_handlers.go) and the admin UI
// (weed/admin/dash) so both enforce identical rules.
func ValidateBucketPolicy(policyDoc *PolicyDocument, bucket string) error {
if policyDoc.Version != PolicyVersion2012_10_17 {
return fmt.Errorf("unsupported policy version: %s (must be %s)", policyDoc.Version, PolicyVersion2012_10_17)
}
if len(policyDoc.Statement) == 0 {
return fmt.Errorf("policy document must contain at least one statement")
}
for i, statement := range policyDoc.Statement {
// Bucket policies must have Principal
if statement.Principal == nil {
@@ -54,15 +54,17 @@ func TestValidateBucketPolicy(t *testing.T) {
})
t.Run("bad version", func(t *testing.T) {
// Version and non-empty-statement rules live in ValidatePolicy,
// which callers run first.
doc := &PolicyDocument{Version: "2008-10-17", Statement: []PolicyStatement{validStatement()}}
if err := ValidateBucketPolicy(doc, bucket); err == nil {
if err := ValidatePolicy(doc); err == nil {
t.Error("expected error for bad version")
}
})
t.Run("zero statements", func(t *testing.T) {
doc := &PolicyDocument{Version: PolicyVersion2012_10_17, Statement: []PolicyStatement{}}
if err := ValidateBucketPolicy(doc, bucket); err == nil {
if err := ValidatePolicy(doc); err == nil {
t.Error("expected error for zero statements")
}
})
+13
View File
@@ -381,6 +381,19 @@ func (s3a *S3ApiServer) getBucketConfig(bucket string) (*BucketConfig, s3err.Err
config := s3a.newBucketConfigFromEntry(bucket, entry)
// A cold load is the first time this gateway learns the bucket's policy
// exists, and the metadata subscription only mirrors changes - a policy
// that predates the IAM integration would otherwise never reach the
// advanced-IAM mirror and its grants would not bind on the IAM path.
// Synchronous: the IAM auth path primes the bucket through here before
// evaluating the mirror, so the backfill has to land first - a one-time
// cost on the load that discovers the policy. Raw entry bytes, not the
// parsed document, so the backfill can byte-compare against a later
// entry read when it reconciles.
if policyJSON := entry.Extended[BUCKET_POLICY_METADATA_KEY]; len(policyJSON) > 0 && config.BucketPolicy != nil && s3a.bucketPolicyIAMManager() != nil {
s3a.ensureBucketPolicyInIAM(bucket, policyJSON)
}
// Cache the result
s3a.bucketConfigCache.Set(bucket, config)
+148 -21
View File
@@ -1,6 +1,7 @@
package s3api
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -9,6 +10,7 @@ import (
"net/http"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
@@ -75,7 +77,7 @@ func (s3a *S3ApiServer) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Re
glog.V(3).Infof("PutBucketPolicyHandler: bucket=%s", bucket)
// Read policy document from request body
body, err := io.ReadAll(r.Body)
body, err := io.ReadAll(io.LimitReader(r.Body, policy_engine.MaxBucketPolicySize+1))
if err != nil {
glog.Errorf("Failed to read bucket policy request body: %v", err)
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidPolicyDocument)
@@ -83,6 +85,11 @@ func (s3a *S3ApiServer) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Re
}
defer r.Body.Close()
if len(body) > policy_engine.MaxBucketPolicySize {
s3err.WriteErrorResponse(w, r, s3err.ErrPolicyTooLarge)
return
}
// Parse and validate policy document
var policyDoc policy_engine.PolicyDocument
if err := json.Unmarshal(body, &policyDoc); err != nil {
@@ -299,37 +306,157 @@ func (s3a *S3ApiServer) deleteBucketPolicy(bucket string) error {
// updateBucketPolicyInIAM updates the IAM system with the new bucket policy
func (s3a *S3ApiServer) updateBucketPolicyInIAM(bucket string, policyDoc *policy_engine.PolicyDocument) error {
// Update IAM integration with new bucket policy
if s3a.iam.iamIntegration != nil {
// Type assert to access the concrete implementation which has access to iamManager
if s3Integration, ok := s3a.iam.iamIntegration.(*S3IAMIntegration); ok {
if s3Integration.iamManager != nil {
glog.V(2).Infof("Updated bucket policy for %s in IAM system", bucket)
policyJSON, err := json.Marshal(policyDoc)
if err != nil {
return fmt.Errorf("failed to marshal policy: %w", err)
}
return s3Integration.iamManager.UpdateBucketPolicy(context.Background(), bucket, policyJSON)
}
}
iamManager := s3a.bucketPolicyIAMManager()
if iamManager == nil {
return nil
}
return nil
policyJSON, err := json.Marshal(policyDoc)
if err != nil {
return fmt.Errorf("failed to marshal policy: %w", err)
}
glog.V(2).Infof("Updated bucket policy for %s in IAM system", bucket)
return iamManager.UpdateBucketPolicy(context.Background(), bucket, policyJSON)
}
// ensureBucketPolicyInIAM backfills the IAM mirror for a policy the
// subscription never saw change (one that predates the IAM integration).
// Called from the lazy bucket-config load; a present mirror is left alone.
func (s3a *S3ApiServer) ensureBucketPolicyInIAM(bucket string, policyJSON []byte) {
iamManager := s3a.bucketPolicyIAMManager()
if iamManager == nil {
return
}
wrote, err := iamManager.EnsureBucketPolicy(context.Background(), bucket, policyJSON)
if err != nil {
glog.Warningf("backfill bucket policy for %s into IAM: %v", bucket, err)
return
}
if !wrote {
return
}
// The check-then-write above can race a concurrent policy change or
// delete: the event-driven mirror may have landed in between, and this
// write would then have re-stored bytes that are already stale - with
// no later event to heal it. Reconcile against a fresh entry read,
// which is authoritative; anything changing after this read fires its
// own event, and the mirror for it finds this write already present.
entry, err := s3a.getBucketEntry(bucket)
if err != nil {
if errors.Is(err, filer_pb.ErrNotFound) {
if err := s3a.removeBucketPolicyFromIAM(bucket); err != nil {
glog.Warningf("remove bucket policy for %s from IAM: %v", bucket, err)
}
}
return
}
current := entry.Extended[BUCKET_POLICY_METADATA_KEY]
if bytes.Equal(current, policyJSON) {
return
}
if len(current) == 0 {
if err := s3a.removeBucketPolicyFromIAM(bucket); err != nil {
glog.Warningf("remove bucket policy for %s from IAM: %v", bucket, err)
}
return
}
var policyDoc policy_engine.PolicyDocument
if err := json.Unmarshal(current, &policyDoc); err != nil {
glog.Warningf("backfill bucket policy for %s into IAM: parse: %v", bucket, err)
return
}
if err := s3a.updateBucketPolicyInIAM(bucket, &policyDoc); err != nil {
glog.Warningf("backfill bucket policy for %s into IAM: %v", bucket, err)
}
}
// removeBucketPolicyFromIAM removes the bucket policy from the IAM system
func (s3a *S3ApiServer) removeBucketPolicyFromIAM(bucket string) error {
// This would remove the bucket policy from our advanced IAM system
iamManager := s3a.bucketPolicyIAMManager()
if iamManager == nil {
return nil
}
glog.V(2).Infof("Removed bucket policy for %s from IAM system", bucket)
return iamManager.RemoveBucketPolicy(context.Background(), bucket)
}
// TODO: Integrate with IAM manager to remove resource-based policies
// s3a.iam.iamIntegration.iamManager.RemoveBucketPolicy(bucket)
// bucketPolicyIAMManager returns the advanced-IAM manager the
// "bucket-policy:<bucket>" mirror lives in, or nil when the integration is
// not enabled.
func (s3a *S3ApiServer) bucketPolicyIAMManager() *integration.IAMManager {
if s3a.iam == nil || s3a.iam.iamIntegration == nil {
return nil
}
if s3Integration, ok := s3a.iam.iamIntegration.(*S3IAMIntegration); ok {
return s3Integration.iamManager
}
return nil
}
// mirrorBucketPolicyToIAM keeps the "bucket-policy:<bucket>" IAM mirror in
// sync with the policy stored on a bucket's filer entry. Driven from the
// metadata subscription so it covers every writer - this gateway's own
// PutBucketPolicy, another gateway's, the admin UI, bucket deletion, and
// rename - where the handlers' direct calls only ever covered the first.
func (s3a *S3ApiServer) mirrorBucketPolicyToIAM(oldEntry, newEntry *filer_pb.Entry) {
if s3a.bucketPolicyIAMManager() == nil {
return
}
removeName, updateName, updatePolicy := bucketPolicyMirrorOps(oldEntry, newEntry)
if removeName != "" {
if err := s3a.removeBucketPolicyFromIAM(removeName); err != nil {
glog.Warningf("remove bucket policy for %s from IAM: %v", removeName, err)
}
}
if updateName == "" {
return
}
var policyDoc policy_engine.PolicyDocument
if err := json.Unmarshal(updatePolicy, &policyDoc); err != nil {
glog.Warningf("mirror bucket policy for %s to IAM: parse: %v", updateName, err)
return
}
if err := s3a.updateBucketPolicyInIAM(updateName, &policyDoc); err != nil {
glog.Warningf("mirror bucket policy for %s to IAM: %v", updateName, err)
}
}
// bucketPolicyMirrorOps computes what a bucket entry change means for the
// IAM mirror: a name whose mirror must be removed, and a (name, policy) to
// write. A rename delivers both entries under different names in one event,
// and the old name's mirror has to move even when the policy bytes are
// unchanged - equality only short-circuits same-name updates.
func bucketPolicyMirrorOps(oldEntry, newEntry *filer_pb.Entry) (removeName, updateName string, updatePolicy []byte) {
var oldName, newName string
var oldPolicy, newPolicy []byte
if oldEntry != nil {
oldName = oldEntry.Name
oldPolicy = oldEntry.Extended[BUCKET_POLICY_METADATA_KEY]
}
if newEntry != nil {
newName = newEntry.Name
newPolicy = newEntry.Extended[BUCKET_POLICY_METADATA_KEY]
}
if oldName != "" && oldName != newName && len(oldPolicy) > 0 {
removeName = oldName
oldPolicy = nil
}
if newName == "" || bytes.Equal(oldPolicy, newPolicy) {
return
}
if len(newPolicy) == 0 {
removeName = newName
return
}
updateName = newName
updatePolicy = newPolicy
return
}
// GetPublicAccessBlockHandler Retrieves the PublicAccessBlock configuration for an S3 bucket
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetPublicAccessBlock.html
func (s3a *S3ApiServer) GetPublicAccessBlockHandler(w http.ResponseWriter, r *http.Request) {
@@ -0,0 +1,114 @@
package s3api
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)
func bucketEntryWithPolicy(name, policy string) *filer_pb.Entry {
entry := &filer_pb.Entry{Name: name}
if policy != "" {
entry.Extended = map[string][]byte{BUCKET_POLICY_METADATA_KEY: []byte(policy)}
}
return entry
}
func TestBucketPolicyMirrorOps(t *testing.T) {
policyA := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow"}]}`
policyB := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny"}]}`
tests := []struct {
name string
oldEntry *filer_pb.Entry
newEntry *filer_pb.Entry
removeName string
updateName string
updatePolicy string
}{
{
name: "create without policy",
newEntry: bucketEntryWithPolicy("a", ""),
},
{
name: "policy set",
oldEntry: bucketEntryWithPolicy("a", ""),
newEntry: bucketEntryWithPolicy("a", policyA),
updateName: "a",
updatePolicy: policyA,
},
{
name: "policy replaced",
oldEntry: bucketEntryWithPolicy("a", policyA),
newEntry: bucketEntryWithPolicy("a", policyB),
updateName: "a",
updatePolicy: policyB,
},
{
name: "policy unchanged",
oldEntry: bucketEntryWithPolicy("a", policyA),
newEntry: bucketEntryWithPolicy("a", policyA),
},
{
name: "policy removed",
oldEntry: bucketEntryWithPolicy("a", policyA),
newEntry: bucketEntryWithPolicy("a", ""),
removeName: "a",
},
{
name: "bucket deleted",
oldEntry: bucketEntryWithPolicy("a", policyA),
removeName: "a",
},
{
name: "bucket deleted without policy",
oldEntry: bucketEntryWithPolicy("a", ""),
},
{
// The rename case: same policy bytes on both sides must still
// move the mirror to the new name.
name: "renamed with unchanged policy",
oldEntry: bucketEntryWithPolicy("a", policyA),
newEntry: bucketEntryWithPolicy("b", policyA),
removeName: "a",
updateName: "b",
updatePolicy: policyA,
},
{
name: "renamed with changed policy",
oldEntry: bucketEntryWithPolicy("a", policyA),
newEntry: bucketEntryWithPolicy("b", policyB),
removeName: "a",
updateName: "b",
updatePolicy: policyB,
},
{
name: "renamed and policy dropped",
oldEntry: bucketEntryWithPolicy("a", policyA),
newEntry: bucketEntryWithPolicy("b", ""),
removeName: "a",
},
{
name: "renamed without prior policy",
oldEntry: bucketEntryWithPolicy("a", ""),
newEntry: bucketEntryWithPolicy("b", policyA),
updateName: "b",
updatePolicy: policyA,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
removeName, updateName, updatePolicy := bucketPolicyMirrorOps(tc.oldEntry, tc.newEntry)
if removeName != tc.removeName {
t.Errorf("removeName = %q, want %q", removeName, tc.removeName)
}
if updateName != tc.updateName {
t.Errorf("updateName = %q, want %q", updateName, tc.updateName)
}
if string(updatePolicy) != tc.updatePolicy {
t.Errorf("updatePolicy = %q, want %q", updatePolicy, tc.updatePolicy)
}
})
}
}
+9
View File
@@ -358,6 +358,15 @@ func NewS3ApiServerWithStore(router *mux.Router, option *S3ApiServerOption, expl
// policy conditions on s3:x-amz-server-side-encryption evaluate correctly.
policyEngine.MultipartSSELookup = s3ApiServer.getMultipartSSEAlgorithm
// Advanced-IAM authorization evaluates the bucket-policy:<bucket> mirror
// before any handler runs, so the auth path has to be what triggers the
// lazy bucket load (and with it the mirror backfill): a grant carried
// only by a not-yet-mirrored policy would otherwise deny forever, and
// the denied request never reaches the handlers that load the bucket.
iam.primeBucketForIAM = func(bucket string) {
s3ApiServer.getBucketConfig(bucket)
}
// Initialize advanced IAM system if config is provided or explicitly enabled
if option.IamConfig != "" || option.EnableIam {
configSource := "defaults"
+6
View File
@@ -95,6 +95,7 @@ const (
ErrMalformedCredentialDate
ErrMalformedPolicy
ErrInvalidPolicyDocument
ErrPolicyTooLarge
ErrMissingSignHeadersTag
ErrMissingSignTag
ErrUnsignedHeaders
@@ -389,6 +390,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Policy has invalid resource.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrPolicyTooLarge: {
Code: "PolicyTooLarge",
Description: "Policy exceeds the maximum allowed document size.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidPolicyDocument: {
Code: "InvalidPolicyDocument",
Description: "The content of the policy document is invalid.",