Files
seaweedfs/weed/s3api/s3api_acp.go
T
48782e2b30 s3: allow PutBucketAcl when auth is off (#12180)
* s3: allow PutBucketAcl when auth is off

* s3: resolve the PutBucketAcl owner against the recorded owner

The unauthenticated path now resolves the account as anonymous, but XML
ACLs were still checked against it while GetBucketAcl reported an empty
owner for ownerless buckets, so a read-modify-write ACL update failed
with AccessDenied. GetBucketAcl and the XML check now both use the
recorded owner when the bucket has one and the resolved account
otherwise. The canned-ACL grantee moves to the same owner, and a request
carrying no account no longer erases a stored owner.

* s3: cover PutBucketAcl without auth for XML bodies and stored owners

The XML case posts the anonymous owner GetBucketAcl reports for an
ownerless bucket; the canned case asserts a recorded owner survives an
unauthenticated update and receives the full-control grant. Ambient AWS
credentials are cleared so the memory store never turns auth on.

* s3: keep the recorded owner through a PutBucketAcl update

The grantee and the stored owner were resolved differently: grants
named the recorded owner while config.Owner still took the caller's
account, so an admin updating another account's bucket rewrote the
owner and broke an XML retry naming the original one. An ownerless
bucket adopts the ACL's owner; an owned bucket keeps it.

* s3: fall back to the admin owner for an ownerless bucket in ACL handlers

An unauthenticated PutBucketAcl resolved the owner as anonymous and stored
it on the bucket, but the registry reports an ownerless bucket as
admin-owned: BucketOwnerEnforced uploads then stamped anonymous instead of
admin, and ownership checks after enabling auth compared against the wrong
owner. Both ACL handlers now resolve an anonymous fallback to the admin
account, so GetBucketAcl reports the same effective owner the registry
does and the stored owner matches it.

* s3: resolve the admin owner for bucket grants when the caller is anonymous or untrusted

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: skip recording anonymous identity or account as bucket owner

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: report stored bucket ACL grants verbatim

Explicitly saved anonymous full-control grants must survive a
GET-to-PUT round trip unchanged, so the read path no longer rewrites
them to the reported owner.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: expect the stored anonymous grant verbatim in GetBucketAcl

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: user.email <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 14:45:23 -07:00

58 lines
1.7 KiB
Go

package s3api
import (
"net/http"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
func getAccountId(r *http.Request) string {
id := r.Header.Get(s3_constants.AmzAccountId)
if len(id) == 0 {
return AccountAnonymous.Id
} else {
return id
}
}
// requestAccountId returns the authenticated caller's account; with auth off
// the header is client-supplied, so the admin account stands in instead.
func (s3a *S3ApiServer) requestAccountId(r *http.Request) string {
if s3a.iam != nil && !s3a.iam.isEnabled() {
return AccountAdmin.Id
}
return r.Header.Get(s3_constants.AmzAccountId)
}
// ownerAccountId resolves the account that owns grants and ownerless buckets:
// the requester's account when one is trusted, else the admin account — with
// auth off the account header is client-supplied and cannot claim ownership.
func (s3a *S3ApiServer) ownerAccountId(r *http.Request) string {
accountId := getAccountId(r)
if accountId == AccountAnonymous.Id || (s3a.iam != nil && !s3a.iam.isEnabled()) {
return AccountAdmin.Id
}
return accountId
}
func (s3a *S3ApiServer) checkAccessByOwnership(r *http.Request, bucket string) s3err.ErrorCode {
metadata, errCode := s3a.bucketRegistry.GetBucketMetadata(bucket)
if errCode != s3err.ErrNone {
return errCode
}
// Without auth there is no owner to compare against.
if s3a.iam != nil && !s3a.iam.isEnabled() {
return s3err.ErrNone
}
// Admin by capability, not account id: account-less identities share the "admin" id.
if s3a.isUserAdmin(r) {
return s3err.ErrNone
}
accountId := getAccountId(r)
if metadata.Owner != nil && metadata.Owner.ID != nil && accountId == *metadata.Owner.ID {
return s3err.ErrNone
}
return s3err.ErrAccessDenied
}