diff --git a/api/handler/acl.go b/api/handler/acl.go index c295c08d..abad8933 100644 --- a/api/handler/acl.go +++ b/api/handler/acl.go @@ -13,6 +13,8 @@ import ( "strconv" "strings" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "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" @@ -25,6 +27,31 @@ import ( "go.uber.org/zap" ) +var ( + // There are special user.IDs for ACL marker ownership records. + + // NKuyBkoGdZZSLyPbJEetheRhMjezqzTxcW. + ownerEnforcedUserID user.ID + // NKuyBkoGdZZSLyPbJEetheRhMjezwhg9vh. + ownerPreferredUserID user.ID + // NKuyBkoGdZZSLyPbJEetheRhMjf17rTWMC. + ownerObjectWriterUserID user.ID +) + +func init() { + ownerEnforcedUserID[0] = address.Prefix + ownerEnforcedUserID[20] = 1 + copy(ownerEnforcedUserID[21:], hash.Checksum(ownerEnforcedUserID[:21])) + + ownerPreferredUserID[0] = address.Prefix + ownerPreferredUserID[20] = 2 + copy(ownerPreferredUserID[21:], hash.Checksum(ownerPreferredUserID[:21])) + + ownerObjectWriterUserID[0] = address.Prefix + ownerObjectWriterUserID[20] = 3 + copy(ownerObjectWriterUserID[21:], hash.Checksum(ownerObjectWriterUserID[:21])) +} + var ( writeOps = []eacl.Operation{eacl.OperationPut, eacl.OperationDelete} writeOpsMap = map[eacl.Operation]struct{}{ @@ -393,6 +420,14 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { r.Header.Set(api.AmzACL, "") } + if isBucketOwnerPreferred(eacl.EACL) { + if !isValidOwnerPreferred(r) { + h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) + return + } + r.Header.Set(api.AmzACL, "") + } + p := &layer.HeadObjectParams{ BktInfo: bktInfo, Object: reqInfo.ObjectName, @@ -965,18 +1000,27 @@ 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] + + if len(astOp.Users) == 1 { + var markerRecord *eacl.Record + + switch astOp.Users[0] { + case ownerEnforcedUserID: + markerRecord = bucketOwnerEnforcedRecord() + case ownerPreferredUserID: + markerRecord = bucketOwnerPreferredRecord() + case ownerObjectWriterUserID: + markerRecord = bucketACLObjectWriterRecord() + } + + if markerRecord != nil { + res = append(res, markerRecord) + return res, nil + } + } + record := eacl.NewRecord() record.SetOperation(astOp.Op) record.SetAction(astOp.Action) @@ -1014,8 +1058,9 @@ func addToList(operations []*astOperation, rec eacl.Record, target eacl.Target) ) for _, astOp := range operations { - if astOp.Op == rec.Operation() && astOp.IsGroupGrantee() == groupTarget { + if astOp.Op == rec.Operation() && astOp.Action == rec.Action() && astOp.IsGroupGrantee() == groupTarget { found = astOp + break } } @@ -1057,40 +1102,6 @@ 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) @@ -1666,6 +1677,10 @@ func bucketOwnerEnforcedRecord() *eacl.Record { amzBucketOwnerEnforced, ) + t := eacl.NewTarget() + t.SetAccounts([]user.ID{ownerEnforcedUserID}) + markerRecord.SetTargets(*t) + return markerRecord } @@ -1685,14 +1700,18 @@ func isValidOwnerEnforced(r *http.Request) bool { } func bucketACLObjectWriterRecord() *eacl.Record { - var markerRecord = eacl.CreateRecord(eacl.ActionAllow, eacl.OperationPut) + var markerRecord = eacl.CreateRecord(eacl.ActionDeny, eacl.OperationPut) markerRecord.AddFilter( eacl.HeaderFromRequest, - eacl.MatchStringEqual, + eacl.MatchStringNotEqual, amzBucketOwnerField, - aclEnabledObjectWriter, + amzBucketOwnerObjectWriter, ) + t := eacl.NewTarget() + t.SetAccounts([]user.ID{ownerObjectWriterUserID}) + markerRecord.SetTargets(*t) + return markerRecord } @@ -1708,7 +1727,109 @@ func isBucketOwnerForced(table *eacl.Table) bool { f.Value() == amzBucketOwnerEnforced && f.From() == eacl.HeaderFromRequest && f.Matcher() == eacl.MatchStringNotEqual { - return true + if len(r.Targets()) == 1 && len(r.Targets()[0].Accounts()) == 1 { + return r.Targets()[0].Accounts()[0] == ownerEnforcedUserID + } + } + } + } + } + + return false +} + +func bucketOwnerPreferredRecord() *eacl.Record { + var markerRecord = eacl.CreateRecord(eacl.ActionDeny, eacl.OperationPut) + markerRecord.AddFilter( + eacl.HeaderFromRequest, + eacl.MatchStringNotEqual, + amzBucketOwnerField, + amzBucketOwnerPreferred, + ) + + t := eacl.NewTarget() + t.SetAccounts([]user.ID{ownerPreferredUserID}) + markerRecord.SetTargets(*t) + + return markerRecord +} + +func isBucketOwnerPreferred(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() == amzBucketOwnerPreferred && + f.From() == eacl.HeaderFromRequest && + f.Matcher() == eacl.MatchStringNotEqual { + if len(r.Targets()) == 1 && len(r.Targets()[0].Accounts()) == 1 { + return r.Targets()[0].Accounts()[0] == ownerPreferredUserID + } + } + } + } + } + + return false +} + +func isValidOwnerPreferred(r *http.Request) bool { + cannedACL := r.Header.Get(api.AmzACL) + return cannedACL == cannedACLBucketOwnerFullControl +} + +func updateBucketOwnership(records []eacl.Record, newRecord *eacl.Record) []eacl.Record { + var ( + rowID = -1 + ) + + for index, r := range records { + if r.Action() == eacl.ActionDeny && r.Operation() == eacl.OperationPut { + for _, f := range r.Filters() { + if f.Key() == amzBucketOwnerField && + f.From() == eacl.HeaderFromRequest && + f.Matcher() == eacl.MatchStringNotEqual { + rowID = index + break + } + } + } + + if rowID > -1 { + break + } + } + + if rowID > -1 { + records = slices.Delete(records, rowID, rowID+1) + } + + if newRecord != nil { + records = append(records, *newRecord) + } + + return records +} + +func isBucketOwnerObjectWriter(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() == amzBucketOwnerObjectWriter && + f.From() == eacl.HeaderFromRequest && + f.Matcher() == eacl.MatchStringNotEqual { + if len(r.Targets()) == 1 && len(r.Targets()[0].Accounts()) == 1 { + return r.Targets()[0].Accounts()[0] == ownerObjectWriterUserID + } } } } diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index 78173d55..b0e5293f 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -8,9 +8,11 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "encoding/xml" "fmt" "io" "net/http" + "net/http/httptest" "testing" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -1360,21 +1362,13 @@ func TestPutBucketACL(t *testing.T) { // 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) + putBucketOwnership(tc, bktName, box, amzBucketOwnerObjectWriter, 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, http.StatusOK) - checkLastRecords(t, tc, bktInfo, eacl.ActionDeny) + checkLastRecords(t, tc, bktInfo, eacl.ActionDeny, ownerObjectWriterUserID) } func TestBucketPolicy(t *testing.T) { @@ -1390,7 +1384,13 @@ func TestBucketPolicy(t *testing.T) { require.Equal(t, user.NewFromScriptHash(key.GetScriptHash()).String(), st.Principal.CanonicalUser) require.Equal(t, []string{arnAwsPrefix + bktName}, st.Resource.values) } else { - require.Equal(t, allUsersWildcard, st.Principal.AWS) + if st.Principal.AWS != "" { + require.Equal(t, allUsersWildcard, st.Principal.AWS) + } else { + // special marker record for ownership. + require.Equal(t, ownerEnforcedUserID.String(), st.Principal.CanonicalUser) + } + require.Equal(t, "Deny", st.Effect) require.Equal(t, []string{arnAwsPrefix + bktName}, st.Resource.values) } @@ -1443,7 +1443,32 @@ func putBucketPolicy(hc *handlerContext, bktName string, bktPolicy *bucketPolicy assertStatus(hc.t, w, status) } -func checkLastRecords(t *testing.T, tc *handlerContext, bktInfo *data.BucketInfo, action eacl.Action) { +func putBucketOwnership(hc *handlerContext, bktName string, box *accessbox.Box, ownership string, status int) { + p := putBucketOwnershipControlsParams{ + Rules: []objectOwnershipRules{ + { + ObjectOwnership: ownership, + }, + }, + } + + var b bytes.Buffer + err := xml.NewEncoder(&b).Encode(p) + require.NoError(hc.t, err) + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPut, defaultURL, &b) + + reqInfo := api.NewReqInfo(w, r, api.ObjectRequest{Bucket: bktName}) + r = r.WithContext(api.SetReqInfo(hc.Context(), reqInfo)) + ctx := context.WithValue(r.Context(), api.BoxData, box) + r = r.WithContext(ctx) + hc.Handler().PutBucketOwnershipControlsHandler(w, r) + + assertStatus(hc.t, w, status) +} + +func checkLastRecords(t *testing.T, tc *handlerContext, bktInfo *data.BucketInfo, action eacl.Action, markerUserID user.ID) { bktACL, err := tc.Layer().GetBucketACL(tc.Context(), bktInfo) require.NoError(t, err) @@ -1454,8 +1479,11 @@ func checkLastRecords(t *testing.T, tc *handlerContext, bktInfo *data.BucketInfo } for _, rec := range bktACL.EACL.Records()[length-7:] { - if rec.Action() != action || rec.Targets()[0].Role() != eacl.RoleOthers { - t.Fatalf("inavid last record: '%s', '%s', '%s',", rec.Action(), rec.Operation(), rec.Targets()[0].Role()) + if rec.Targets()[0].Role() == eacl.RoleOthers { + require.Equal(t, action, rec.Action()) + } else { + // special ownership marker rule. + require.Equal(t, markerUserID, rec.Targets()[0].Accounts()[0]) } } } diff --git a/api/handler/copy.go b/api/handler/copy.go index 283d2655..8eef55dc 100644 --- a/api/handler/copy.go +++ b/api/handler/copy.go @@ -90,13 +90,13 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if containsACL { - eacl, err := h.obj.GetBucketACL(r.Context(), dstBktInfo) - if err != nil { - h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) - return - } + eacl, err := h.obj.GetBucketACL(r.Context(), dstBktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + if containsACL { if isBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) @@ -111,6 +111,14 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { } } + if isBucketOwnerPreferred(eacl.EACL) { + if !isValidOwnerPreferred(r) { + h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) + return + } + r.Header.Set(api.AmzACL, "") + } + extendedSrcObjInfo, err := h.obj.GetExtendedObjectInfo(r.Context(), srcObjPrm) if err != nil { h.logAndSendError(w, "could not find object", reqInfo, err) diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go index 115a31f2..21e02fed 100644 --- a/api/handler/handlers_test.go +++ b/api/handler/handlers_test.go @@ -124,6 +124,20 @@ func createTestBucket(hc *handlerContext, bktName string) *data.BucketInfo { bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) require.NoError(hc.t, err) + + p := layer.PutBucketACLParams{ + BktInfo: bktInfo, + } + + acp, err := parseACLHeaders(http.Header{}, hc.owner) + require.NoError(hc.t, err) + + p.EACL, err = bucketACLToTable(acp) + require.NoError(hc.t, err) + + err = hc.Layer().PutBucketACL(hc.Context(), &p) + require.NoError(hc.t, err) + return bktInfo } @@ -144,6 +158,19 @@ func createTestBucketWithLock(hc *handlerContext, bktName string, conf *data.Obj Owner: ownerID, } + p := layer.PutBucketACLParams{ + BktInfo: bktInfo, + } + + acp, err := parseACLHeaders(http.Header{}, hc.owner) + require.NoError(hc.t, err) + + p.EACL, err = bucketACLToTable(acp) + require.NoError(hc.t, err) + + err = hc.Layer().PutBucketACL(hc.Context(), &p) + require.NoError(hc.t, err) + sp := &layer.PutSettingsParams{ BktInfo: bktInfo, Settings: &data.BucketSettings{ diff --git a/api/handler/multipart_upload.go b/api/handler/multipart_upload.go index dc834520..d8bb2d3b 100644 --- a/api/handler/multipart_upload.go +++ b/api/handler/multipart_upload.go @@ -112,13 +112,13 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re Data: &layer.UploadData{}, } - 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 - } + eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + if containsACLHeaders(r) { if isBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) @@ -139,6 +139,14 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re p.Data.ACLHeaders = formACLHeadersForMultipart(r.Header) } + if isBucketOwnerPreferred(eacl.EACL) { + if !isValidOwnerPreferred(r) { + h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) + return + } + r.Header.Set(api.AmzACL, "") + } + if len(r.Header.Get(api.AmzTagging)) > 0 { p.Data.TagSet, err = parseTaggingHeader(r.Header) if err != nil { diff --git a/api/handler/ownership.go b/api/handler/ownership.go new file mode 100644 index 00000000..af8de936 --- /dev/null +++ b/api/handler/ownership.go @@ -0,0 +1,210 @@ +package handler + +import ( + "encoding/xml" + "io" + "net/http" + + "github.com/nspcc-dev/neofs-s3-gw/api" + "github.com/nspcc-dev/neofs-s3-gw/api/layer" + "github.com/nspcc-dev/neofs-s3-gw/api/s3errors" + "github.com/nspcc-dev/neofs-sdk-go/eacl" +) + +type ( + putBucketOwnershipControlsParams struct { + Rules []objectOwnershipRules `xml:"Rule"` + } + + objectOwnershipRules struct { + ObjectOwnership string `xml:"ObjectOwnership"` + } +) + +const ( + xAmzExpectedBucketOwner = "x-amz-expected-bucket-owner" +) + +func decodeXML(r io.Reader, destination any) error { + if err := xml.NewDecoder(r).Decode(destination); err != nil { + return s3errors.GetAPIError(s3errors.ErrMalformedXML) + } + + return nil +} + +func (h *handler) PutBucketOwnershipControlsHandler(w http.ResponseWriter, r *http.Request) { + var ( + reqInfo = api.GetReqInfo(r.Context()) + params putBucketOwnershipControlsParams + rec *eacl.Record + ) + + defer func() { + _ = r.Body.Close() + }() + + if err := decodeXML(r.Body, ¶ms); err != nil { + h.logAndSendError(w, "could not parse body", reqInfo, err) + return + } + + if len(params.Rules) == 0 { + h.logAndSendError(w, "empty rules list", reqInfo, s3errors.GetAPIError(s3errors.ErrEmptyRequestBody)) + return + } + + switch params.Rules[0].ObjectOwnership { + case amzBucketOwnerEnforced: + rec = bucketOwnerEnforcedRecord() + case amzBucketOwnerPreferred: + rec = bucketOwnerPreferredRecord() + case amzBucketOwnerObjectWriter: + rec = bucketACLObjectWriterRecord() + default: + h.logAndSendError(w, "invalid ownership", reqInfo, s3errors.GetAPIError(s3errors.ErrBadRequest)) + return + } + + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err) + return + } + + if expectedBucketOwner := r.Header.Get(xAmzExpectedBucketOwner); expectedBucketOwner != "" { + if expectedBucketOwner != bktInfo.Owner.String() { + h.logAndSendError(w, "bucket owner mismatch", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) + } + } + + token, err := getSessionTokenSetEACL(r.Context()) + if err != nil { + h.logAndSendError(w, "couldn't get eacl token", reqInfo, err) + return + } + + bucketACL, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + var newEACL eacl.Table + + newRecords := updateBucketOwnership(bucketACL.EACL.Records(), rec) + for _, record := range newRecords { + newEACL.AddRecord(&record) + } + + p := layer.PutBucketACLParams{ + BktInfo: bktInfo, + EACL: &newEACL, + SessionToken: token, + } + + if err = h.obj.PutBucketACL(r.Context(), &p); err != nil { + h.logAndSendError(w, "could not put bucket eacl", reqInfo, err) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (h *handler) GetBucketOwnershipControlsHandler(w http.ResponseWriter, r *http.Request) { + var ( + reqInfo = api.GetReqInfo(r.Context()) + response *putBucketOwnershipControlsParams + ) + + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err) + return + } + + if expectedBucketOwner := r.Header.Get(xAmzExpectedBucketOwner); expectedBucketOwner != "" { + if expectedBucketOwner != bktInfo.Owner.String() { + h.logAndSendError(w, "bucket owner mismatch", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) + } + } + + bucketACL, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + if isBucketOwnerForced(bucketACL.EACL) { + response = &putBucketOwnershipControlsParams{ + Rules: []objectOwnershipRules{{ObjectOwnership: amzBucketOwnerEnforced}}, + } + } else if isBucketOwnerPreferred(bucketACL.EACL) { + response = &putBucketOwnershipControlsParams{ + Rules: []objectOwnershipRules{{ObjectOwnership: amzBucketOwnerPreferred}}, + } + } else if isBucketOwnerObjectWriter(bucketACL.EACL) { + response = &putBucketOwnershipControlsParams{ + Rules: []objectOwnershipRules{{ObjectOwnership: amzBucketOwnerObjectWriter}}, + } + } + + if response == nil { + api.WriteSuccessResponseHeadersOnly(w) + return + } + + if err = api.EncodeToResponse(w, response); err != nil { + h.logAndSendError(w, "something went wrong", reqInfo, err) + } +} + +func (h *handler) DeleteBucketOwnershipControlsHandler(w http.ResponseWriter, r *http.Request) { + var ( + reqInfo = api.GetReqInfo(r.Context()) + ) + + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err) + return + } + + if expectedBucketOwner := r.Header.Get(xAmzExpectedBucketOwner); expectedBucketOwner != "" { + if expectedBucketOwner != bktInfo.Owner.String() { + h.logAndSendError(w, "bucket owner mismatch", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) + } + } + + token, err := getSessionTokenSetEACL(r.Context()) + if err != nil { + h.logAndSendError(w, "couldn't get eacl token", reqInfo, err) + return + } + + bucketACL, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + + var newEACL eacl.Table + + newRecords := updateBucketOwnership(bucketACL.EACL.Records(), nil) + for _, record := range newRecords { + newEACL.AddRecord(&record) + } + + p := layer.PutBucketACLParams{ + BktInfo: bktInfo, + EACL: &newEACL, + SessionToken: token, + } + + if err = h.obj.PutBucketACL(r.Context(), &p); err != nil { + h.logAndSendError(w, "could not put bucket eacl", reqInfo, err) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/api/handler/put.go b/api/handler/put.go index e1c59ff9..62864e9b 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -170,10 +170,10 @@ const ( cannedACLAuthRead = "authenticated-read" cannedACLBucketOwnerFullControl = "bucket-owner-full-control" - amzBucketOwnerField = "BucketOwnerEnforcedField" - amzBucketOwnerEnforced = "BucketOwnerEnforced" - - aclEnabledObjectWriter = "ObjectWriter" + amzBucketOwnerField = "BucketOwnerField" + amzBucketOwnerEnforced = "BucketOwnerEnforced" + amzBucketOwnerPreferred = "BucketOwnerPreferred" + amzBucketOwnerObjectWriter = "ObjectWriter" ) type createBucketParams struct { @@ -209,13 +209,13 @@ 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 - } + eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + if containsACL { if isBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) @@ -225,6 +225,14 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { } } + if isBucketOwnerPreferred(eacl.EACL) { + if !isValidOwnerPreferred(r) { + h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) + return + } + r.Header.Set(api.AmzACL, "") + } + metadata := parseMetadata(r) if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 { metadata[api.ContentType] = contentType @@ -458,13 +466,13 @@ 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 - } + eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) + return + } + if containsACL { if isBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) @@ -474,6 +482,14 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { } } + if isBucketOwnerPreferred(eacl.EACL) { + if !isValidOwnerPreferred(r) { + h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) + return + } + r.Header.Set(api.AmzACL, "") + } + params := &layer.PutObjectParams{ BktInfo: bktInfo, Object: reqInfo.ObjectName, diff --git a/api/handler/put_test.go b/api/handler/put_test.go index b0057f3b..7260b6bc 100644 --- a/api/handler/put_test.go +++ b/api/handler/put_test.go @@ -117,6 +117,8 @@ func TestPutObjectOverrideCopiesNumber(t *testing.T) { r.Header.Set(api.MetadataPrefix+strings.ToUpper(layer.AttributeNeofsCopiesNumber), "1") tc.Handler().PutObjectHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + p := &layer.HeadObjectParams{ BktInfo: bktInfo, Object: objName, diff --git a/api/layer/container.go b/api/layer/container.go index 0f2bc753..17d5f586 100644 --- a/api/layer/container.go +++ b/api/layer/container.go @@ -183,7 +183,12 @@ func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da func (n *layer) setContainerEACLTable(ctx context.Context, idCnr cid.ID, table *eacl.Table, sessionToken *session.Container) error { table.SetCID(idCnr) - return n.neoFS.SetContainerEACL(ctx, *table, sessionToken) + err := n.neoFS.SetContainerEACL(ctx, *table, sessionToken) + if err == nil { + n.cache.PutBucketACL(idCnr, table) + } + + return err } func (n *layer) GetContainerEACL(ctx context.Context, idCnr cid.ID) (*eacl.Table, error) { diff --git a/api/layer/layer.go b/api/layer/layer.go index e9136c39..851d494f 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -378,7 +378,7 @@ func (n *layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInf func (n *layer) GetBucketACL(ctx context.Context, bktInfo *data.BucketInfo) (*BucketACL, error) { var eACL = n.cache.GetBucketACL(bktInfo.CID) - if eACL == nil { + if eACL != nil { return &BucketACL{Info: bktInfo, EACL: eACL}, nil } eACL, err := n.GetContainerEACL(ctx, bktInfo.CID) diff --git a/api/router.go b/api/router.go index 4a788632..b173d11b 100644 --- a/api/router.go +++ b/api/router.go @@ -87,6 +87,9 @@ type ( GetObjectTorrentHandler(http.ResponseWriter, *http.Request) PutPublicAccessBlockHandler(http.ResponseWriter, *http.Request) GetPublicAccessBlockHandler(http.ResponseWriter, *http.Request) + PutBucketOwnershipControlsHandler(http.ResponseWriter, *http.Request) + GetBucketOwnershipControlsHandler(http.ResponseWriter, *http.Request) + DeleteBucketOwnershipControlsHandler(http.ResponseWriter, *http.Request) } // mimeType represents various MIME types used in API responses. @@ -447,6 +450,10 @@ func Attach(r *mux.Router, domains []string, m MaxClients, h Handler, center aut bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(metrics.APIStats("getpublicaccessblock", h.GetPublicAccessBlockHandler))).Queries("publicAccessBlock", ""). Name("GetPublicAccessBlock") + // GetBucketOwnershipControls + bucket.Methods(http.MethodGet).HandlerFunc( + m.Handle(metrics.APIStats("getbucketownershipcontrols", h.GetBucketOwnershipControlsHandler))).Queries("ownershipControls", ""). + Name("GetBucketOwnershipControls") // ListObjectsV1 (Legacy) bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(metrics.APIStats("listobjectsv1", h.ListObjectsV1Handler))). @@ -481,6 +488,14 @@ func Attach(r *mux.Router, domains []string, m MaxClients, h Handler, center aut bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(metrics.APIStats("putbucketnotification", h.PutBucketNotificationHandler))).Queries("notification", ""). Name("PutBucketNotification") + // PutBucketOwnershipControls + bucket.Methods(http.MethodPut).HandlerFunc( + m.Handle(metrics.APIStats("putbucketownershipcontrols", h.PutBucketOwnershipControlsHandler))).Queries("ownershipControls", ""). + Name("PutBucketOwnershipControls") + // DeleteBucketOwnershipControls + bucket.Methods(http.MethodDelete).HandlerFunc( + m.Handle(metrics.APIStats("deletebucketownershipcontrols", h.DeleteBucketOwnershipControlsHandler))).Queries("ownershipControls", ""). + Name("DeleteBucketOwnershipControls") // CreateBucket bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(metrics.APIStats("createbucket", h.CreateBucketHandler))). diff --git a/docs/aws_s3_compat.md b/docs/aws_s3_compat.md index f5c5eccb..d91fc55e 100644 --- a/docs/aws_s3_compat.md +++ b/docs/aws_s3_compat.md @@ -198,6 +198,9 @@ See also `GetObject` and other method parameters. | 🟡 | GetBucketAcl | See ACL limitations | | 🟡 | PutBucketAcl | See ACL Limitations | +Bucket ACLs are disabled, by default. See details [here](https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html). +See [Ownership](./aws_s3_compat.md#ownership-controls) section for details. + ## Analytics | | Method | Comments | @@ -272,9 +275,27 @@ See also `GetObject` and other method parameters. | | Method | Comments | |----|-------------------------------|----------| -| 🔵 | DeleteBucketOwnershipControls | | -| 🔵 | GetBucketOwnershipControls | | -| 🔵 | PutBucketOwnershipControls | | +| 🟢 | DeleteBucketOwnershipControls | | +| 🟢 | GetBucketOwnershipControls | | +| 🟢 | PutBucketOwnershipControls | | + +In case you need to disable ACLs manually (for instance your bucket has ACLs enabled) you should use `PutBucketOwnershipControls` command: +```shell +$ aws s3api put-bucket-ownership-controls --endpoint $S3HOST --bucket $BUCKET --ownership-controls "Rules=[{ObjectOwnership=BucketOwnerEnforced}]" +``` + +Switch to `Preferred` mode with the next command: +```shell +$ aws s3api put-bucket-ownership-controls --endpoint $S3HOST --bucket $BUCKET --ownership-controls "Rules=[{ObjectOwnership=BucketOwnerPreferred}]" +``` + +Switch to `ObjectWriter` mode with the next command: +```shell +$ aws s3api put-bucket-ownership-controls --endpoint $S3HOST --bucket $BUCKET --ownership-controls "Rules=[{ObjectOwnership=ObjectWriter}]" +``` + +Note: `ObjectWriter` mode means fully enabled ACL. +Pay attention to the fact that object owner in NeoFS is bucket owner in any case. ## Policy and replication @@ -290,41 +311,6 @@ 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 |