mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-27 15:30:45 +01:00
* filer: format ChunkView without its cipher key readChunkSliceAt logs a failed chunk fetch with %+v, which printed the per-chunk AES-256-GCM key into error-level logs on every failed read. Give ChunkView a String() that omits CipherKey so %v and %+v stay safe. * pb: scrub cipher keys when an entry must be logged whole ScrubCipherKeys clones an Entry with chunk CipherKey fields cleared, so a warning that needs the full entry still cannot leak the keys. * log entry identifiers instead of whole metadata messages Several log lines formatted whole protobuf messages — SubscribeMetadataResponse, EventNotification, Entry-carrying requests — whose chunks include cipher_key under -encryptVolumeData, so the keys escaped into logs (some at default verbosity). Log the directory, entry name and event timestamp instead; where a chunk list was printed, print file ids via FileIdsString. * log queue: unwrap SubscribeMetadataResponse and never print raw protos The fallback printed any message with %+v, so a SubscribeMetadataResponse carrying entries would still put cipher keys in the log. Unwrap its event notifications (batched events included) and reduce unknown messages to their type name. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * remote sync logs: name the event's real path Rename targets logged under resp.Directory while the entry lives under NewParentPath; route them through MetadataEventTargetDirectory. Sync failure logs printed only the directory, so same-dir failures shared an identifier — log the source and destination paths instead. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * log queue: log the entry's destination path on moves A cross-directory rename printed "file" -> "file" under the source path, hiding where the entry landed. Use NewParentPath so the log shows the destination directory next to the new name. * admin: install the go.mod-pinned templ, not @latest install-templ fetched templ@latest, which now emits templruntime.NewAttributeExpression - a symbol absent from the runtime pinned in go.mod - so every fresh 'make' regenerated uncompilable _templ.go files. Install the go.mod version and reinstall when the binary on PATH reports a different one. * admin: drop stray Makefile change belonging to the templ pin PR Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * notification: join the move destination path in log queue output A NewParentPath of "/" printed //name for moves into the root; path.Join keeps a single leading slash. * log queue: skip a nil event notification instead of logging an empty move Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * pb: tolerate nil chunk entries while scrubbing cipher keys for logs Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * subscription and backup failure logs name the event's file The process/marshal failure logs printed only the event directory and timestamp, so a failed sync left operators guessing which file failed. Use MetadataEventSourceFullPath/MetadataEventTargetFullPath so each failure names the source and target paths without printing the entries. * replication: identify entry-less events without printing the proto Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3: scrub cipher keys from the bucket-metadata debug dump buildBucketMetadata marshals the whole filer entry to JSON for a V(3) log line; a chunked entry would emit each chunk's cipher_key base64. Marshal the scrubbed copy instead. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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(filer_pb.ScrubCipherKeys(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)
|
|
}
|