From 1f5f2bd3d5080f297b70a547029a8b27c1067f2f Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Tue, 19 Mar 2024 16:56:32 +0300 Subject: [PATCH] [#306] In APE buckets forbid canned acl except private Signed-off-by: Denis Kirillov --- api/handler/acl.go | 68 ++++++++++---------- api/handler/acl_test.go | 108 ++++++++++++++++++++++++++------ api/handler/copy.go | 13 +++- api/handler/copy_test.go | 5 ++ api/handler/encryption_test.go | 24 ++++--- api/handler/multipart_upload.go | 19 +++++- api/handler/put.go | 100 ++++++++++++++++++++--------- api/layer/frostfs_mock.go | 62 +++++++++++++----- internal/logs/logs.go | 1 - 9 files changed, 290 insertions(+), 110 deletions(-) diff --git a/api/handler/acl.go b/api/handler/acl.go index 876f460..ae8877a 100644 --- a/api/handler/acl.go +++ b/api/handler/acl.go @@ -284,6 +284,32 @@ func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) { } func (h *handler) encodeBucketCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy { + res := h.encodePrivateCannedACL(ctx, bktInfo, settings) + + switch settings.CannedACL { + case basicACLPublic: + grantee := NewGrantee(acpGroup) + grantee.URI = allUsersGroup + + res.AccessControlList = append(res.AccessControlList, &Grant{ + Grantee: grantee, + Permission: aclWrite, + }) + fallthrough + case basicACLReadOnly: + grantee := NewGrantee(acpGroup) + grantee.URI = allUsersGroup + + res.AccessControlList = append(res.AccessControlList, &Grant{ + Grantee: grantee, + Permission: aclRead, + }) + } + + return res +} + +func (h *handler) encodePrivateCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy { ownerDisplayName := bktInfo.Owner.EncodeToString() ownerEncodedID := ownerDisplayName @@ -308,26 +334,6 @@ func (h *handler) encodeBucketCannedACL(ctx context.Context, bktInfo *data.Bucke Permission: aclFullControl, }} - switch settings.CannedACL { - case basicACLPublic: - grantee := NewGrantee(acpGroup) - grantee.URI = allUsersGroup - - res.AccessControlList = append(res.AccessControlList, &Grant{ - Grantee: grantee, - Permission: aclWrite, - }) - fallthrough - case basicACLReadOnly: - grantee := NewGrantee(acpGroup) - grantee.URI = allUsersGroup - - res.AccessControlList = append(res.AccessControlList, &Grant{ - Grantee: grantee, - Permission: aclRead, - }) - } - return res } @@ -513,19 +519,17 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) { return } - apeEnabled := bktInfo.APEEnabled - - if !apeEnabled { - settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) - if err != nil { - h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) - return - } - apeEnabled = len(settings.CannedACL) != 0 + settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) + return } - if apeEnabled { - h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) + if bktInfo.APEEnabled || len(settings.CannedACL) != 0 { + if err = middleware.EncodeToResponse(w, h.encodePrivateCannedACL(ctx, bktInfo, settings)); err != nil { + h.logAndSendError(w, "something went wrong", reqInfo, err) + return + } return } @@ -543,7 +547,7 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) { objInfo, err := h.obj.GetObjectInfo(ctx, prm) if err != nil { - h.logAndSendError(w, "could not object info", reqInfo, err) + h.logAndSendError(w, "could not get object info", reqInfo, err) return } diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index 55abb4d..7d02c33 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -1329,55 +1329,96 @@ func TestPutBucketAPE(t *testing.T) { require.Len(t, chains, 2) } -func TestPutBucketObjectACLErrorAPE(t *testing.T) { +func TestPutObjectACLErrorAPE(t *testing.T) { hc := prepareHandlerContext(t) bktName, objName := "bucket-for-acl-ape", "object" info := createBucket(hc, bktName) - putObject(hc, bktName, objName) + + putObjectWithHeadersAssertS3Error(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, s3errors.ErrAccessControlListNotSupported) + putObjectWithHeaders(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate}) // only `private` canned acl is allowed, that is actually ignored + putObjectWithHeaders(hc, bktName, objName, nil) aclBody := &AccessControlPolicy{} - putBucketACLAssertS3Error(hc, bktName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported) - putObjectACLAssertS3Error(hc, bktName, objName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported) - getObjectACLAssertS3Error(hc, bktName, objName, s3errors.ErrAccessControlListNotSupported) + + aclRes := getObjectACL(hc, bktName, objName) + checkPrivateACL(t, aclRes, info.Key.PublicKey()) } -func TestGetBucketACLAPE(t *testing.T) { +func TestCreateObjectACLErrorAPE(t *testing.T) { + hc := prepareHandlerContext(t) + bktName, objName, objNameCopy := "bucket-for-acl-ape", "object", "copy" + + createBucket(hc, bktName) + + putObject(hc, bktName, objName) + copyObject(hc, bktName, objName, objNameCopy, CopyMeta{Headers: map[string]string{api.AmzACL: basicACLPublic}}, http.StatusBadRequest) + copyObject(hc, bktName, objName, objNameCopy, CopyMeta{Headers: map[string]string{api.AmzACL: basicACLPrivate}}, http.StatusOK) + + createMultipartUploadAssertS3Error(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, s3errors.ErrAccessControlListNotSupported) + createMultipartUpload(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate}) +} + +func TestPutObjectACLBackwardCompatibility(t *testing.T) { + hc := prepareHandlerContext(t) + hc.config.aclEnabled = true + bktName, objName := "bucket-for-acl-ape", "object" + + info := createBucket(hc, bktName) + + putObjectWithHeadersBase(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate}, info.Box, nil) + putObjectWithHeadersBase(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, info.Box, nil) + + aclRes := getObjectACL(hc, bktName, objName) + require.Len(t, aclRes.AccessControlList, 2) + require.Equal(t, hex.EncodeToString(info.Key.PublicKey().Bytes()), aclRes.AccessControlList[0].Grantee.ID) + require.Equal(t, aclFullControl, aclRes.AccessControlList[0].Permission) + require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI) + require.Equal(t, aclFullControl, aclRes.AccessControlList[1].Permission) + + aclBody := &AccessControlPolicy{} + putObjectACLBase(hc, bktName, objName, info.Box, nil, aclBody) +} + +func TestBucketACLAPE(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-for-acl-ape" info := createBucket(hc, bktName) + aclBody := &AccessControlPolicy{} + putBucketACLAssertS3Error(hc, bktName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported) + aclRes := getBucketACL(hc, bktName) - checkPrivateBucketACL(t, aclRes, info.Key.PublicKey()) + checkPrivateACL(t, aclRes, info.Key.PublicKey()) putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLPrivate}) aclRes = getBucketACL(hc, bktName) - checkPrivateBucketACL(t, aclRes, info.Key.PublicKey()) + checkPrivateACL(t, aclRes, info.Key.PublicKey()) putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLReadOnly}) aclRes = getBucketACL(hc, bktName) - checkPublicReadBucketACL(t, aclRes, info.Key.PublicKey()) + checkPublicReadACL(t, aclRes, info.Key.PublicKey()) putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLPublic}) aclRes = getBucketACL(hc, bktName) - checkPublicReadWriteBucketACL(t, aclRes, info.Key.PublicKey()) + checkPublicReadWriteACL(t, aclRes, info.Key.PublicKey()) } -func checkPrivateBucketACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) { - checkBucketACLOwner(t, aclRes, ownerKey, 1) +func checkPrivateACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) { + checkACLOwner(t, aclRes, ownerKey, 1) } -func checkPublicReadBucketACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) { - checkBucketACLOwner(t, aclRes, ownerKey, 2) +func checkPublicReadACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) { + checkACLOwner(t, aclRes, ownerKey, 2) require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI) require.Equal(t, aclRead, aclRes.AccessControlList[1].Permission) } -func checkPublicReadWriteBucketACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) { - checkBucketACLOwner(t, aclRes, ownerKey, 3) +func checkPublicReadWriteACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) { + checkACLOwner(t, aclRes, ownerKey, 3) require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI) require.Equal(t, aclWrite, aclRes.AccessControlList[1].Permission) @@ -1386,7 +1427,7 @@ func checkPublicReadWriteBucketACL(t *testing.T, aclRes *AccessControlPolicy, ow require.Equal(t, aclRead, aclRes.AccessControlList[2].Permission) } -func checkBucketACLOwner(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey, ln int) { +func checkACLOwner(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey, ln int) { ownerIDStr := hex.EncodeToString(ownerKey.Bytes()) ownerNameStr := ownerKey.Address() @@ -1661,9 +1702,12 @@ func putObjectACLBase(hc *handlerContext, bktName, objName string, box *accessbo return w } -func getObjectACLAssertS3Error(hc *handlerContext, bktName, objName string, code s3errors.ErrorCode) { +func getObjectACL(hc *handlerContext, bktName, objName string) *AccessControlPolicy { w := getObjectACLBase(hc, bktName, objName) - assertS3Error(hc.t, w, s3errors.GetAPIError(code)) + assertStatus(hc.t, w, http.StatusOK) + res := &AccessControlPolicy{} + parseTestResponse(hc.t, w, res) + return res } func getObjectACLBase(hc *handlerContext, bktName, objName string) *httptest.ResponseRecorder { @@ -1671,3 +1715,29 @@ func getObjectACLBase(hc *handlerContext, bktName, objName string) *httptest.Res hc.Handler().GetObjectACLHandler(w, r) return w } + +func putObjectWithHeaders(hc *handlerContext, bktName, objName string, headers map[string]string) http.Header { + w := putObjectWithHeadersBase(hc, bktName, objName, headers, nil, nil) + assertStatus(hc.t, w, http.StatusOK) + return w.Header() +} + +func putObjectWithHeadersAssertS3Error(hc *handlerContext, bktName, objName string, headers map[string]string, code s3errors.ErrorCode) { + w := putObjectWithHeadersBase(hc, bktName, objName, headers, nil, nil) + assertS3Error(hc.t, w, s3errors.GetAPIError(code)) +} + +func putObjectWithHeadersBase(hc *handlerContext, bktName, objName string, headers map[string]string, box *accessbox.Box, data []byte) *httptest.ResponseRecorder { + body := bytes.NewReader(data) + w, r := prepareTestPayloadRequest(hc, bktName, objName, body) + + for k, v := range headers { + r.Header.Set(k, v) + } + + ctx := middleware.SetBoxData(r.Context(), box) + r = r.WithContext(ctx) + + hc.Handler().PutObjectHandler(w, r) + return w +} diff --git a/api/handler/copy.go b/api/handler/copy.go index 2083b31..ef35ef8 100644 --- a/api/handler/copy.go +++ b/api/handler/copy.go @@ -51,7 +51,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { ctx = r.Context() reqInfo = middleware.GetReqInfo(ctx) - containsACL = containsACLHeaders(r) + cannedACLStatus = aclHeadersStatus(r) ) src := r.Header.Get(api.AmzCopySource) @@ -93,7 +93,14 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if containsACL { + apeEnabled := dstBktInfo.APEEnabled || settings.CannedACL != "" + if apeEnabled && cannedACLStatus == aclStatusYes { + h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) + return + } + + needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo) + if needUpdateEACLTable { if sessionTokenEACL, err = getSessionTokenSetEACL(ctx); err != nil { h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err) return @@ -232,7 +239,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if containsACL { + if needUpdateEACLTable { newEaclTable, err := h.getNewEAclTable(r, dstBktInfo, dstObjInfo) if err != nil { h.logAndSendError(w, "could not get new eacl table", reqInfo, err) diff --git a/api/handler/copy_test.go b/api/handler/copy_test.go index 9ee0783..d6bdff2 100644 --- a/api/handler/copy_test.go +++ b/api/handler/copy_test.go @@ -22,6 +22,7 @@ type CopyMeta struct { Tags map[string]string MetadataDirective string Metadata map[string]string + Headers map[string]string } func TestCopyWithTaggingDirective(t *testing.T) { @@ -279,6 +280,10 @@ func copyObject(hc *handlerContext, bktName, fromObject, toObject string, copyMe } r.Header.Set(api.AmzTagging, tagsQuery.Encode()) + for key, val := range copyMeta.Headers { + r.Header.Set(key, val) + } + hc.Handler().CopyObjectHandler(w, r) assertStatus(hc.t, w, statusCode) } diff --git a/api/handler/encryption_test.go b/api/handler/encryption_test.go index 5d3d4f5..899ce47 100644 --- a/api/handler/encryption_test.go +++ b/api/handler/encryption_test.go @@ -14,6 +14,7 @@ import ( "testing" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "github.com/stretchr/testify/require" ) @@ -234,24 +235,33 @@ func multipartUpload(hc *handlerContext, bktName, objName string, headers map[st } func createMultipartUploadEncrypted(hc *handlerContext, bktName, objName string, headers map[string]string) *InitiateMultipartUploadResponse { - return createMultipartUploadBase(hc, bktName, objName, true, headers) + return createMultipartUploadOkBase(hc, bktName, objName, true, headers) } func createMultipartUpload(hc *handlerContext, bktName, objName string, headers map[string]string) *InitiateMultipartUploadResponse { - return createMultipartUploadBase(hc, bktName, objName, false, headers) + return createMultipartUploadOkBase(hc, bktName, objName, false, headers) } -func createMultipartUploadBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *InitiateMultipartUploadResponse { +func createMultipartUploadOkBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *InitiateMultipartUploadResponse { + w := createMultipartUploadBase(hc, bktName, objName, encrypted, headers) + multipartInitInfo := &InitiateMultipartUploadResponse{} + readResponse(hc.t, w, http.StatusOK, multipartInitInfo) + return multipartInitInfo +} + +func createMultipartUploadAssertS3Error(hc *handlerContext, bktName, objName string, headers map[string]string, code errors.ErrorCode) { + w := createMultipartUploadBase(hc, bktName, objName, false, headers) + assertS3Error(hc.t, w, errors.GetAPIError(code)) +} + +func createMultipartUploadBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *httptest.ResponseRecorder { w, r := prepareTestRequest(hc, bktName, objName, nil) if encrypted { setEncryptHeaders(r) } setHeaders(r, headers) hc.Handler().CreateMultipartUploadHandler(w, r) - multipartInitInfo := &InitiateMultipartUploadResponse{} - readResponse(hc.t, w, http.StatusOK, multipartInitInfo) - - return multipartInitInfo + return w } func completeMultipartUpload(hc *handlerContext, bktName, objName, uploadID string, partsETags []string) { diff --git a/api/handler/multipart_upload.go b/api/handler/multipart_upload.go index 1a062e5..fd1cc8a 100644 --- a/api/handler/multipart_upload.go +++ b/api/handler/multipart_upload.go @@ -103,6 +103,9 @@ const ( func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Request) { reqInfo := middleware.GetReqInfo(r.Context()) + uploadID := uuid.New() + cannedACLStatus := aclHeadersStatus(r) + additional := []zap.Field{zap.String("uploadID", uploadID.String())} bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) if err != nil { @@ -110,8 +113,17 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re return } - uploadID := uuid.New() - additional := []zap.Field{zap.String("uploadID", uploadID.String())} + settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) + return + } + + apeEnabled := bktInfo.APEEnabled || settings.CannedACL != "" + if apeEnabled && cannedACLStatus == aclStatusYes { + h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) + return + } p := &layer.CreateMultipartParams{ Info: &layer.UploadInfoParams{ @@ -122,7 +134,8 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re Data: &layer.UploadData{}, } - if containsACLHeaders(r) { + needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo) + if needUpdateEACLTable { key, err := h.bearerTokenIssuerKey(r.Context()) if err != nil { h.logAndSendError(w, "couldn't get gate key", reqInfo, err, additional...) diff --git a/api/handler/put.go b/api/handler/put.go index 525da9d..30197b7 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -186,12 +186,31 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { err error newEaclTable *eacl.Table sessionTokenEACL *session.Container - containsACL = containsACLHeaders(r) + cannedACLStatus = aclHeadersStatus(r) ctx = r.Context() reqInfo = middleware.GetReqInfo(ctx) ) - if containsACL { + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err) + return + } + + settings, err := h.obj.GetBucketSettings(ctx, bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket settings", reqInfo, err) + return + } + + apeEnabled := bktInfo.APEEnabled || settings.CannedACL != "" + if apeEnabled && cannedACLStatus == aclStatusYes { + h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) + return + } + + needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo) + if needUpdateEACLTable { if sessionTokenEACL, err = getSessionTokenSetEACL(r.Context()); err != nil { h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err) return @@ -204,12 +223,6 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { return } - bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) - if err != nil { - h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err) - return - } - metadata := parseMetadata(r) if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 { metadata[api.ContentType] = contentType @@ -261,12 +274,6 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { return } - settings, err := h.obj.GetBucketSettings(ctx, bktInfo) - if err != nil { - h.logAndSendError(w, "could not get bucket settings", reqInfo, err) - return - } - params.Lock, err = formObjectLock(ctx, bktInfo, settings.LockConfiguration, r.Header) if err != nil { h.logAndSendError(w, "could not form object lock", reqInfo, err) @@ -292,7 +299,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err)) } - if containsACL { + if needUpdateEACLTable { if newEaclTable, err = h.getNewEAclTable(r, bktInfo, objInfo); err != nil { h.logAndSendError(w, "could not get new eacl table", reqInfo, err) return @@ -465,7 +472,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { ctx = r.Context() reqInfo = middleware.GetReqInfo(ctx) metadata = make(map[string]string) - containsACL = containsACLHeaders(r) + cannedACLStatus = aclHeadersStatus(r) ) policy, err := checkPostPolicy(r, reqInfo, metadata) @@ -483,7 +490,26 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { } } - if containsACL { + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err) + return + } + + settings, err := h.obj.GetBucketSettings(ctx, bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket settings", reqInfo, err) + return + } + + apeEnabled := bktInfo.APEEnabled || settings.CannedACL != "" + if apeEnabled && cannedACLStatus == aclStatusYes { + h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) + return + } + + needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo) + if needUpdateEACLTable { if sessionTokenEACL, err = getSessionTokenSetEACL(ctx); err != nil { h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err) return @@ -510,12 +536,6 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { return } - bktInfo, err := h.obj.GetBucketInfo(ctx, reqInfo.BucketName) - if err != nil { - h.logAndSendError(w, "could not get bucket info", reqInfo, err) - return - } - params := &layer.PutObjectParams{ BktInfo: bktInfo, Object: reqInfo.ObjectName, @@ -582,9 +602,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { } } - if settings, err := h.obj.GetBucketSettings(ctx, bktInfo); err != nil { - h.reqLogger(ctx).Warn(logs.CouldntGetBucketVersioning, zap.String("bucket name", reqInfo.BucketName), zap.Error(err)) - } else if settings.VersioningEnabled() { + if settings.VersioningEnabled() { w.Header().Set(api.AmzVersionID, objInfo.VersionID()) } @@ -680,9 +698,33 @@ func checkPostPolicy(r *http.Request, reqInfo *middleware.ReqInfo, metadata map[ return policy, nil } -func containsACLHeaders(r *http.Request) bool { - return r.Header.Get(api.AmzACL) != "" || r.Header.Get(api.AmzGrantRead) != "" || - r.Header.Get(api.AmzGrantFullControl) != "" || r.Header.Get(api.AmzGrantWrite) != "" +type aclStatus int + +const ( + // aclStatusNo means no acl headers at all. + aclStatusNo aclStatus = iota + // aclStatusYesAPECompatible means that only X-Amz-Acl present and equals to private. + aclStatusYesAPECompatible + // aclStatusYes means any other acl headers configuration. + aclStatusYes +) + +func aclHeadersStatus(r *http.Request) aclStatus { + if r.Header.Get(api.AmzGrantRead) != "" || + r.Header.Get(api.AmzGrantFullControl) != "" || + r.Header.Get(api.AmzGrantWrite) != "" { + return aclStatusYes + } + + cannedACL := r.Header.Get(api.AmzACL) + if cannedACL != "" { + if cannedACL == basicACLPrivate { + return aclStatusYesAPECompatible + } + return aclStatusYes + } + + return aclStatusNo } func (h *handler) getNewEAclTable(r *http.Request, bktInfo *data.BucketInfo, objInfo *data.ObjectInfo) (*eacl.Table, error) { diff --git a/api/layer/frostfs_mock.go b/api/layer/frostfs_mock.go index 786e14b..d30d304 100644 --- a/api/layer/frostfs_mock.go +++ b/api/layer/frostfs_mock.go @@ -10,6 +10,7 @@ import ( "io" "time" + "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container" objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" @@ -220,7 +221,7 @@ func (t *TestFrostFS) ReadObject(ctx context.Context, prm PrmObjectRead) (*Objec if obj, ok := t.objects[sAddr]; ok { owner := getBearerOwner(ctx) - if !t.checkAccess(prm.Container, owner, eacl.OperationGet) { + if !t.checkAccess(prm.Container, owner, eacl.OperationGet, obj) { return nil, ErrAccessDenied } @@ -322,9 +323,9 @@ func (t *TestFrostFS) DeleteObject(ctx context.Context, prm PrmObjectDelete) err return err } - if _, ok := t.objects[addr.EncodeToString()]; ok { + if obj, ok := t.objects[addr.EncodeToString()]; ok { owner := getBearerOwner(ctx) - if !t.checkAccess(prm.Container, owner, eacl.OperationDelete) { + if !t.checkAccess(prm.Container, owner, eacl.OperationDelete, obj) { return ErrAccessDenied } @@ -376,7 +377,7 @@ func (t *TestFrostFS) ContainerEACL(_ context.Context, prm PrmContainerEACL) (*e return table, nil } -func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID, op eacl.Operation) bool { +func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID, op eacl.Operation, obj *object.Object) bool { cnr, ok := t.containers[cnrID.EncodeToString()] if !ok { return false @@ -392,22 +393,51 @@ func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID, op eacl.Operation } for _, rec := range table.Records() { - if rec.Operation() == op && len(rec.Filters()) == 0 { - for _, trgt := range rec.Targets() { - if trgt.Role() == eacl.RoleOthers { - return rec.Action() == eacl.ActionAllow - } - var targetOwner user.ID - for _, pk := range eacl.TargetECDSAKeys(&trgt) { - user.IDFromKey(&targetOwner, *pk) - if targetOwner.Equals(owner) { - return rec.Action() == eacl.ActionAllow - } - } + if rec.Operation() != op { + continue + } + + if !matchTarget(rec, owner) { + continue + } + + if matchFilter(rec.Filters(), obj) { + return rec.Action() == eacl.ActionAllow + } + } + + return true +} + +func matchTarget(rec eacl.Record, owner user.ID) bool { + for _, trgt := range rec.Targets() { + if trgt.Role() == eacl.RoleOthers { + return true + } + var targetOwner user.ID + for _, pk := range eacl.TargetECDSAKeys(&trgt) { + user.IDFromKey(&targetOwner, *pk) + if targetOwner.Equals(owner) { + return true } } } + return false +} + +func matchFilter(filters []eacl.Filter, obj *object.Object) bool { + objID, _ := obj.ID() + for _, f := range filters { + fv2 := f.ToV2() + if fv2.GetMatchType() != acl.MatchTypeStringEqual || + fv2.GetHeaderType() != acl.HeaderTypeObject || + fv2.GetKey() != acl.FilterObjectID || + fv2.GetValue() != objID.EncodeToString() { + return false + } + } + return true } diff --git a/internal/logs/logs.go b/internal/logs/logs.go index fd3207e..3bed892 100644 --- a/internal/logs/logs.go +++ b/internal/logs/logs.go @@ -66,7 +66,6 @@ const ( SomeACLNotFullyMapped = "some acl not fully mapped" // Warn in ../../api/handler/acl.go CouldntDeleteObject = "couldn't delete object" // Error in ../../api/layer/layer.go NotificatorIsDisabledS3WontProduceNotificationEvents = "notificator is disabled, s3 won't produce notification events" // Warn in ../../api/handler/api.go - CouldntGetBucketVersioning = "couldn't get bucket versioning" // Warn in ../../api/handler/put.go BucketIsCreated = "bucket is created" // Info in ../../api/handler/put.go CouldntDeleteNotificationConfigurationObject = "couldn't delete notification configuration object" // Error in ../../api/layer/notifications.go CouldNotParseContainerObjectLockEnabledAttribute = "could not parse container object lock enabled attribute" // Error in ../../api/layer/container.go