From 90033845adfb8e18beb7985db2c1a5ad853c7934 Mon Sep 17 00:00:00 2001 From: niksis02 Date: Thu, 27 Mar 2025 20:47:51 +0400 Subject: [PATCH] feat: Implements bucket cors actions in FE to return not implemented. Implements the bucket cors s3 actions in FE to return `NotImplemented` error. Actions implemented: - `PutBucketCors` - `GetBucketCors` - `DeleteBucketCors` `Note`: no logic is implemented for the actions in any backend and no input or output data validation is added. --- auth/bucket_policy_actions.go | 4 + backend/backend.go | 12 +++ metrics/actions.go | 15 +++ s3api/controllers/backend_moq_test.go | 150 ++++++++++++++++++++++++++ s3api/controllers/base.go | 94 ++++++++++++++++ s3api/middlewares/acl-parser.go | 3 +- 6 files changed, 277 insertions(+), 1 deletion(-) diff --git a/auth/bucket_policy_actions.go b/auth/bucket_policy_actions.go index 3e7d7d8c..bab769da 100644 --- a/auth/bucket_policy_actions.go +++ b/auth/bucket_policy_actions.go @@ -58,6 +58,8 @@ const ( BypassGovernanceRetentionAction Action = "s3:BypassGovernanceRetention" PutBucketOwnershipControlsAction Action = "s3:PutBucketOwnershipControls" GetBucketOwnershipControlsAction Action = "s3:GetBucketOwnershipControls" + PutBucketCorsAction Action = "s3:PutBucketCORS" + GetBucketCorsAction Action = "s3:GetBucketCORS" AllActions Action = "s3:*" ) @@ -97,6 +99,8 @@ var supportedActionList = map[Action]struct{}{ BypassGovernanceRetentionAction: {}, PutBucketOwnershipControlsAction: {}, GetBucketOwnershipControlsAction: {}, + PutBucketCorsAction: {}, + GetBucketCorsAction: {}, AllActions: {}, } diff --git a/backend/backend.go b/backend/backend.go index a26bedfc..1b0fecdb 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -46,6 +46,9 @@ type Backend interface { PutBucketOwnershipControls(_ context.Context, bucket string, ownership types.ObjectOwnership) error GetBucketOwnershipControls(_ context.Context, bucket string) (types.ObjectOwnership, error) DeleteBucketOwnershipControls(_ context.Context, bucket string) error + PutBucketCors(context.Context, []byte) error + GetBucketCors(_ context.Context, bucket string) ([]byte, error) + DeleteBucketCors(_ context.Context, bucket string) error // multipart operations CreateMultipartUpload(context.Context, s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) @@ -150,6 +153,15 @@ func (BackendUnsupported) GetBucketOwnershipControls(_ context.Context, bucket s func (BackendUnsupported) DeleteBucketOwnershipControls(_ context.Context, bucket string) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } +func (BackendUnsupported) PutBucketCors(context.Context, []byte) error { + return s3err.GetAPIError(s3err.ErrNotImplemented) +} +func (BackendUnsupported) GetBucketCors(_ context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrNotImplemented) +} +func (BackendUnsupported) DeleteBucketCors(_ context.Context, bucket string) error { + return s3err.GetAPIError(s3err.ErrNotImplemented) +} func (BackendUnsupported) CreateMultipartUpload(context.Context, s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) { return s3response.InitiateMultipartUploadResult{}, s3err.GetAPIError(s3err.ErrNotImplemented) diff --git a/metrics/actions.go b/metrics/actions.go index 4d46671a..683ca903 100644 --- a/metrics/actions.go +++ b/metrics/actions.go @@ -72,6 +72,9 @@ var ( ActionPutBucketOwnershipControls = "s3_PutBucketOwnershipControls" ActionGetBucketOwnershipControls = "s3_GetBucketOwnershipControls" ActionDeleteBucketOwnershipControls = "s3_DeleteBucketOwnershipControls" + ActionPutBucketCors = "s3_PutBucketCors" + ActionGetBucketCors = "s3_GetBucketCors" + ActionDeleteBucketCors = "s3_DeleteBucketCors" // Admin actions ActionAdminCreateUser = "admin_CreateUser" @@ -266,4 +269,16 @@ func init() { Name: "UploadPartCopy", Service: "s3", } + ActionMap[ActionPutBucketCors] = Action{ + Name: "PutBucketCors", + Service: "s3", + } + ActionMap[ActionGetBucketCors] = Action{ + Name: "GetBucketCors", + Service: "s3", + } + ActionMap[ActionDeleteBucketCors] = Action{ + Name: "DeleteBucketCors", + Service: "s3", + } } diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go index 0fc40d72..a8e3d279 100644 --- a/s3api/controllers/backend_moq_test.go +++ b/s3api/controllers/backend_moq_test.go @@ -44,6 +44,9 @@ var _ backend.Backend = &BackendMock{} // DeleteBucketFunc: func(contextMoqParam context.Context, bucket string) error { // panic("mock out the DeleteBucket method") // }, +// DeleteBucketCorsFunc: func(contextMoqParam context.Context, bucket string) error { +// panic("mock out the DeleteBucketCors method") +// }, // DeleteBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) error { // panic("mock out the DeleteBucketOwnershipControls method") // }, @@ -65,6 +68,9 @@ var _ backend.Backend = &BackendMock{} // GetBucketAclFunc: func(contextMoqParam context.Context, getBucketAclInput *s3.GetBucketAclInput) ([]byte, error) { // panic("mock out the GetBucketAcl method") // }, +// GetBucketCorsFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { +// panic("mock out the GetBucketCors method") +// }, // GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) { // panic("mock out the GetBucketOwnershipControls method") // }, @@ -128,6 +134,9 @@ var _ backend.Backend = &BackendMock{} // PutBucketAclFunc: func(contextMoqParam context.Context, bucket string, data []byte) error { // panic("mock out the PutBucketAcl method") // }, +// PutBucketCorsFunc: func(contextMoqParam context.Context, bytes []byte) error { +// panic("mock out the PutBucketCors method") +// }, // PutBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error { // panic("mock out the PutBucketOwnershipControls method") // }, @@ -204,6 +213,9 @@ type BackendMock struct { // DeleteBucketFunc mocks the DeleteBucket method. DeleteBucketFunc func(contextMoqParam context.Context, bucket string) error + // DeleteBucketCorsFunc mocks the DeleteBucketCors method. + DeleteBucketCorsFunc func(contextMoqParam context.Context, bucket string) error + // DeleteBucketOwnershipControlsFunc mocks the DeleteBucketOwnershipControls method. DeleteBucketOwnershipControlsFunc func(contextMoqParam context.Context, bucket string) error @@ -225,6 +237,9 @@ type BackendMock struct { // GetBucketAclFunc mocks the GetBucketAcl method. GetBucketAclFunc func(contextMoqParam context.Context, getBucketAclInput *s3.GetBucketAclInput) ([]byte, error) + // GetBucketCorsFunc mocks the GetBucketCors method. + GetBucketCorsFunc func(contextMoqParam context.Context, bucket string) ([]byte, error) + // GetBucketOwnershipControlsFunc mocks the GetBucketOwnershipControls method. GetBucketOwnershipControlsFunc func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) @@ -288,6 +303,9 @@ type BackendMock struct { // PutBucketAclFunc mocks the PutBucketAcl method. PutBucketAclFunc func(contextMoqParam context.Context, bucket string, data []byte) error + // PutBucketCorsFunc mocks the PutBucketCors method. + PutBucketCorsFunc func(contextMoqParam context.Context, bytes []byte) error + // PutBucketOwnershipControlsFunc mocks the PutBucketOwnershipControls method. PutBucketOwnershipControlsFunc func(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error @@ -391,6 +409,13 @@ type BackendMock struct { // Bucket is the bucket argument value. Bucket string } + // DeleteBucketCors holds details about calls to the DeleteBucketCors method. + DeleteBucketCors []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // Bucket is the bucket argument value. + Bucket string + } // DeleteBucketOwnershipControls holds details about calls to the DeleteBucketOwnershipControls method. DeleteBucketOwnershipControls []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -442,6 +467,13 @@ type BackendMock struct { // GetBucketAclInput is the getBucketAclInput argument value. GetBucketAclInput *s3.GetBucketAclInput } + // GetBucketCors holds details about calls to the GetBucketCors method. + GetBucketCors []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // Bucket is the bucket argument value. + Bucket string + } // GetBucketOwnershipControls holds details about calls to the GetBucketOwnershipControls method. GetBucketOwnershipControls []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -599,6 +631,13 @@ type BackendMock struct { // Data is the data argument value. Data []byte } + // PutBucketCors holds details about calls to the PutBucketCors method. + PutBucketCors []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // Bytes is the bytes argument value. + Bytes []byte + } // PutBucketOwnershipControls holds details about calls to the PutBucketOwnershipControls method. PutBucketOwnershipControls []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -739,6 +778,7 @@ type BackendMock struct { lockCreateBucket sync.RWMutex lockCreateMultipartUpload sync.RWMutex lockDeleteBucket sync.RWMutex + lockDeleteBucketCors sync.RWMutex lockDeleteBucketOwnershipControls sync.RWMutex lockDeleteBucketPolicy sync.RWMutex lockDeleteBucketTagging sync.RWMutex @@ -746,6 +786,7 @@ type BackendMock struct { lockDeleteObjectTagging sync.RWMutex lockDeleteObjects sync.RWMutex lockGetBucketAcl sync.RWMutex + lockGetBucketCors sync.RWMutex lockGetBucketOwnershipControls sync.RWMutex lockGetBucketPolicy sync.RWMutex lockGetBucketTagging sync.RWMutex @@ -767,6 +808,7 @@ type BackendMock struct { lockListObjectsV2 sync.RWMutex lockListParts sync.RWMutex lockPutBucketAcl sync.RWMutex + lockPutBucketCors sync.RWMutex lockPutBucketOwnershipControls sync.RWMutex lockPutBucketPolicy sync.RWMutex lockPutBucketTagging sync.RWMutex @@ -1045,6 +1087,42 @@ func (mock *BackendMock) DeleteBucketCalls() []struct { return calls } +// DeleteBucketCors calls DeleteBucketCorsFunc. +func (mock *BackendMock) DeleteBucketCors(contextMoqParam context.Context, bucket string) error { + if mock.DeleteBucketCorsFunc == nil { + panic("BackendMock.DeleteBucketCorsFunc: method is nil but Backend.DeleteBucketCors was just called") + } + callInfo := struct { + ContextMoqParam context.Context + Bucket string + }{ + ContextMoqParam: contextMoqParam, + Bucket: bucket, + } + mock.lockDeleteBucketCors.Lock() + mock.calls.DeleteBucketCors = append(mock.calls.DeleteBucketCors, callInfo) + mock.lockDeleteBucketCors.Unlock() + return mock.DeleteBucketCorsFunc(contextMoqParam, bucket) +} + +// DeleteBucketCorsCalls gets all the calls that were made to DeleteBucketCors. +// Check the length with: +// +// len(mockedBackend.DeleteBucketCorsCalls()) +func (mock *BackendMock) DeleteBucketCorsCalls() []struct { + ContextMoqParam context.Context + Bucket string +} { + var calls []struct { + ContextMoqParam context.Context + Bucket string + } + mock.lockDeleteBucketCors.RLock() + calls = mock.calls.DeleteBucketCors + mock.lockDeleteBucketCors.RUnlock() + return calls +} + // DeleteBucketOwnershipControls calls DeleteBucketOwnershipControlsFunc. func (mock *BackendMock) DeleteBucketOwnershipControls(contextMoqParam context.Context, bucket string) error { if mock.DeleteBucketOwnershipControlsFunc == nil { @@ -1301,6 +1379,42 @@ func (mock *BackendMock) GetBucketAclCalls() []struct { return calls } +// GetBucketCors calls GetBucketCorsFunc. +func (mock *BackendMock) GetBucketCors(contextMoqParam context.Context, bucket string) ([]byte, error) { + if mock.GetBucketCorsFunc == nil { + panic("BackendMock.GetBucketCorsFunc: method is nil but Backend.GetBucketCors was just called") + } + callInfo := struct { + ContextMoqParam context.Context + Bucket string + }{ + ContextMoqParam: contextMoqParam, + Bucket: bucket, + } + mock.lockGetBucketCors.Lock() + mock.calls.GetBucketCors = append(mock.calls.GetBucketCors, callInfo) + mock.lockGetBucketCors.Unlock() + return mock.GetBucketCorsFunc(contextMoqParam, bucket) +} + +// GetBucketCorsCalls gets all the calls that were made to GetBucketCors. +// Check the length with: +// +// len(mockedBackend.GetBucketCorsCalls()) +func (mock *BackendMock) GetBucketCorsCalls() []struct { + ContextMoqParam context.Context + Bucket string +} { + var calls []struct { + ContextMoqParam context.Context + Bucket string + } + mock.lockGetBucketCors.RLock() + calls = mock.calls.GetBucketCors + mock.lockGetBucketCors.RUnlock() + return calls +} + // GetBucketOwnershipControls calls GetBucketOwnershipControlsFunc. func (mock *BackendMock) GetBucketOwnershipControls(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) { if mock.GetBucketOwnershipControlsFunc == nil { @@ -2077,6 +2191,42 @@ func (mock *BackendMock) PutBucketAclCalls() []struct { return calls } +// PutBucketCors calls PutBucketCorsFunc. +func (mock *BackendMock) PutBucketCors(contextMoqParam context.Context, bytes []byte) error { + if mock.PutBucketCorsFunc == nil { + panic("BackendMock.PutBucketCorsFunc: method is nil but Backend.PutBucketCors was just called") + } + callInfo := struct { + ContextMoqParam context.Context + Bytes []byte + }{ + ContextMoqParam: contextMoqParam, + Bytes: bytes, + } + mock.lockPutBucketCors.Lock() + mock.calls.PutBucketCors = append(mock.calls.PutBucketCors, callInfo) + mock.lockPutBucketCors.Unlock() + return mock.PutBucketCorsFunc(contextMoqParam, bytes) +} + +// PutBucketCorsCalls gets all the calls that were made to PutBucketCors. +// Check the length with: +// +// len(mockedBackend.PutBucketCorsCalls()) +func (mock *BackendMock) PutBucketCorsCalls() []struct { + ContextMoqParam context.Context + Bytes []byte +} { + var calls []struct { + ContextMoqParam context.Context + Bytes []byte + } + mock.lockPutBucketCors.RLock() + calls = mock.calls.PutBucketCors + mock.lockPutBucketCors.RUnlock() + return calls +} + // PutBucketOwnershipControls calls PutBucketOwnershipControlsFunc. func (mock *BackendMock) PutBucketOwnershipControls(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error { if mock.PutBucketOwnershipControlsFunc == nil { diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index bb4e9c12..0d3cab94 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -871,6 +871,36 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("cors") { + err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketCorsAction, + }) + if err != nil { + return SendXMLResponse(ctx, nil, err, + &MetaOpts{ + Logger: c.logger, + MetricsMng: c.mm, + Action: metrics.ActionGetBucketCors, + BucketOwner: parsedAcl.Owner, + }) + } + + data, err := c.be.GetBucketCors(ctx.Context(), bucket) + return SendXMLResponse(ctx, data, err, + &MetaOpts{ + Logger: c.logger, + MetricsMng: c.mm, + Action: metrics.ActionGetBucketCors, + BucketOwner: parsedAcl.Owner, + }) + } + if ctx.Request().URI().QueryArgs().Has("versions") { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Readonly: c.readonly, @@ -1391,6 +1421,39 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("cors") { + parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) + err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketCorsAction, + }) + if err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + MetricsMng: c.mm, + Action: metrics.ActionPutBucketCors, + BucketOwner: parsedAcl.Owner, + }) + } + + err = c.be.PutBucketCors(ctx.Context(), []byte{}) + if err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + MetricsMng: c.mm, + Action: metrics.ActionPutBucketCors, + BucketOwner: parsedAcl.Owner, + }, + ) + } + } if ctx.Request().URI().QueryArgs().Has("policy") { parsedAcl := ctx.Locals("parsedAcl").(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ @@ -2777,6 +2840,37 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("cors") { + err := auth.VerifyAccess(ctx.Context(), c.be, + auth.AccessOptions{ + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketCorsAction, + }) + if err != nil { + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + MetricsMng: c.mm, + Action: metrics.ActionDeleteBucketCors, + BucketOwner: parsedAcl.Owner, + }) + } + + err = c.be.DeleteBucketCors(ctx.Context(), bucket) + return SendResponse(ctx, err, + &MetaOpts{ + Logger: c.logger, + MetricsMng: c.mm, + Action: metrics.ActionDeleteBucketCors, + BucketOwner: parsedAcl.Owner, + }) + } + err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Readonly: c.readonly, diff --git a/s3api/middlewares/acl-parser.go b/s3api/middlewares/acl-parser.go index cdc1bdaa..61633c3f 100644 --- a/s3api/middlewares/acl-parser.go +++ b/s3api/middlewares/acl-parser.go @@ -51,7 +51,8 @@ func AclParser(be backend.Backend, logger s3log.AuditLogger, readonly bool) fibe !ctx.Request().URI().QueryArgs().Has("versioning") && !ctx.Request().URI().QueryArgs().Has("policy") && !ctx.Request().URI().QueryArgs().Has("object-lock") && - !ctx.Request().URI().QueryArgs().Has("ownershipControls") { + !ctx.Request().URI().QueryArgs().Has("ownershipControls") && + !ctx.Request().URI().QueryArgs().Has("cors") { if err := auth.MayCreateBucket(acct, isRoot); err != nil { return controllers.SendXMLResponse(ctx, nil, err, &controllers.MetaOpts{Logger: logger, Action: "CreateBucket"}) }