mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-20 14:17:07 +00:00
* s3: report the effective ownership when a bucket has none stored GetBucketOwnershipControls read Seaweed-X-Amz-Ownership straight out of the bucket entry, so a bucket that never had one written reported an empty ObjectOwnership. The object write path defaults the same missing attribute to BucketOwnerEnforced, so the API contradicted the behavior it describes. Resolve the stored value through one helper both readers share, and let PutBucketOwnershipControls persist unconditionally so setting the default value still gives DeleteBucketOwnershipControls something to remove. * test: cover the bucket ownership controls round trip Pins the behaviors the ownership default fix depends on: a bucket that never had ownership controls written reports BucketOwnerEnforced, and putting that same value on such a bucket still persists it, so the delete that follows has something to remove. The put-then-delete case gets its own bucket -- run after an ObjectWriter put, it would pass against an implementation that skips only the initial write. The acl workflow already runs this package against a live weed mini, so it needs no wiring.
211 lines
7.1 KiB
Go
211 lines
7.1 KiB
Go
package s3api
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
|
lru "github.com/hashicorp/golang-lru/v2"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
"golang.org/x/sync/singleflight"
|
|
)
|
|
|
|
var loadBucketMetadataFromFiler = func(r *BucketRegistry, bucketName string) (*BucketMetaData, error) {
|
|
entry, err := r.s3a.getBucketEntry(bucketName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buildBucketMetadata(r.s3a.iam, entry), nil
|
|
}
|
|
|
|
type BucketMetaData struct {
|
|
_ struct{} `type:"structure"`
|
|
|
|
Name string
|
|
// Indicates the bucket is a table bucket.
|
|
IsTableBucket bool
|
|
|
|
//By default, when another AWS account uploads an object to S3 bucket,
|
|
//that account (the object writer) owns the object, has access to it, and
|
|
//can grant other users access to it through ACLs. You can use Object Ownership
|
|
//to change this default behavior so that ACLs are disabled and you, as the
|
|
//bucket owner, automatically own every object in your bucket.
|
|
ObjectOwnership string
|
|
|
|
// Container for the bucket owner's display name and ID.
|
|
Owner *s3.Owner `type:"structure"`
|
|
|
|
// A list of grants for access controls.
|
|
Acl []*s3.Grant `locationName:"AccessControlList" locationNameList:"Grant" type:"list"`
|
|
}
|
|
|
|
type BucketRegistry struct {
|
|
metadataCache *lru.Cache[string, *BucketMetaData]
|
|
|
|
notFound *lru.Cache[string, struct{}]
|
|
// loadGroup deduplicates concurrent filer loads of the same bucket
|
|
// without serializing loads of different buckets
|
|
loadGroup singleflight.Group
|
|
s3a *S3ApiServer
|
|
}
|
|
|
|
// NewBucketRegistry creates a lazy registry: nothing is listed at startup,
|
|
// buckets load from the filer on first access and stay fresh via the
|
|
// metadata subscription.
|
|
func NewBucketRegistry(s3a *S3ApiServer) *BucketRegistry {
|
|
metadataCache, _ := lru.New[string, *BucketMetaData](bucketCacheCapacity)
|
|
notFound, _ := lru.New[string, struct{}](bucketCacheCapacity)
|
|
return &BucketRegistry{
|
|
metadataCache: metadataCache,
|
|
notFound: notFound,
|
|
s3a: s3a,
|
|
}
|
|
}
|
|
|
|
// LoadBucketMetadata refreshes a bucket already resident in the cache from a
|
|
// subscription event. Cold buckets are left to lazy-load on first access so
|
|
// the cache holds only this gateway's working set.
|
|
func (r *BucketRegistry) LoadBucketMetadata(entry *filer_pb.Entry) {
|
|
if r.metadataCache.Contains(entry.Name) {
|
|
r.metadataCache.Add(entry.Name, buildBucketMetadata(r.s3a.iam, entry))
|
|
}
|
|
// Remove from notFound cache since bucket now exists
|
|
r.unMarkNotFound(entry.Name)
|
|
}
|
|
|
|
// bucketOwnerAccountId returns the account id owning the bucket entry. A bucket
|
|
// created outside the S3 API (the admin UI, weed shell) records only its owning
|
|
// identity, so that identity is resolved to its account. Without this such a
|
|
// bucket looks unowned: it reports the default admin account as its ACL owner,
|
|
// and under BucketOwnerEnforced every object written to it is stamped with that
|
|
// account instead of the bucket owner.
|
|
func bucketOwnerAccountId(accountManager AccountManager, entry *filer_pb.Entry) string {
|
|
if ownerAccountId := string(entry.Extended[s3_constants.ExtAmzOwnerKey]); ownerAccountId != "" {
|
|
return ownerAccountId
|
|
}
|
|
if identityName := string(entry.Extended[s3_constants.AmzIdentityId]); identityName != "" {
|
|
return accountManager.GetAccountIdByIdentityName(identityName)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func buildBucketMetadata(accountManager AccountManager, entry *filer_pb.Entry) *BucketMetaData {
|
|
entryJson, _ := json.Marshal(entry)
|
|
glog.V(3).Infof("build bucket metadata,entry=%s", entryJson)
|
|
bucketMetadata := &BucketMetaData{
|
|
Name: entry.Name,
|
|
IsTableBucket: s3tables.IsTableBucketEntry(entry),
|
|
|
|
//Default ownership: OwnershipBucketOwnerEnforced, which means Acl is disabled
|
|
ObjectOwnership: s3_constants.DefaultOwnershipForExists,
|
|
|
|
// Default owner: `AccountAdmin`
|
|
Owner: &s3.Owner{
|
|
ID: &AccountAdmin.Id,
|
|
DisplayName: &AccountAdmin.DisplayName,
|
|
},
|
|
}
|
|
if entry.Extended != nil {
|
|
//ownership control
|
|
if ownership, ok := entry.Extended[s3_constants.ExtOwnershipKey]; ok {
|
|
if !s3_constants.ValidateOwnership(string(ownership)) {
|
|
glog.Warningf("Invalid ownership: %s, bucket: %s", string(ownership), bucketMetadata.Name)
|
|
}
|
|
bucketMetadata.ObjectOwnership = s3_constants.EffectiveOwnership(string(ownership))
|
|
}
|
|
|
|
//access control policy
|
|
//owner
|
|
ownerAccountId := bucketOwnerAccountId(accountManager, entry)
|
|
if ownerAccountId != "" {
|
|
ownerAccountName := accountManager.GetAccountNameById(ownerAccountId)
|
|
if ownerAccountName == "" {
|
|
glog.Warningf("owner[id=%s] is invalid, bucket: %s", ownerAccountId, bucketMetadata.Name)
|
|
} else {
|
|
bucketMetadata.Owner = &s3.Owner{
|
|
ID: &ownerAccountId,
|
|
DisplayName: &ownerAccountName,
|
|
}
|
|
}
|
|
}
|
|
//grants
|
|
acpGrantsBytes, ok := entry.Extended[s3_constants.ExtAmzAclKey]
|
|
if ok && len(acpGrantsBytes) > 0 {
|
|
var grants []*s3.Grant
|
|
err := json.Unmarshal(acpGrantsBytes, &grants)
|
|
if err == nil {
|
|
bucketMetadata.Acl = grants
|
|
} else {
|
|
glog.Warningf("Unmarshal ACP grants: %s(%v), bucket: %s", string(acpGrantsBytes), err, bucketMetadata.Name)
|
|
}
|
|
}
|
|
}
|
|
return bucketMetadata
|
|
}
|
|
|
|
func (r *BucketRegistry) RemoveBucketMetadata(entry *filer_pb.Entry) {
|
|
r.removeMetadataCache(entry.Name)
|
|
r.unMarkNotFound(entry.Name)
|
|
}
|
|
|
|
func (r *BucketRegistry) GetBucketMetadata(bucketName string) (*BucketMetaData, s3err.ErrorCode) {
|
|
bucketMetadata, ok := r.metadataCache.Get(bucketName)
|
|
if ok {
|
|
return bucketMetadata, s3err.ErrNone
|
|
}
|
|
|
|
if r.notFound.Contains(bucketName) {
|
|
return nil, s3err.ErrNoSuchBucket
|
|
}
|
|
|
|
return r.LoadBucketMetadataFromFiler(bucketName)
|
|
}
|
|
|
|
// LoadBucketMetadataFromFiler loads the bucket from the filer; concurrent
|
|
// calls for the same bucket share one load, and the cache is filled inside
|
|
// the flight so a bucket is fetched only once.
|
|
func (r *BucketRegistry) LoadBucketMetadataFromFiler(bucketName string) (*BucketMetaData, s3err.ErrorCode) {
|
|
metadata, err, _ := r.loadGroup.Do(bucketName, func() (interface{}, error) {
|
|
//check if already exists
|
|
if bucketMetaData, ok := r.metadataCache.Get(bucketName); ok {
|
|
return bucketMetaData, nil
|
|
}
|
|
|
|
//if not exists, load from filer
|
|
bucketMetadata, err := loadBucketMetadataFromFiler(r, bucketName)
|
|
if err != nil {
|
|
if err == filer_pb.ErrNotFound {
|
|
// The bucket doesn't actually exist and should no longer loaded from the filer
|
|
r.notFound.Add(bucketName, struct{}{})
|
|
}
|
|
return nil, err
|
|
}
|
|
r.setMetadataCache(bucketMetadata)
|
|
r.unMarkNotFound(bucketName)
|
|
return bucketMetadata, nil
|
|
})
|
|
if err != nil {
|
|
if err == filer_pb.ErrNotFound {
|
|
return nil, s3err.ErrNoSuchBucket
|
|
}
|
|
return nil, s3err.ErrInternalError
|
|
}
|
|
return metadata.(*BucketMetaData), s3err.ErrNone
|
|
}
|
|
|
|
func (r *BucketRegistry) setMetadataCache(metadata *BucketMetaData) {
|
|
r.metadataCache.Add(metadata.Name, metadata)
|
|
}
|
|
|
|
func (r *BucketRegistry) removeMetadataCache(bucket string) {
|
|
r.metadataCache.Remove(bucket)
|
|
}
|
|
|
|
func (r *BucketRegistry) unMarkNotFound(bucket string) {
|
|
r.notFound.Remove(bucket)
|
|
}
|