diff --git a/backend/auth/acl.go b/backend/auth/acl.go index 8140002..168e3cd 100644 --- a/backend/auth/acl.go +++ b/backend/auth/acl.go @@ -14,7 +14,15 @@ package auth -import "github.com/aws/aws-sdk-go-v2/service/s3/types" +import ( + "encoding/json" + "fmt" + "os" + + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/pkg/xattr" + "github.com/versity/versitygw/s3err" +) type ACL struct { ACL types.BucketCannedACL @@ -35,3 +43,88 @@ type GetBucketAclOutput struct { type AccessControlList struct { Grants []types.Grant } + +type ACLService interface { + VerifyACL(bucket, access string, permission types.Permission, isRoot bool) error + IsAdmin(access string, isRoot bool) error +} + +type ACLServiceUnsupported struct{} + +var _ ACLService = &ACLServiceUnsupported{} + +func (ACLServiceUnsupported) VerifyACL(bucket, access string, permission types.Permission, isRoot bool) error { + var ACL ACL + + if isRoot { + return nil + } + + acl, err := xattr.Get(bucket, "user.acl") + if err != nil { + return fmt.Errorf("get acl: %w", err) + } + + if err := json.Unmarshal(acl, &ACL); err != nil { + return fmt.Errorf("parse acl: %w", err) + } + if ACL.Owner == access { + return nil + } + + if ACL.ACL != "" { + if (permission == "READ" || permission == "READ_ACP") && (ACL.ACL != "public-read" && ACL.ACL != "public-read-write") { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + if (permission == "WRITE" || permission == "WRITE_ACP") && ACL.ACL != "public-read-write" { + return s3err.GetAPIError(s3err.ErrAccessDenied) + } + + return nil + } else { + grantee := Grantee{Access: access, Permission: permission} + granteeFullCtrl := Grantee{Access: access, Permission: "FULL_CONTROL"} + + isFound := false + + for _, grt := range ACL.Grantees { + if grt == grantee || grt == granteeFullCtrl { + isFound = true + break + } + } + + if isFound { + return nil + } + } + + return s3err.GetAPIError(s3err.ErrAccessDenied) +} + +func (ACLServiceUnsupported) IsAdmin(access string, isRoot bool) error { + var data IAMConfig + + if isRoot { + return nil + } + + file, err := os.ReadFile("users.json") + if err != nil { + return fmt.Errorf("unable to read config file: %w", err) + } + + if err := json.Unmarshal(file, &data); err != nil { + return err + } + + acc, ok := data.AccessAccounts[access] + if !ok { + return fmt.Errorf("user does not exist") + } + + if acc.Role == "admin" { + return nil + } + return fmt.Errorf("only admin users have access to this resource") +} diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 79690f9..4231c55 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -1114,66 +1114,60 @@ func (p *Posix) PutBucketAcl(input *s3.PutBucketAclInput) error { return s3err.GetAPIError(s3err.ErrAccessDenied) } - grantees := []auth.Grantee{} - - fullControlList, readList, readACPList, writeList, writeACPList := []string{}, []string{}, []string{}, []string{}, []string{} - - if *input.GrantFullControl != "" { - fullControlList = strings.Split(*input.GrantFullControl, ",") - for _, str := range fullControlList { - grantees = append(grantees, auth.Grantee{Access: str, Permission: "FULL_CONTROL"}) - } - } - if *input.GrantRead != "" { - readList = strings.Split(*input.GrantRead, ",") - for _, str := range readList { - grantees = append(grantees, auth.Grantee{Access: str, Permission: "READ"}) - } - } - if *input.GrantReadACP != "" { - readACPList = strings.Split(*input.GrantReadACP, ",") - for _, str := range readACPList { - grantees = append(grantees, auth.Grantee{Access: str, Permission: "READ_ACP"}) - } - } - if *input.GrantWrite != "" { - writeList = strings.Split(*input.GrantWrite, ",") - for _, str := range writeList { - grantees = append(grantees, auth.Grantee{Access: str, Permission: "WRITE"}) - } - } - if *input.GrantWriteACP != "" { - writeACPList = strings.Split(*input.GrantWriteACP, ",") - for _, str := range writeACPList { - grantees = append(grantees, auth.Grantee{Access: str, Permission: "WRITE_ACP"}) - } - } - - accs := append(append(append(append(fullControlList, readList...), writeACPList...), readACPList...), writeList...) - - accList, err := checkIfAccountsExist(accs) - if err != nil { - return err - } - if len(accList) > 0 { - return fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", ")) - } - - for _, elem := range grantees { - doesContain := false - for _, grantee := range ACL.Grantees { - if elem == grantee { - doesContain = true - break - } - } - if !doesContain { - ACL.Grantees = append(ACL.Grantees, elem) - } - } - + // if the ACL is specified, set the ACL, else replace the grantees if input.ACL != "" { ACL.ACL = input.ACL + ACL.Grantees = []auth.Grantee{} + } else { + grantees := []auth.Grantee{} + + fullControlList, readList, readACPList, writeList, writeACPList := []string{}, []string{}, []string{}, []string{}, []string{} + + if *input.GrantFullControl != "" { + fullControlList = splitUnique(*input.GrantFullControl, ",") + fmt.Println(fullControlList) + for _, str := range fullControlList { + grantees = append(grantees, auth.Grantee{Access: str, Permission: "FULL_CONTROL"}) + } + } + if *input.GrantRead != "" { + readList = splitUnique(*input.GrantRead, ",") + for _, str := range readList { + grantees = append(grantees, auth.Grantee{Access: str, Permission: "READ"}) + } + } + if *input.GrantReadACP != "" { + readACPList = splitUnique(*input.GrantReadACP, ",") + for _, str := range readACPList { + grantees = append(grantees, auth.Grantee{Access: str, Permission: "READ_ACP"}) + } + } + if *input.GrantWrite != "" { + writeList = splitUnique(*input.GrantWrite, ",") + for _, str := range writeList { + grantees = append(grantees, auth.Grantee{Access: str, Permission: "WRITE"}) + } + } + if *input.GrantWriteACP != "" { + writeACPList = splitUnique(*input.GrantWriteACP, ",") + for _, str := range writeACPList { + grantees = append(grantees, auth.Grantee{Access: str, Permission: "WRITE_ACP"}) + } + } + + accs := append(append(append(append(fullControlList, readList...), writeACPList...), readACPList...), writeList...) + + // Check if the specified accounts exist + accList, err := checkIfAccountsExist(accs) + if err != nil { + return err + } + if len(accList) > 0 { + return fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", ")) + } + + ACL.Grantees = grantees + ACL.ACL = "" } ACLJson, err := json.Marshal(ACL) @@ -1202,7 +1196,8 @@ func (p *Posix) GetBucketAcl(bucket string) (*auth.GetBucketAclOutput, error) { grants := []types.Grant{} for _, elem := range ACL.Grantees { - grants = append(grants, types.Grant{Grantee: &types.Grantee{ID: &elem.Access}, Permission: elem.Permission}) + acs := elem.Access + grants = append(grants, types.Grant{Grantee: &types.Grantee{ID: &acs}, Permission: elem.Permission}) } return &auth.GetBucketAclOutput{ @@ -1323,3 +1318,18 @@ func checkIfAccountsExist(accs []string) ([]string, error) { } return result, nil } + +func splitUnique(s, divider string) []string { + elements := strings.Split(s, divider) + uniqueElements := make(map[string]bool) + result := make([]string, 0, len(elements)) + + for _, element := range elements { + if _, ok := uniqueElements[element]; !ok { + result = append(result, element) + uniqueElements[element] = true + } + } + + return result +} diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index b38c443..9d32cc5 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -30,19 +30,25 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/backend/auth" "github.com/versity/versitygw/s3api/utils" "github.com/versity/versitygw/s3err" ) type S3ApiController struct { - be backend.Backend + be backend.Backend + acl auth.ACLService } func New(be backend.Backend) S3ApiController { - return S3ApiController{be: be} + return S3ApiController{be: be, acl: auth.ACLServiceUnsupported{}} } func (c S3ApiController) ListBuckets(ctx *fiber.Ctx) error { + access, isRoot := ctx.Locals("access").(string), ctx.Locals("isRoot").(bool) + if err := c.acl.IsAdmin(access, isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } res, err := c.be.ListBuckets() return SendXMLResponse(ctx, res, err) } @@ -55,6 +61,8 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { maxParts := ctx.QueryInt("max-parts", 0) partNumberMarker := ctx.QueryInt("part-number-marker", 0) acceptRange := ctx.Get("Range") + access := ctx.Locals("access").(string) + isRoot := ctx.Locals("isRoot").(bool) if keyEnd != "" { key = strings.Join([]string{key, keyEnd}, "/") } @@ -66,20 +74,35 @@ func (c S3ApiController) GetActions(ctx *fiber.Ctx) error { if partNumberMarker < 0 || (partNumberMarker == 0 && ctx.Query("part-number-marker") != "") { return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker)) } + + if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } + res, err := c.be.ListObjectParts(bucket, key, uploadId, partNumberMarker, maxParts) return SendXMLResponse(ctx, res, err) } if ctx.Request().URI().QueryArgs().Has("acl") { + if err := c.acl.VerifyACL(bucket, access, "READ_ACP", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } res, err := c.be.GetObjectAcl(bucket, key) return SendXMLResponse(ctx, res, err) } if attrs := ctx.Get("X-Amz-Object-Attributes"); attrs != "" { + if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } res, err := c.be.GetObjectAttributes(bucket, key, strings.Split(attrs, ",")) return SendXMLResponse(ctx, res, err) } + if err := c.acl.VerifyACL(bucket, access, "READ_ACP", isRoot); err != nil { + return SendResponse(ctx, err) + } + res, err := c.be.GetObject(bucket, key, acceptRange, ctx.Response().BodyWriter()) if err != nil { return SendResponse(ctx, err) @@ -131,28 +154,43 @@ func (c S3ApiController) ListActions(ctx *fiber.Ctx) error { marker := ctx.Query("continuation-token") delimiter := ctx.Query("delimiter") maxkeys := ctx.QueryInt("max-keys") + access := ctx.Locals("access").(string) + isRoot := ctx.Locals("isRoot").(bool) if ctx.Request().URI().QueryArgs().Has("acl") { + if err := c.acl.VerifyACL(bucket, access, "READ_ACP", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } res, err := c.be.GetBucketAcl(ctx.Params("bucket")) return SendXMLResponse(ctx, res, err) } if ctx.Request().URI().QueryArgs().Has("uploads") { + if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } res, err := c.be.ListMultipartUploads(&s3.ListMultipartUploadsInput{Bucket: aws.String(ctx.Params("bucket"))}) return SendXMLResponse(ctx, res, err) } if ctx.QueryInt("list-type") == 2 { + if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } res, err := c.be.ListObjectsV2(bucket, prefix, marker, delimiter, maxkeys) return SendXMLResponse(ctx, res, err) } + if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } + res, err := c.be.ListObjects(bucket, prefix, marker, delimiter, maxkeys) return SendXMLResponse(ctx, res, err) } func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { - bucket, acl, grantFullControl, grantRead, grantReadACP, granWrite, grantWriteACP, access := + bucket, acl, grantFullControl, grantRead, grantReadACP, granWrite, grantWriteACP, access, isRoot := ctx.Params("bucket"), ctx.Get("X-Amz-Acl"), ctx.Get("X-Amz-Grant-Full-Control"), @@ -160,15 +198,24 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { ctx.Get("X-Amz-Grant-Read-Acp"), ctx.Get("X-Amz-Grant-Write"), ctx.Get("X-Amz-Grant-Write-Acp"), - ctx.Locals("access") + ctx.Locals("access").(string), + ctx.Locals("isRoot").(bool) - owner := access.(string) grants := grantFullControl + grantRead + grantReadACP + granWrite + grantWriteACP if grants != "" || acl != "" { if grants != "" && acl != "" { return errors.New("wrong api call") } + + if acl != "" && acl != "private" && acl != "public-read" && acl != "public-read-write" { + return errors.New("wrong api call") + } + + if err := c.acl.VerifyACL(bucket, access, "WRITE_ACP", isRoot); err != nil { + return SendResponse(ctx, err) + } + err := c.be.PutBucketAcl(&s3.PutBucketAclInput{ Bucket: &bucket, ACL: types.BucketCannedACL(acl), @@ -177,13 +224,13 @@ func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error { GrantReadACP: &grantReadACP, GrantWrite: &granWrite, GrantWriteACP: &grantWriteACP, - AccessControlPolicy: &types.AccessControlPolicy{Owner: &types.Owner{ID: &owner}}, + AccessControlPolicy: &types.AccessControlPolicy{Owner: &types.Owner{ID: &access}}, }) return SendResponse(ctx, err) } - err := c.be.PutBucket(bucket, owner) + err := c.be.PutBucket(bucket, access) return SendResponse(ctx, err) } @@ -193,6 +240,8 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") partNumberStr := ctx.Query("partNumber") + access := ctx.Locals("access").(string) + isRoot := ctx.Locals("isRoot").(bool) // Copy source headers copySource := ctx.Get("X-Amz-Copy-Source") @@ -237,6 +286,10 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPart)) } + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendResponse(ctx, err) + } + body := io.ReadSeeker(bytes.NewReader([]byte(ctx.Body()))) etag, err := c.be.PutObjectPart(bucket, keyStart, uploadId, partNumber, contentLength, body) @@ -249,6 +302,10 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { return errors.New("wrong api call") } + if err := c.acl.VerifyACL(bucket, access, "WRITE_ACP", isRoot); err != nil { + return SendResponse(ctx, err) + } + err := c.be.PutObjectAcl(&s3.PutObjectAclInput{ Bucket: &bucket, Key: &keyStart, @@ -268,12 +325,20 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { copySourceSplit := strings.Split(copySource, "/") srcBucket, srcObject := copySourceSplit[0], copySourceSplit[1:] + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } + res, err := c.be.CopyObject(srcBucket, strings.Join(srcObject, "/"), bucket, keyStart) return SendXMLResponse(ctx, res, err) } metadata := utils.GetUserMetaData(&ctx.Request().Header) + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendResponse(ctx, err) + } + etag, err := c.be.PutObject(&s3.PutObjectInput{ Bucket: &bucket, Key: &keyStart, @@ -286,17 +351,27 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { } func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error { - err := c.be.DeleteBucket(ctx.Params("bucket")) + bucket, access, isRoot := ctx.Params("bucket"), ctx.Locals("access").(string), ctx.Locals("isRoot").(bool) + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendResponse(ctx, err) + } + err := c.be.DeleteBucket(bucket) return SendResponse(ctx, err) } func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error { + bucket, access, isRoot := ctx.Params("bucket"), ctx.Locals("access").(string), ctx.Locals("isRoot").(bool) var dObj types.Delete + if err := xml.Unmarshal(ctx.Body(), &dObj); err != nil { return errors.New("wrong api call") } - err := c.be.DeleteObjects(ctx.Params("bucket"), &s3.DeleteObjectsInput{Delete: &dObj}) + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendResponse(ctx, err) + } + + err := c.be.DeleteObjects(bucket, &s3.DeleteObjectsInput{Delete: &dObj}) return SendResponse(ctx, err) } @@ -305,6 +380,8 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { key := ctx.Params("key") keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") + access := ctx.Locals("access").(string) + isRoot := ctx.Locals("isRoot").(bool) if keyEnd != "" { key = strings.Join([]string{key, keyEnd}, "/") @@ -313,6 +390,10 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { if uploadId != "" { expectedBucketOwner, requestPayer := ctx.Get("X-Amz-Expected-Bucket-Owner"), ctx.Get("X-Amz-Request-Payer") + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendResponse(ctx, err) + } + err := c.be.AbortMultipartUpload(&s3.AbortMultipartUploadInput{ UploadId: &uploadId, Bucket: &bucket, @@ -323,12 +404,21 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { return SendResponse(ctx, err) } + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendResponse(ctx, err) + } + err := c.be.DeleteObject(bucket, key) return SendResponse(ctx, err) } func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) error { - _, err := c.be.HeadBucket(ctx.Params("bucket")) + bucket, access, isRoot := ctx.Params("bucket"), ctx.Locals("access").(string), ctx.Locals("isRoot").(bool) + if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil { + return SendResponse(ctx, err) + } + + _, err := c.be.HeadBucket(bucket) // TODO: set bucket response headers return SendResponse(ctx, err) } @@ -338,13 +428,17 @@ const ( ) func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error { - bucket := ctx.Params("bucket") + bucket, access, isRoot := ctx.Params("bucket"), ctx.Locals("access").(string), ctx.Locals("isRoot").(bool) key := ctx.Params("key") keyEnd := ctx.Params("*1") if keyEnd != "" { key = strings.Join([]string{key, keyEnd}, "/") } + if err := c.acl.VerifyACL(bucket, access, "READ", isRoot); err != nil { + return SendResponse(ctx, err) + } + res, err := c.be.HeadObject(bucket, key) if err != nil { return SendResponse(ctx, err) @@ -389,6 +483,8 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { key := ctx.Params("key") keyEnd := ctx.Params("*1") uploadId := ctx.Query("uploadId") + access := ctx.Locals("access").(string) + isRoot := ctx.Locals("isRoot").(bool) if keyEnd != "" { key = strings.Join([]string{key, keyEnd}, "/") @@ -400,6 +496,11 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { if xmlErr != nil { return errors.New("wrong api call") } + + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendResponse(ctx, err) + } + err := c.be.RestoreObject(bucket, key, &restoreRequest) return SendResponse(ctx, err) } @@ -413,9 +514,18 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { return errors.New("wrong api call") } + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } + res, err := c.be.CompleteMultipartUpload(bucket, key, uploadId, data.Parts) return SendXMLResponse(ctx, res, err) } + + if err := c.acl.VerifyACL(bucket, access, "WRITE", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err) + } + res, err := c.be.CreateMultipartUpload(&s3.CreateMultipartUploadInput{Bucket: &bucket, Key: &key}) return SendXMLResponse(ctx, res, err) } diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index c92bead..6931544 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -39,6 +39,7 @@ func TestNew(t *testing.T) { } be := backend.BackendUnsupported{} + acl := auth.ACLServiceUnsupported{} tests := []struct { name string @@ -51,7 +52,8 @@ func TestNew(t *testing.T) { be: be, }, want: S3ApiController{ - be: be, + be: be, + acl: acl, }, }, } @@ -66,58 +68,80 @@ func TestNew(t *testing.T) { func TestS3ApiController_ListBuckets(t *testing.T) { type args struct { - ctx *fiber.Ctx + req *http.Request } app := fiber.New() + s3ApiController := S3ApiController{ + be: &BackendMock{ + ListBucketsFunc: func() (*s3.ListBucketsOutput, error) { + return &s3.ListBucketsOutput{}, nil + }, + }, + acl: auth.ACLServiceUnsupported{}, + } + + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) + app.Get("/", s3ApiController.ListBuckets) + + // Error case + appErr := fiber.New() + s3ApiControllerErr := S3ApiController{ + be: &BackendMock{ + ListBucketsFunc: func() (*s3.ListBucketsOutput, error) { + return nil, s3err.GetAPIError(s3err.ErrMethodNotAllowed) + }, + }, + acl: auth.ACLServiceUnsupported{}, + } + + appErr.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) + appErr.Get("/", s3ApiControllerErr.ListBuckets) tests := []struct { name string - c S3ApiController args args + app *fiber.App wantErr bool statusCode int }{ { - name: "List-bucket-not-implemented", - c: S3ApiController{ - be: backend.BackendUnsupported{}, - }, + name: "List-bucket-method-not-allowed", args: args{ - ctx: app.AcquireCtx(&fasthttp.RequestCtx{}), + req: httptest.NewRequest(http.MethodGet, "/", nil), }, + app: appErr, wantErr: false, - statusCode: 501, + statusCode: 405, }, { name: "list-bucket-success", - c: S3ApiController{ - be: &BackendMock{ - ListBucketsFunc: func() (*s3.ListBucketsOutput, error) { - return &s3.ListBucketsOutput{}, nil - }, - }, - }, args: args{ - ctx: app.AcquireCtx(&fasthttp.RequestCtx{}), + req: httptest.NewRequest(http.MethodGet, "/", nil), }, + app: app, wantErr: false, statusCode: 200, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.c.ListBuckets(tt.args.ctx) + resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.ListBuckets() error = %v, wantErr %v", err, tt.wantErr) - return } - statusCode := tt.args.ctx.Response().StatusCode() - - if statusCode != tt.statusCode { - t.Errorf("S3ApiController.ListBuckets() code = %v, wantErr %v", statusCode, tt.wantErr) + if resp.StatusCode != tt.statusCode { + t.Errorf("S3ApiController.ListBuckets() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } }) } @@ -129,20 +153,28 @@ func TestS3ApiController_GetActions(t *testing.T) { } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - ListObjectPartsFunc: func(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) { - return s3response.ListPartsResponse{}, nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + ListObjectPartsFunc: func(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) { + return s3response.ListPartsResponse{}, nil + }, + GetObjectAclFunc: func(bucket, object string) (*s3.GetObjectAclOutput, error) { + return &s3.GetObjectAclOutput{}, nil + }, + GetObjectAttributesFunc: func(bucket, object string, attributes []string) (*s3.GetObjectAttributesOutput, error) { + return &s3.GetObjectAttributesOutput{}, nil + }, + GetObjectFunc: func(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) { + return &s3.GetObjectOutput{Metadata: nil}, nil + }, }, - GetObjectAclFunc: func(bucket, object string) (*s3.GetObjectAclOutput, error) { - return &s3.GetObjectAclOutput{}, nil - }, - GetObjectAttributesFunc: func(bucket, object string, attributes []string) (*s3.GetObjectAttributesOutput, error) { - return &s3.GetObjectAttributesOutput{}, nil - }, - GetObjectFunc: func(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) { - return &s3.GetObjectOutput{Metadata: nil}, nil - }, - }} + acl: &auth.ACLServiceUnsupported{}, + } + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) app.Get("/:bucket/:key/*", s3ApiController.GetActions) // GetObjectACL @@ -231,29 +263,47 @@ func TestS3ApiController_ListActions(t *testing.T) { } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - GetBucketAclFunc: func(bucket string) (*auth.GetBucketAclOutput, error) { - return nil, nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + GetBucketAclFunc: func(bucket string) (*auth.GetBucketAclOutput, error) { + return nil, nil + }, + ListMultipartUploadsFunc: func(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) { + return s3response.ListMultipartUploadsResponse{}, nil + }, + ListObjectsV2Func: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error) { + return &s3.ListObjectsV2Output{}, nil + }, + ListObjectsFunc: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) { + return &s3.ListObjectsOutput{}, nil + }, }, - ListMultipartUploadsFunc: func(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) { - return s3response.ListMultipartUploadsResponse{}, nil - }, - ListObjectsV2Func: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error) { - return &s3.ListObjectsV2Output{}, nil - }, - ListObjectsFunc: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) { - return &s3.ListObjectsOutput{}, nil - }, - }} + acl: auth.ACLServiceUnsupported{}, + } + + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) + app.Get("/:bucket", s3ApiController.ListActions) //Error case - s3ApiControllerError := S3ApiController{be: &BackendMock{ - ListObjectsFunc: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) { - return nil, s3err.GetAPIError(s3err.ErrNotImplemented) + s3ApiControllerError := S3ApiController{ + be: &BackendMock{ + ListObjectsFunc: func(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) { + return nil, s3err.GetAPIError(s3err.ErrNotImplemented) + }, }, - }} + acl: auth.ACLServiceUnsupported{}, + } appError := fiber.New() + appError.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) appError.Get("/:bucket", s3ApiControllerError.ListActions) tests := []struct { @@ -330,17 +380,21 @@ func TestS3ApiController_PutBucketActions(t *testing.T) { } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - PutBucketAclFunc: func(*s3.PutBucketAclInput) error { - return nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + PutBucketAclFunc: func(*s3.PutBucketAclInput) error { + return nil + }, + PutBucketFunc: func(bucket, owner string) error { + return nil + }, }, - PutBucketFunc: func(bucket, owner string) error { - return nil - }, - }} + acl: auth.ACLServiceUnsupported{}, + } // Mock ctx.Locals app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) return ctx.Next() }) app.Put("/:bucket", s3ApiController.PutBucketActions) @@ -408,20 +462,28 @@ func TestS3ApiController_PutActions(t *testing.T) { } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - UploadPartCopyFunc: func(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) { - return &s3.UploadPartCopyOutput{}, nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + UploadPartCopyFunc: func(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) { + return &s3.UploadPartCopyOutput{}, nil + }, + PutObjectAclFunc: func(*s3.PutObjectAclInput) error { + return nil + }, + CopyObjectFunc: func(srcBucket, srcObject, DstBucket, dstObject string) (*s3.CopyObjectOutput, error) { + return &s3.CopyObjectOutput{}, nil + }, + PutObjectFunc: func(*s3.PutObjectInput) (string, error) { + return "Hey", nil + }, }, - PutObjectAclFunc: func(*s3.PutObjectAclInput) error { - return nil - }, - CopyObjectFunc: func(srcBucket, srcObject, DstBucket, dstObject string) (*s3.CopyObjectOutput, error) { - return &s3.CopyObjectOutput{}, nil - }, - PutObjectFunc: func(*s3.PutObjectInput) (string, error) { - return "Hey", nil - }, - }} + acl: auth.ACLServiceUnsupported{}, + } + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) app.Put("/:bucket/:key/*", s3ApiController.PutActions) //PutObjectAcl error @@ -538,23 +600,40 @@ func TestS3ApiController_DeleteBucket(t *testing.T) { } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - DeleteBucketFunc: func(bucket string) error { - return nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + DeleteBucketFunc: func(bucket string) error { + return nil + }, }, - }} + acl: auth.ACLServiceUnsupported{}, + } + + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) app.Delete("/:bucket", s3ApiController.DeleteBucket) // error case appErr := fiber.New() - s3ApiControllerErr := S3ApiController{be: &BackendMock{ - DeleteBucketFunc: func(bucket string) error { - return s3err.GetAPIError(48) + s3ApiControllerErr := S3ApiController{ + be: &BackendMock{ + DeleteBucketFunc: func(bucket string) error { + return s3err.GetAPIError(48) + }, }, - }} + acl: auth.ACLServiceUnsupported{}, + } + appErr.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) appErr.Delete("/:bucket", s3ApiControllerErr.DeleteBucket) tests := []struct { @@ -602,12 +681,20 @@ func TestS3ApiController_DeleteObjects(t *testing.T) { } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - DeleteObjectsFunc: func(bucket string, objects *s3.DeleteObjectsInput) error { - return nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + DeleteObjectsFunc: func(bucket string, objects *s3.DeleteObjectsInput) error { + return nil + }, }, - }} + acl: auth.ACLServiceUnsupported{}, + } + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) app.Post("/:bucket", s3ApiController.DeleteObjects) // Valid request body @@ -661,15 +748,23 @@ func TestS3ApiController_DeleteActions(t *testing.T) { } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - DeleteObjectFunc: func(bucket, object string) error { - return nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + DeleteObjectFunc: func(bucket, object string) error { + return nil + }, + AbortMultipartUploadFunc: func(*s3.AbortMultipartUploadInput) error { + return nil + }, }, - AbortMultipartUploadFunc: func(*s3.AbortMultipartUploadInput) error { - return nil - }, - }} + acl: auth.ACLServiceUnsupported{}, + } + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions) //Error case @@ -681,6 +776,11 @@ func TestS3ApiController_DeleteActions(t *testing.T) { }, }} + appErr.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) appErr.Delete("/:bucket", s3ApiControllerErr.DeleteBucket) tests := []struct { @@ -737,22 +837,39 @@ func TestS3ApiController_HeadBucket(t *testing.T) { } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) { - return &s3.HeadBucketOutput{}, nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) { + return &s3.HeadBucketOutput{}, nil + }, }, - }} + acl: auth.ACLServiceUnsupported{}, + } + + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) app.Head("/:bucket", s3ApiController.HeadBucket) - //Error case + // Error case appErr := fiber.New() s3ApiControllerErr := S3ApiController{be: &BackendMock{ HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) { return nil, s3err.GetAPIError(3) }, - }} + }, + acl: auth.ACLServiceUnsupported{}, + } + + appErr.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) appErr.Head("/:bucket", s3ApiControllerErr.HeadBucket) @@ -808,29 +925,45 @@ func TestS3ApiController_HeadObject(t *testing.T) { eTag := "Valid etag" lastModifie := time.Now() - s3ApiController := S3ApiController{be: &BackendMock{ - HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) { - return &s3.HeadObjectOutput{ - ContentEncoding: &contentEncoding, - ContentLength: 64, - ContentType: &contentType, - LastModified: &lastModifie, - ETag: &eTag, - }, nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) { + return &s3.HeadObjectOutput{ + ContentEncoding: &contentEncoding, + ContentLength: 64, + ContentType: &contentType, + LastModified: &lastModifie, + ETag: &eTag, + }, nil + }, }, - }} + acl: auth.ACLServiceUnsupported{}, + } + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) app.Head("/:bucket/:key/*", s3ApiController.HeadObject) //Error case appErr := fiber.New() - s3ApiControllerErr := S3ApiController{be: &BackendMock{ - HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) { - return nil, s3err.GetAPIError(42) + s3ApiControllerErr := S3ApiController{ + be: &BackendMock{ + HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) { + return nil, s3err.GetAPIError(42) + }, }, - }} + acl: auth.ACLServiceUnsupported{}, + } + appErr.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) appErr.Head("/:bucket/:key/*", s3ApiControllerErr.HeadObject) tests := []struct { @@ -877,18 +1010,26 @@ func TestS3ApiController_CreateActions(t *testing.T) { req *http.Request } app := fiber.New() - s3ApiController := S3ApiController{be: &BackendMock{ - RestoreObjectFunc: func(bucket, object string, restoreRequest *s3.RestoreObjectInput) error { - return nil + s3ApiController := S3ApiController{ + be: &BackendMock{ + RestoreObjectFunc: func(bucket, object string, restoreRequest *s3.RestoreObjectInput) error { + return nil + }, + CompleteMultipartUploadFunc: func(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) { + return &s3.CompleteMultipartUploadOutput{}, nil + }, + CreateMultipartUploadFunc: func(*s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { + return &s3.CreateMultipartUploadOutput{}, nil + }, }, - CompleteMultipartUploadFunc: func(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) { - return &s3.CompleteMultipartUploadOutput{}, nil - }, - CreateMultipartUploadFunc: func(*s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { - return &s3.CreateMultipartUploadOutput{}, nil - }, - }} + acl: auth.ACLServiceUnsupported{}, + } + app.Use(func(ctx *fiber.Ctx) error { + ctx.Locals("access", "valid access") + ctx.Locals("isRoot", true) + return ctx.Next() + }) app.Post("/:bucket/:key/*", s3ApiController.CreateActions) tests := []struct { diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index c3faeee..8858922 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -136,6 +136,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, debug bool) fib ctx.Locals("role", account.Role) ctx.Locals("access", creds[0]) + ctx.Locals("isRoot", creds[0] == root.Access) return ctx.Next() }