diff --git a/api/cache/eacls.go b/api/cache/eacls.go new file mode 100644 index 00000000..b2e02ca4 --- /dev/null +++ b/api/cache/eacls.go @@ -0,0 +1,71 @@ +package cache + +import ( + "fmt" + "time" + + "github.com/bluele/gcache" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "go.uber.org/zap" +) + +type ( + // BucketACLCache contains cache with bucket EACL. + BucketACLCache struct { + cache gcache.Cache + logger *zap.Logger + } +) + +const ( + // DefaultEACLCacheSize is a default maximum number of entries in cache. + DefaultEACLCacheSize = 1e3 + // DefaultEACLCacheLifetime is a default lifetime of entries in cache. + DefaultEACLCacheLifetime = time.Minute +) + +// DefaultBucketACLCacheConfig returns new default cache expiration values. +func DefaultBucketACLCacheConfig(logger *zap.Logger) *Config { + return &Config{ + Size: DefaultEACLCacheSize, + Lifetime: DefaultEACLCacheLifetime, + Logger: logger, + } +} + +// NewEACLCache creates an object of BucketACLCache. +func NewEACLCache(config *Config) *BucketACLCache { + return &BucketACLCache{ + cache: gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build(), + logger: config.Logger} +} + +// Get returns a cached state. +func (o *BucketACLCache) Get(id cid.ID) *eacl.Table { + entry, err := o.cache.Get(id.String()) + if err != nil { + return nil + } + + result, ok := entry.(*eacl.Table) + if !ok { + o.logger.Warn("invalid cache entry type", + zap.String("actual", fmt.Sprintf("%T", entry)), + zap.String("expected", fmt.Sprintf("%T", result)), + ) + return nil + } + + return result +} + +// Put puts a state to cache. +func (o *BucketACLCache) Put(id cid.ID, v *eacl.Table) error { + return o.cache.Set(id.String(), v) +} + +// Delete deletes a state from cache. +func (o *BucketACLCache) Delete(id cid.ID) bool { + return o.cache.Remove(id.String()) +} diff --git a/api/data/info.go b/api/data/info.go index 4d1f86e9..b38da040 100644 --- a/api/data/info.go +++ b/api/data/info.go @@ -21,7 +21,21 @@ const ( VersioningSuspended = "Suspended" ) +const ( + // BucketACLEnabled means ACLs are enabled. + BucketACLEnabled BucketACLState = iota + + // BucketACLBucketOwnerEnforced mean ACLs are disabled. Any additional objects ACL raises AccessControlListNotSupported error. + BucketACLBucketOwnerEnforced + + // BucketACLBucketOwnerPreferred mean ACLs enabled. New object must be uploaded with `bucket-owner-full-control` canned ACL. + BucketACLBucketOwnerPreferred +) + type ( + // BucketACLState is bucket ACL state. + BucketACLState uint32 + // BucketInfo stores basic bucket data. BucketInfo struct { Name string diff --git a/api/handler/acl.go b/api/handler/acl.go index bbc7b2ba..c295c08d 100644 --- a/api/handler/acl.go +++ b/api/handler/acl.go @@ -103,11 +103,12 @@ type bucketPolicy struct { } type statement struct { - Sid string `json:"Sid"` - Effect string `json:"Effect"` - Principal principal `json:"Principal"` - Action stringOrSlice `json:"Action"` - Resource stringOrSlice `json:"Resource"` + Sid string `json:"Sid"` + Effect string `json:"Effect"` + Principal principal `json:"Principal"` + Action stringOrSlice `json:"Action"` + Resource stringOrSlice `json:"Resource"` + Condition map[string]map[string]string `json:"Condition"` } type principal struct { @@ -241,6 +242,26 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { return } + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket info", reqInfo, err) + return + } + + eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + if isBucketOwnerForced(eacl.EACL) { + if !isValidOwnerEnforced(r) { + h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) + return + } + r.Header.Set(api.AmzACL, "") + } + list := &AccessControlPolicy{} if r.ContentLength == 0 { list, err = parseACLHeaders(r.Header, iss) @@ -260,12 +281,6 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { return } - bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) - if err != nil { - h.logAndSendError(w, "could not get bucket info", reqInfo, err) - return - } - if _, err = h.updateBucketACL(r, astBucket, bktInfo, token); err != nil { h.logAndSendError(w, "could not update bucket acl", reqInfo, err) return @@ -364,6 +379,20 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { return } + eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + if isBucketOwnerForced(eacl.EACL) { + if !isValidOwnerEnforced(r) { + h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) + return + } + r.Header.Set(api.AmzACL, "") + } + p := &layer.HeadObjectParams{ BktInfo: bktInfo, Object: reqInfo.ObjectName, @@ -618,6 +647,7 @@ func formGrantee(granteeType, value string) (*Grantee, error) { func addPredefinedACP(acp *AccessControlPolicy, cannedACL string) (*AccessControlPolicy, error) { switch cannedACL { case basicACLPrivate: + case cannedACLBucketOwnerFullControl: case basicACLPublic: acp.AccessControlList = append(acp.AccessControlList, &Grant{ Grantee: &Grantee{ @@ -935,6 +965,16 @@ func tryServiceRecord(record eacl.Record) *ServiceRecord { func formRecords(resource *astResource) ([]*eacl.Record, error) { var res []*eacl.Record + if resource.Version == amzBucketOwnerEnforced && resource.Object == amzBucketOwnerEnforced { + res = append(res, bucketOwnerEnforcedRecord()) + return res, nil + } + + if resource.Version == aclEnabledObjectWriter && resource.Object == aclEnabledObjectWriter { + res = append(res, bucketACLObjectWriterRecord()) + return res, nil + } + for i := len(resource.Operations) - 1; i >= 0; i-- { astOp := resource.Operations[i] record := eacl.NewRecord() @@ -1017,6 +1057,40 @@ func policyToAst(bktPolicy *bucketPolicy) (*ast, error) { rr := make(map[string]*astResource) for _, state := range bktPolicy.Statement { + if state.Sid == "BucketOwnerEnforced" && + state.Action.Equal(stringOrSlice{values: []string{"*"}}) && + state.Effect == "Deny" && + state.Resource.Equal(stringOrSlice{values: []string{"*"}}) { + if conditionObj, ok := state.Condition["StringNotEquals"]; ok { + if val := conditionObj["s3:x-amz-object-ownership"]; val == amzBucketOwnerEnforced { + rr[amzBucketOwnerEnforced] = &astResource{ + resourceInfo: resourceInfo{ + Version: amzBucketOwnerEnforced, + Object: amzBucketOwnerEnforced, + }, + } + + continue + } + } + + return nil, fmt.Errorf("unsupported ownership: %v", state.Principal) + } + + if state.Sid == "BucketEnableACL" && + state.Action.Equal(stringOrSlice{values: []string{"s3:PutObject"}}) && + state.Effect == "Allow" && + state.Resource.Equal(stringOrSlice{values: []string{"*"}}) { + rr[aclEnabledObjectWriter] = &astResource{ + resourceInfo: resourceInfo{ + Version: aclEnabledObjectWriter, + Object: aclEnabledObjectWriter, + }, + } + + continue + } + if state.Principal.AWS != "" && state.Principal.AWS != allUsersWildcard || state.Principal.AWS == "" && state.Principal.CanonicalUser == "" { return nil, fmt.Errorf("unsupported principal: %v", state.Principal) @@ -1550,6 +1624,8 @@ func bucketACLToTable(acp *AccessControlPolicy) (*eacl.Table, error) { table.AddRecord(getOthersRecord(op, eacl.ActionDeny)) } + table.AddRecord(bucketOwnerEnforcedRecord()) + return table, nil } @@ -1580,3 +1656,63 @@ func getOthersRecord(op eacl.Operation, action eacl.Action) *eacl.Record { eacl.AddFormedTarget(record, eacl.RoleOthers) return record } + +func bucketOwnerEnforcedRecord() *eacl.Record { + var markerRecord = eacl.CreateRecord(eacl.ActionDeny, eacl.OperationPut) + markerRecord.AddFilter( + eacl.HeaderFromRequest, + eacl.MatchStringNotEqual, + amzBucketOwnerField, + amzBucketOwnerEnforced, + ) + + return markerRecord +} + +func isValidOwnerEnforced(r *http.Request) bool { + if cannedACL := r.Header.Get(api.AmzACL); cannedACL != "" { + switch cannedACL { + case basicACLPrivate: + return true + case cannedACLBucketOwnerFullControl: + return true + default: + return false + } + } + + return true +} + +func bucketACLObjectWriterRecord() *eacl.Record { + var markerRecord = eacl.CreateRecord(eacl.ActionAllow, eacl.OperationPut) + markerRecord.AddFilter( + eacl.HeaderFromRequest, + eacl.MatchStringEqual, + amzBucketOwnerField, + aclEnabledObjectWriter, + ) + + return markerRecord +} + +func isBucketOwnerForced(table *eacl.Table) bool { + if table == nil { + return false + } + + for _, r := range table.Records() { + if r.Action() == eacl.ActionDeny && r.Operation() == eacl.OperationPut { + for _, f := range r.Filters() { + if f.Key() == amzBucketOwnerField && + f.Value() == amzBucketOwnerEnforced && + f.From() == eacl.HeaderFromRequest && + f.Matcher() == eacl.MatchStringNotEqual { + return true + } + } + } + } + + return false +} diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index 6a76ec0c..78173d55 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -1183,6 +1183,7 @@ func TestBucketAclToTable(t *testing.T) { for _, op := range fullOps { expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny)) } + expectedTable.AddRecord(bucketOwnerEnforcedRecord()) actualTable, err := bucketACLToTable(acl) require.NoError(t, err) @@ -1356,10 +1357,23 @@ func TestPutBucketACL(t *testing.T) { bktInfo := createBucket(t, tc, bktName, box) header := map[string]string{api.AmzACL: "public-read"} - putBucketACL(t, tc, bktName, box, header) + // ACLs disabled. + putBucketACL(t, tc, bktName, box, header, http.StatusBadRequest) + aclPolicy := &bucketPolicy{ + Statement: []statement{{ + Sid: "BucketEnableACL", + Effect: "Allow", + Action: stringOrSlice{values: []string{"s3:PutObject"}}, + Resource: stringOrSlice{values: []string{"*"}}, + }}, + } + putBucketPolicy(tc, bktName, aclPolicy, box, http.StatusOK) + + // ACLs enabled. + putBucketACL(t, tc, bktName, box, header, http.StatusOK) header = map[string]string{api.AmzACL: "private"} - putBucketACL(t, tc, bktName, box, header) + putBucketACL(t, tc, bktName, box, header, http.StatusOK) checkLastRecords(t, tc, bktInfo, eacl.ActionDeny) } @@ -1481,7 +1495,7 @@ func createBucket(t *testing.T, tc *handlerContext, bktName string, box *accessb return bktInfo } -func putBucketACL(t *testing.T, tc *handlerContext, bktName string, box *accessbox.Box, header map[string]string) { +func putBucketACL(t *testing.T, tc *handlerContext, bktName string, box *accessbox.Box, header map[string]string, status int) { w, r := prepareTestRequest(tc, bktName, "", nil) for key, val := range header { r.Header.Set(key, val) @@ -1489,7 +1503,7 @@ func putBucketACL(t *testing.T, tc *handlerContext, bktName string, box *accessb ctx := context.WithValue(r.Context(), api.BoxData, box) r = r.WithContext(ctx) tc.Handler().PutBucketACLHandler(w, r) - assertStatus(t, w, http.StatusOK) + assertStatus(t, w, status) } func generateRecord(action eacl.Action, op eacl.Operation, targets []eacl.Target) *eacl.Record { diff --git a/api/handler/api.go b/api/handler/api.go index 519437d9..0fc409f9 100644 --- a/api/handler/api.go +++ b/api/handler/api.go @@ -1,12 +1,15 @@ package handler import ( + "context" "errors" "time" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neofs-s3-gw/api" + "github.com/nspcc-dev/neofs-s3-gw/api/data" "github.com/nspcc-dev/neofs-s3-gw/api/layer" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/netmap" "go.uber.org/zap" ) @@ -45,6 +48,11 @@ type ( // Returns [models.ErrNotFound] if policy not found. GetPlacementPolicy(userAddr util.Uint160, policyName string) (*netmap.PlacementPolicy, error) } + + // ACLStateProvider get bucket ACL state. + ACLStateProvider interface { + GetState(ctx context.Context, idCnr cid.ID) (data.BucketACLState, error) + } ) const ( diff --git a/api/handler/copy.go b/api/handler/copy.go index 22a2f77a..283d2655 100644 --- a/api/handler/copy.go +++ b/api/handler/copy.go @@ -91,6 +91,20 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { } if containsACL { + eacl, err := h.obj.GetBucketACL(r.Context(), dstBktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + if isBucketOwnerForced(eacl.EACL) { + if !isValidOwnerEnforced(r) { + h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) + return + } + r.Header.Set(api.AmzACL, "") + } + if sessionTokenEACL, err = getSessionTokenSetEACL(r.Context()); err != nil { h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err) return diff --git a/api/handler/json_types.go b/api/handler/json_types.go index 6795bca7..cc54753d 100644 --- a/api/handler/json_types.go +++ b/api/handler/json_types.go @@ -2,6 +2,7 @@ package handler import ( "encoding/json" + "slices" ) type ( @@ -37,3 +38,14 @@ func (s stringOrSlice) MarshalJSON() ([]byte, error) { return json.Marshal(values) } + +func (s stringOrSlice) Equal(v stringOrSlice) bool { + if len(s.values) != len(v.values) { + return false + } + + slices.Sort(s.values) + slices.Sort(v.values) + + return slices.Compare(s.values, v.values) == 0 +} diff --git a/api/handler/multipart_upload.go b/api/handler/multipart_upload.go index 44980142..dc834520 100644 --- a/api/handler/multipart_upload.go +++ b/api/handler/multipart_upload.go @@ -113,6 +113,20 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re } if containsACLHeaders(r) { + eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + if isBucketOwnerForced(eacl.EACL) { + if !isValidOwnerEnforced(r) { + h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) + return + } + r.Header.Set(api.AmzACL, "") + } + iss, err := h.bearerTokenIssuer(r.Context()) if err != nil { h.logAndSendError(w, "couldn't get bearer token issuer", reqInfo, err) @@ -166,7 +180,7 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re UploadID: uploadID, } - additional = append(additional, zap.String("uploadID", uploadID)) + additional = append(additional, zap.String("uploadID", uploadID), zap.String("reqId", reqInfo.RequestID)) if err = api.EncodeToResponse(w, resp); err != nil { h.logAndSendError(w, "could not encode InitiateMultipartUploadResponse to response", reqInfo, err, additional...) return @@ -204,7 +218,7 @@ func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) { var ( queryValues = r.URL.Query() uploadID = queryValues.Get(uploadIDHeaderName) - additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName)} + additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName), zap.String("reqId", reqInfo.RequestID)} ) partNumber, err := strconv.Atoi(queryValues.Get(partNumberHeaderName)) @@ -250,7 +264,7 @@ func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) { reqInfo = api.GetReqInfo(r.Context()) queryValues = reqInfo.URL.Query() uploadID = queryValues.Get(uploadIDHeaderName) - additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName)} + additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName), zap.String("reqId", reqInfo.RequestID)} ) partNumber, err := strconv.Atoi(queryValues.Get(partNumberHeaderName)) @@ -378,7 +392,7 @@ func (h *handler) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http. Bkt: bktInfo, Key: reqInfo.ObjectName, } - additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName)} + additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName), zap.String("reqId", reqInfo.RequestID)} ) reqBody := new(CompleteMultipartUpload) @@ -545,7 +559,7 @@ func (h *handler) ListPartsHandler(w http.ResponseWriter, r *http.Request) { queryValues = reqInfo.URL.Query() uploadID = queryValues.Get(uploadIDHeaderName) - additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName)} + additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName), zap.String("reqId", reqInfo.RequestID)} maxParts = layer.MaxSizePartsList ) @@ -604,7 +618,7 @@ func (h *handler) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Req } uploadID := reqInfo.URL.Query().Get(uploadIDHeaderName) - additional := []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName)} + additional := []zap.Field{zap.String("uploadID", uploadID), zap.String("Key", reqInfo.ObjectName), zap.String("reqId", reqInfo.RequestID)} p := &layer.UploadInfoParams{ UploadID: uploadID, diff --git a/api/handler/put.go b/api/handler/put.go index 8f98c241..e1c59ff9 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -164,10 +164,16 @@ func (p *policyCondition) UnmarshalJSON(data []byte) error { // keywords of predefined basic ACL values. const ( - basicACLPrivate = "private" - basicACLReadOnly = "public-read" - basicACLPublic = "public-read-write" - cannedACLAuthRead = "authenticated-read" + basicACLPrivate = "private" + basicACLReadOnly = "public-read" + basicACLPublic = "public-read-write" + cannedACLAuthRead = "authenticated-read" + cannedACLBucketOwnerFullControl = "bucket-owner-full-control" + + amzBucketOwnerField = "BucketOwnerEnforcedField" + amzBucketOwnerEnforced = "BucketOwnerEnforced" + + aclEnabledObjectWriter = "ObjectWriter" ) type createBucketParams struct { @@ -203,6 +209,22 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { return } + if containsACL { + eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + if isBucketOwnerForced(eacl.EACL) { + if !isValidOwnerEnforced(r) { + h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) + return + } + r.Header.Set(api.AmzACL, "") + } + } + metadata := parseMetadata(r) if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 { metadata[api.ContentType] = contentType @@ -436,6 +458,22 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { return } + if containsACL { + eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + if isBucketOwnerForced(eacl.EACL) { + if !isValidOwnerEnforced(r) { + h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) + return + } + r.Header.Set(api.AmzACL, "") + } + } + params := &layer.PutObjectParams{ BktInfo: bktInfo, Object: reqInfo.ObjectName, diff --git a/api/layer/cache.go b/api/layer/cache.go index e118aa76..431e96d8 100644 --- a/api/layer/cache.go +++ b/api/layer/cache.go @@ -4,19 +4,21 @@ import ( "github.com/nspcc-dev/neofs-s3-gw/api/cache" "github.com/nspcc-dev/neofs-s3-gw/api/data" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/user" "go.uber.org/zap" ) type Cache struct { - logger *zap.Logger - listsCache *cache.ObjectsListCache - objCache *cache.ObjectsCache - namesCache *cache.ObjectsNameCache - bucketCache *cache.BucketCache - systemCache *cache.SystemCache - accessCache *cache.AccessControlCache + logger *zap.Logger + listsCache *cache.ObjectsListCache + objCache *cache.ObjectsCache + namesCache *cache.ObjectsNameCache + bucketCache *cache.BucketCache + bucketACLCache *cache.BucketACLCache + systemCache *cache.SystemCache + accessCache *cache.AccessControlCache } // CachesConfig contains params for caches. @@ -26,6 +28,7 @@ type CachesConfig struct { ObjectsList *cache.Config Names *cache.Config Buckets *cache.Config + BucketACLs *cache.Config System *cache.Config AccessControl *cache.Config } @@ -38,6 +41,7 @@ func DefaultCachesConfigs(logger *zap.Logger) *CachesConfig { ObjectsList: cache.DefaultObjectsListConfig(logger), Names: cache.DefaultObjectsNameConfig(logger), Buckets: cache.DefaultBucketConfig(logger), + BucketACLs: cache.DefaultBucketACLCacheConfig(logger), System: cache.DefaultSystemConfig(logger), AccessControl: cache.DefaultAccessControlConfig(logger), } @@ -45,13 +49,14 @@ func DefaultCachesConfigs(logger *zap.Logger) *CachesConfig { func NewCache(cfg *CachesConfig) *Cache { return &Cache{ - logger: cfg.Logger, - listsCache: cache.NewObjectsListCache(cfg.ObjectsList), - objCache: cache.New(cfg.Objects), - namesCache: cache.NewObjectsNameCache(cfg.Names), - bucketCache: cache.NewBucketCache(cfg.Buckets), - systemCache: cache.NewSystemCache(cfg.System), - accessCache: cache.NewAccessControlCache(cfg.AccessControl), + logger: cfg.Logger, + listsCache: cache.NewObjectsListCache(cfg.ObjectsList), + objCache: cache.New(cfg.Objects), + namesCache: cache.NewObjectsNameCache(cfg.Names), + bucketCache: cache.NewBucketCache(cfg.Buckets), + bucketACLCache: cache.NewEACLCache(cfg.BucketACLs), + systemCache: cache.NewSystemCache(cfg.System), + accessCache: cache.NewAccessControlCache(cfg.AccessControl), } } @@ -249,3 +254,14 @@ func (c *Cache) PutNotificationConfiguration(owner user.ID, bktInfo *data.Bucket c.logger.Warn("couldn't cache access control operation", zap.Error(err)) } } + +func (c *Cache) GetBucketACL(id cid.ID) *eacl.Table { + return c.bucketACLCache.Get(id) +} + +func (c *Cache) PutBucketACL(id cid.ID, acl *eacl.Table) { + var err = c.bucketACLCache.Put(id, acl) + if err != nil { + c.logger.Warn("couldn't cache bucket EACL", zap.Error(err), zap.String("cid", id.String())) + } +} diff --git a/api/layer/container.go b/api/layer/container.go index f70ac043..0f2bc753 100644 --- a/api/layer/container.go +++ b/api/layer/container.go @@ -175,6 +175,7 @@ func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da } n.cache.PutBucket(bktInfo) + n.cache.PutBucketACL(bktInfo.CID, p.EACL) return bktInfo, nil } diff --git a/api/layer/layer.go b/api/layer/layer.go index 1956138c..e9136c39 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -376,10 +376,16 @@ func (n *layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInf // GetBucketACL returns bucket acl info by name. func (n *layer) GetBucketACL(ctx context.Context, bktInfo *data.BucketInfo) (*BucketACL, error) { + var eACL = n.cache.GetBucketACL(bktInfo.CID) + + if eACL == nil { + return &BucketACL{Info: bktInfo, EACL: eACL}, nil + } eACL, err := n.GetContainerEACL(ctx, bktInfo.CID) if err != nil { return nil, fmt.Errorf("get container eacl: %w", err) } + n.cache.PutBucketACL(bktInfo.CID, eACL) return &BucketACL{ Info: bktInfo, diff --git a/api/layer/multipart_upload.go b/api/layer/multipart_upload.go index 902be1d2..8a84aa91 100644 --- a/api/layer/multipart_upload.go +++ b/api/layer/multipart_upload.go @@ -269,7 +269,7 @@ func (n *layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf // The previous part is not uploaded yet. if lastPart == nil { - return nil, s3errors.GetAPIError(s3errors.ErrOperationAborted) + return nil, fmt.Errorf("uploading %d, unable get %d: %w", p.PartNumber, p.PartNumber-1, s3errors.GetAPIError(s3errors.ErrOperationAborted)) } // try to restore hash state from the last part. diff --git a/api/router.go b/api/router.go index 75174a9c..4a788632 100644 --- a/api/router.go +++ b/api/router.go @@ -171,6 +171,14 @@ func logErrorResponse(l *zap.Logger) mux.MiddlewareFunc { lw := &logResponseWriter{ResponseWriter: w} reqInfo := GetReqInfo(r.Context()) + l.Info("call method", + zap.String("host", r.Host), + zap.String("request_id", reqInfo.RequestID), + zap.String("method", mux.CurrentRoute(r).GetName()), + zap.String("bucket", reqInfo.BucketName), + zap.String("object", reqInfo.ObjectName), + ) + // pass execution: h.ServeHTTP(lw, r) @@ -179,13 +187,9 @@ func logErrorResponse(l *zap.Logger) mux.MiddlewareFunc { return } - l.Info("call method", - zap.Int("status", lw.statusCode), - zap.String("host", r.Host), + l.Info("call method result", zap.String("request_id", reqInfo.RequestID), - zap.String("method", mux.CurrentRoute(r).GetName()), - zap.String("bucket", reqInfo.BucketName), - zap.String("object", reqInfo.ObjectName), + zap.Int("status", lw.statusCode), zap.String("description", http.StatusText(lw.statusCode))) }) } diff --git a/api/s3errors/errors.go b/api/s3errors/errors.go index b83d613d..75d3b39c 100644 --- a/api/s3errors/errors.go +++ b/api/s3errors/errors.go @@ -275,6 +275,8 @@ const ( // CORS configuration errors. ErrCORSUnsupportedMethod ErrCORSWildcardExposeHeaders + + ErrAccessControlListNotSupported ) // error code to Error structure, these fields carry respective @@ -1689,6 +1691,12 @@ var errorCodes = errorCodeMap{ Description: "A conflicting conditional operation is currently in progress against this resource. Try again.", HTTPStatusCode: http.StatusConflict, }, + ErrAccessControlListNotSupported: { + ErrCode: ErrAccessControlListNotSupported, + Code: "AccessControlListNotSupported", + Description: "The bucket does not allow ACLs.", + HTTPStatusCode: http.StatusBadRequest, + }, // Add your error structure here. } diff --git a/docs/aws_s3_compat.md b/docs/aws_s3_compat.md index 698ea0cf..f5c5eccb 100644 --- a/docs/aws_s3_compat.md +++ b/docs/aws_s3_compat.md @@ -290,6 +290,41 @@ See also `GetObject` and other method parameters. | 🟡 | PutBucketPolicy | See ACL limitations | | 🔵 | PutBucketReplication | | +By default bucket ACLs is disabled. See details [here](https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html). +In case you need to disable ACLs manually (for instance your bucket has ACLs enabled) you should use `PutBucketPolicy` command with the next policy: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BucketOwnerEnforced", + "Action": "*", + "Effect": "Deny", + "Resource": "*", + "Condition": { + "StringNotEquals": { + "s3:x-amz-object-ownership": "BucketOwnerEnforced" + } + } + } + ] +} +``` +In case you need to enable ACLs (not recommended) option you should use `PutBucketPolicy` command with the next policy: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BucketEnableACL", + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": "*" + } + ] +} +``` + ## Request payment | | Method | Comments |