mirror of
https://github.com/versity/versitygw.git
synced 2026-04-25 23:20:29 +00:00
feat: implements advanced routing for object DELETE and POST actions.
fixes #896 fixes #899 Registeres an all route matcher handler at the end of the router to handle the cases when the api call doesn't match to any s3 action. The all routes matcher returns `MethodNotAllowed` for this kind of requests.
This commit is contained in:
@@ -1938,385 +1938,6 @@ const (
|
||||
timefmt = "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||
)
|
||||
|
||||
func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := ctx.Params("key")
|
||||
keyEnd := ctx.Params("*1")
|
||||
uploadId := ctx.Query("uploadId")
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
contentType := ctx.Get("Content-Type")
|
||||
contentDisposition := ctx.Get("Content-Disposition")
|
||||
contentLanguage := ctx.Get("Content-Language")
|
||||
cacheControl := ctx.Get("Cache-Control")
|
||||
contentEncoding := ctx.Get("Content-Encoding")
|
||||
tagging := ctx.Get("X-Amz-Tagging")
|
||||
|
||||
if keyEnd != "" {
|
||||
key = strings.Join([]string{key, keyEnd}, "/")
|
||||
}
|
||||
|
||||
path := ctx.Path()
|
||||
if path[len(path)-1:] == "/" && key[len(key)-1:] != "/" {
|
||||
key = key + "/"
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("restore") {
|
||||
var restoreRequest types.RestoreRequest
|
||||
if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
if c.debug {
|
||||
debuglogger.Logf("failed to parse the request body: %v", err)
|
||||
}
|
||||
return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionRestoreObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
}
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionWrite,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Object: key,
|
||||
Action: auth.RestoreObjectAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return SendResponse(ctx, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionRestoreObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
err = c.be.RestoreObject(ctx.Context(), &s3.RestoreObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
RestoreRequest: &restoreRequest,
|
||||
})
|
||||
return SendResponse(ctx, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
EvSender: c.evSender,
|
||||
Action: metrics.ActionRestoreObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
EventName: s3event.EventObjectRestoreCompleted,
|
||||
})
|
||||
}
|
||||
|
||||
if ctx.Request().URI().QueryArgs().Has("select") && ctx.Query("select-type") == "2" {
|
||||
var payload s3response.SelectObjectContentPayload
|
||||
|
||||
err := xml.Unmarshal(ctx.Body(), &payload)
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
debuglogger.Logf("error unmarshalling select object content: %v", err)
|
||||
}
|
||||
return SendXMLResponse(ctx, nil,
|
||||
s3err.GetAPIError(s3err.ErrMalformedXML),
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionSelectObjectContent,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
err = auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionRead,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Object: key,
|
||||
Action: auth.GetObjectAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return SendXMLResponse(ctx, nil, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionSelectObjectContent,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
sw := c.be.SelectObjectContent(ctx.Context(),
|
||||
&s3.SelectObjectContentInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
Expression: payload.Expression,
|
||||
ExpressionType: payload.ExpressionType,
|
||||
InputSerialization: payload.InputSerialization,
|
||||
OutputSerialization: payload.OutputSerialization,
|
||||
RequestProgress: payload.RequestProgress,
|
||||
ScanRange: payload.ScanRange,
|
||||
})
|
||||
|
||||
ctx.Context().SetBodyStreamWriter(sw)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if uploadId != "" {
|
||||
data := struct {
|
||||
Parts []types.CompletedPart `xml:"Part"`
|
||||
}{}
|
||||
|
||||
err := xml.Unmarshal(ctx.Body(), &data)
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
debuglogger.Logf("error unmarshalling complete multipart upload: %v", err)
|
||||
}
|
||||
return SendXMLResponse(ctx, nil,
|
||||
s3err.GetAPIError(s3err.ErrMalformedXML),
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
if len(data.Parts) == 0 {
|
||||
if c.debug {
|
||||
debuglogger.Logf("empty parts provided for complete multipart upload")
|
||||
}
|
||||
return SendXMLResponse(ctx, nil,
|
||||
s3err.GetAPIError(s3err.ErrEmptyParts),
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
var mpuObjectSize *int64
|
||||
mpuObjSizeHdr := ctx.Get("X-Amz-Mp-Object-Size")
|
||||
if mpuObjSizeHdr != "" {
|
||||
val, err := strconv.ParseInt(mpuObjSizeHdr, 10, 64)
|
||||
//TODO: Not sure if invalid request should be returned
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
debuglogger.Logf("invalid value for 'x-amz-mp-objects-size' header: %v", err)
|
||||
}
|
||||
return SendXMLResponse(ctx, nil,
|
||||
s3err.GetAPIError(s3err.ErrInvalidRequest),
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
if val < 0 {
|
||||
debuglogger.Logf("value for 'x-amz-mp-objects-size' header is less than 0: %v", val)
|
||||
return SendXMLResponse(ctx, nil,
|
||||
s3err.GetInvalidMpObjectSizeErr(val),
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
mpuObjectSize = &val
|
||||
}
|
||||
|
||||
err = auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionWrite,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Object: key,
|
||||
Action: auth.PutObjectAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return SendXMLResponse(ctx, nil, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
_, checksums, err := utils.ParseChecksumHeaders(ctx)
|
||||
if err != nil {
|
||||
return SendXMLResponse(ctx, nil, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
checksumType := types.ChecksumType(ctx.Get("x-amz-checksum-type"))
|
||||
err = utils.IsChecksumTypeValid(checksumType)
|
||||
if err != nil {
|
||||
return SendXMLResponse(ctx, nil, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
res, versid, err := c.be.CompleteMultipartUpload(ctx.Context(),
|
||||
&s3.CompleteMultipartUploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
UploadId: &uploadId,
|
||||
MultipartUpload: &types.CompletedMultipartUpload{
|
||||
Parts: data.Parts,
|
||||
},
|
||||
MpuObjectSize: mpuObjectSize,
|
||||
ChecksumCRC32: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32]),
|
||||
ChecksumCRC32C: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32c]),
|
||||
ChecksumSHA1: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha1]),
|
||||
ChecksumSHA256: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha256]),
|
||||
ChecksumCRC64NVME: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc64nvme]),
|
||||
ChecksumType: checksumType,
|
||||
})
|
||||
if err == nil {
|
||||
if versid != "" {
|
||||
utils.SetResponseHeaders(ctx, []utils.CustomHeader{
|
||||
{
|
||||
Key: "x-amz-version-id",
|
||||
Value: versid,
|
||||
},
|
||||
})
|
||||
}
|
||||
return SendXMLResponse(ctx, res, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
EvSender: c.evSender,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
ObjectETag: res.ETag,
|
||||
EventName: s3event.EventCompleteMultipartUpload,
|
||||
VersionId: backend.GetPtrFromString(versid),
|
||||
})
|
||||
}
|
||||
return SendXMLResponse(ctx, res, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionWrite,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Object: key,
|
||||
Action: auth.PutObjectAction,
|
||||
})
|
||||
if err != nil {
|
||||
return SendXMLResponse(ctx, nil, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCreateMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
objLockState, err := utils.ParsObjectLockHdrs(ctx)
|
||||
if err != nil {
|
||||
return SendXMLResponse(ctx, nil, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCreateMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
metadata := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
|
||||
checksumAlgorithm, checksumType, err := utils.ParseCreateMpChecksumHeaders(ctx)
|
||||
if err != nil {
|
||||
return SendXMLResponse(ctx, nil, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCreateMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
expires := ctx.Get("Expires")
|
||||
|
||||
res, err := c.be.CreateMultipartUpload(ctx.Context(),
|
||||
s3response.CreateMultipartUploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
Tagging: &tagging,
|
||||
ContentType: &contentType,
|
||||
ContentEncoding: &contentEncoding,
|
||||
ContentDisposition: &contentDisposition,
|
||||
ContentLanguage: &contentLanguage,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
ObjectLockRetainUntilDate: &objLockState.RetainUntilDate,
|
||||
ObjectLockMode: objLockState.ObjectLockMode,
|
||||
ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus,
|
||||
Metadata: metadata,
|
||||
ChecksumAlgorithm: checksumAlgorithm,
|
||||
ChecksumType: checksumType,
|
||||
})
|
||||
if err == nil {
|
||||
if checksumAlgorithm != "" {
|
||||
utils.SetResponseHeaders(ctx, []utils.CustomHeader{
|
||||
{
|
||||
Key: "x-amz-checksum-algorithm",
|
||||
Value: string(checksumAlgorithm),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return SendXMLResponse(ctx, res, err,
|
||||
&MetaOpts{
|
||||
Logger: c.logger,
|
||||
MetricsMng: c.mm,
|
||||
Action: metrics.ActionCreateMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
type MetaOpts struct {
|
||||
Logger s3log.AuditLogger
|
||||
EvSender s3event.S3EventSender
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -837,269 +836,6 @@ func TestS3ApiController_DeleteObjects(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiController_DeleteActions(t *testing.T) {
|
||||
type args struct {
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
|
||||
return &s3.DeleteObjectOutput{}, nil
|
||||
},
|
||||
AbortMultipartUploadFunc: func(context.Context, *s3.AbortMultipartUploadInput) error {
|
||||
return nil
|
||||
},
|
||||
DeleteObjectTaggingFunc: func(_ context.Context, bucket, object string) error {
|
||||
return nil
|
||||
},
|
||||
GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
|
||||
utils.ContextKeyIsRoot.Set(ctx, true)
|
||||
utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
|
||||
return ctx.Next()
|
||||
})
|
||||
app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions)
|
||||
|
||||
// Error case
|
||||
appErr := fiber.New()
|
||||
|
||||
s3ApiControllerErr := S3ApiController{be: &BackendMock{
|
||||
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
||||
},
|
||||
GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)
|
||||
},
|
||||
}}
|
||||
|
||||
appErr.Use(func(ctx *fiber.Ctx) error {
|
||||
utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
|
||||
utils.ContextKeyIsRoot.Set(ctx, true)
|
||||
utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
|
||||
return ctx.Next()
|
||||
})
|
||||
appErr.Delete("/:bucket/:key/*", s3ApiControllerErr.DeleteActions)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
app *fiber.App
|
||||
args args
|
||||
wantErr bool
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "Abort-multipart-upload-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key?uploadId=324234", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 204,
|
||||
},
|
||||
{
|
||||
name: "Remove-object-tagging-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key/key2?tagging", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 204,
|
||||
},
|
||||
{
|
||||
name: "Delete-object-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 204,
|
||||
},
|
||||
{
|
||||
name: "Delete-object-error",
|
||||
app: appErr,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/my-bucket/invalid-key", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 404,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
resp, err := tt.app.Test(tt.args.req)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3ApiController.DeleteActions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != tt.statusCode {
|
||||
t.Errorf("S3ApiController.DeleteActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiController_CreateActions(t *testing.T) {
|
||||
type args struct {
|
||||
req *http.Request
|
||||
}
|
||||
app := fiber.New()
|
||||
s3ApiController := S3ApiController{
|
||||
be: &BackendMock{
|
||||
GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
|
||||
return acldata, nil
|
||||
},
|
||||
RestoreObjectFunc: func(context.Context, *s3.RestoreObjectInput) error {
|
||||
return nil
|
||||
},
|
||||
CompleteMultipartUploadFunc: func(context.Context, *s3.CompleteMultipartUploadInput) (s3response.CompleteMultipartUploadResult, string, error) {
|
||||
return s3response.CompleteMultipartUploadResult{}, "", nil
|
||||
},
|
||||
CreateMultipartUploadFunc: func(context.Context, s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) {
|
||||
return s3response.InitiateMultipartUploadResult{}, nil
|
||||
},
|
||||
SelectObjectContentFunc: func(context.Context, *s3.SelectObjectContentInput) func(w *bufio.Writer) {
|
||||
return func(w *bufio.Writer) {}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bdy := `
|
||||
<SelectObjectContentRequest>
|
||||
<Expression>string</Expression>
|
||||
<ExpressionType>string</ExpressionType>
|
||||
</SelectObjectContentRequest>
|
||||
`
|
||||
|
||||
completMpBody := `
|
||||
<CompleteMultipartUpload xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||
<Part>
|
||||
<ETag>etag</ETag>
|
||||
<PartNumber>1</PartNumber>
|
||||
</Part>
|
||||
</CompleteMultipartUpload>
|
||||
`
|
||||
|
||||
completMpEmptyBody := `
|
||||
<CompleteMultipartUpload xmlns="http://s3.amazonaws.com/doc/2006-03-01/"></CompleteMultipartUpload>
|
||||
`
|
||||
|
||||
app.Use(func(ctx *fiber.Ctx) error {
|
||||
utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
|
||||
utils.ContextKeyIsRoot.Set(ctx, true)
|
||||
utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
|
||||
return ctx.Next()
|
||||
})
|
||||
app.Post("/:bucket/:key/*", s3ApiController.CreateActions)
|
||||
|
||||
invChecksumAlgo := httptest.NewRequest(http.MethodPost, "/my-bucket/my-key", nil)
|
||||
invChecksumAlgo.Header.Set("X-Amz-Checksum-Algorithm", "invalid_checksum_algorithm")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
app *fiber.App
|
||||
args args
|
||||
wantErr bool
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "Restore-object-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?restore", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Select-object-content-invalid-body",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Select-object-content-invalid-body",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", strings.NewReader(bdy)),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Complete-multipart-upload-error",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Complete-multipart-upload-empty-parts",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", strings.NewReader(completMpEmptyBody)),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Complete-multipart-upload-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", strings.NewReader(completMpBody)),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
name: "Create-multipart-upload-invalid-checksum-algorithm",
|
||||
app: app,
|
||||
args: args{
|
||||
req: invChecksumAlgo,
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Create-multipart-upload-success",
|
||||
app: app,
|
||||
args: args{
|
||||
req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
statusCode: 200,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
resp, err := tt.app.Test(tt.args.req)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("S3ApiController.CreateActions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != tt.statusCode {
|
||||
t.Errorf("S3ApiController.CreateActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_XMLresponse(t *testing.T) {
|
||||
type args struct {
|
||||
ctx *fiber.Ctx
|
||||
|
||||
207
s3api/controllers/object-delete.go
Normal file
207
s3api/controllers/object-delete.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/metrics"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3event"
|
||||
)
|
||||
|
||||
func (c S3ApiController) DeleteObjectTagging(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(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,
|
||||
Object: key,
|
||||
Action: auth.DeleteObjectTaggingAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionDeleteObjectTagging,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = c.be.DeleteObjectTagging(ctx.Context(), bucket, key)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Status: http.StatusNoContent,
|
||||
Action: metrics.ActionDeleteObjectTagging,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
EventName: s3event.EventObjectTaggingDelete,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) AbortMultipartUplaod(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
uploadId := ctx.Query("uploadId")
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(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,
|
||||
Object: key,
|
||||
Action: auth.AbortMultipartUploadAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionAbortMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = c.be.AbortMultipartUpload(ctx.Context(),
|
||||
&s3.AbortMultipartUploadInput{
|
||||
UploadId: &uploadId,
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
})
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionAbortMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
Status: http.StatusNoContent,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) DeleteObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
versionId := ctx.Query("versionId")
|
||||
bypass := strings.EqualFold(ctx.Get("X-Amz-Bypass-Governance-Retention"), "true")
|
||||
// context locals
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
|
||||
//TODO: check s3:DeleteObjectVersion policy in case a use tries to delete a version of an object
|
||||
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionWrite,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Object: key,
|
||||
Action: auth.DeleteObjectAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionDeleteObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = auth.CheckObjectAccess(
|
||||
ctx.Context(),
|
||||
bucket,
|
||||
acct.Access,
|
||||
[]types.ObjectIdentifier{
|
||||
{
|
||||
Key: &key,
|
||||
VersionId: &versionId,
|
||||
},
|
||||
},
|
||||
bypass,
|
||||
IsBucketPublic,
|
||||
c.be,
|
||||
)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionDeleteObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
res, err := c.be.DeleteObject(ctx.Context(),
|
||||
&s3.DeleteObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
VersionId: &versionId,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionDeleteObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
EventName: s3event.EventObjectRemovedDelete,
|
||||
Status: http.StatusNoContent,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
headers := map[string]*string{
|
||||
"x-amz-version-id": res.VersionId,
|
||||
}
|
||||
|
||||
if res.DeleteMarker != nil && *res.DeleteMarker {
|
||||
headers["x-amz-delete-marker"] = utils.GetStringPtr("true")
|
||||
}
|
||||
|
||||
return &Response{
|
||||
Headers: headers,
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionDeleteObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
EventName: s3event.EventObjectRemovedDelete,
|
||||
Status: http.StatusNoContent,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
376
s3api/controllers/object-post.go
Normal file
376
s3api/controllers/object-post.go
Normal file
@@ -0,0 +1,376 @@
|
||||
// Copyright 2023 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/metrics"
|
||||
"github.com/versity/versitygw/s3api/debuglogger"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3event"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
func (c S3ApiController) RestoreObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
|
||||
var restoreRequest types.RestoreRequest
|
||||
if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err != nil {
|
||||
debuglogger.Logf("failed to parse the request body: %v", err)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionRestoreObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
||||
}
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionWrite,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Object: key,
|
||||
Action: auth.RestoreObjectAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionRestoreObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = c.be.RestoreObject(ctx.Context(), &s3.RestoreObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
RestoreRequest: &restoreRequest,
|
||||
})
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionRestoreObject,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
EventName: s3event.EventObjectRestoreCompleted,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) SelectObjectContent(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
|
||||
var payload s3response.SelectObjectContentPayload
|
||||
|
||||
err := xml.Unmarshal(ctx.Body(), &payload)
|
||||
if err != nil {
|
||||
debuglogger.Logf("error unmarshalling select object content: %v", err)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionSelectObjectContent,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
||||
}
|
||||
|
||||
err = auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionRead,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Object: key,
|
||||
Action: auth.GetObjectAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionSelectObjectContent,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
sw := c.be.SelectObjectContent(ctx.Context(),
|
||||
&s3.SelectObjectContentInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
Expression: payload.Expression,
|
||||
ExpressionType: payload.ExpressionType,
|
||||
InputSerialization: payload.InputSerialization,
|
||||
OutputSerialization: payload.OutputSerialization,
|
||||
RequestProgress: payload.RequestProgress,
|
||||
ScanRange: payload.ScanRange,
|
||||
})
|
||||
|
||||
ctx.Context().SetBodyStreamWriter(sw)
|
||||
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionSelectObjectContent,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
contentType := ctx.Get("Content-Type")
|
||||
contentDisposition := ctx.Get("Content-Disposition")
|
||||
contentLanguage := ctx.Get("Content-Language")
|
||||
cacheControl := ctx.Get("Cache-Control")
|
||||
contentEncoding := ctx.Get("Content-Encoding")
|
||||
tagging := ctx.Get("X-Amz-Tagging")
|
||||
expires := ctx.Get("Expires")
|
||||
metadata := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
// context locals
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(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,
|
||||
Object: key,
|
||||
Action: auth.PutObjectAction,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCreateMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
objLockState, err := utils.ParsObjectLockHdrs(ctx)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCreateMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
checksumAlgorithm, checksumType, err := utils.ParseCreateMpChecksumHeaders(ctx)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCreateMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
res, err := c.be.CreateMultipartUpload(ctx.Context(),
|
||||
s3response.CreateMultipartUploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
Tagging: &tagging,
|
||||
ContentType: &contentType,
|
||||
ContentEncoding: &contentEncoding,
|
||||
ContentDisposition: &contentDisposition,
|
||||
ContentLanguage: &contentLanguage,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
ObjectLockRetainUntilDate: &objLockState.RetainUntilDate,
|
||||
ObjectLockMode: objLockState.ObjectLockMode,
|
||||
ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus,
|
||||
Metadata: metadata,
|
||||
ChecksumAlgorithm: checksumAlgorithm,
|
||||
ChecksumType: checksumType,
|
||||
})
|
||||
var headers map[string]*string
|
||||
if err == nil {
|
||||
headers = map[string]*string{
|
||||
"x-amz-checksum-algorithm": utils.ConvertToStringPtr(checksumAlgorithm),
|
||||
}
|
||||
}
|
||||
return &Response{
|
||||
Headers: headers,
|
||||
Data: res,
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCreateMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
uploadId := ctx.Query("uploadId")
|
||||
mpuObjSizeHdr := ctx.Get("X-Amz-Mp-Object-Size")
|
||||
checksumType := types.ChecksumType(ctx.Get("x-amz-checksum-type"))
|
||||
// context locals
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(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,
|
||||
Object: key,
|
||||
Action: auth.PutObjectAction,
|
||||
IsBucketPublic: IsBucketPublic,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Parts []types.CompletedPart `xml:"Part"`
|
||||
}{}
|
||||
|
||||
err = xml.Unmarshal(ctx.Body(), &data)
|
||||
if err != nil {
|
||||
debuglogger.Logf("error unmarshalling complete multipart upload: %v", err)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
||||
}
|
||||
|
||||
if len(data.Parts) == 0 {
|
||||
debuglogger.Logf("empty parts provided for complete multipart upload")
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrEmptyParts)
|
||||
}
|
||||
|
||||
var mpuObjectSize *int64
|
||||
if mpuObjSizeHdr != "" {
|
||||
val, err := strconv.ParseInt(mpuObjSizeHdr, 10, 64)
|
||||
//TODO: Not sure if invalid request should be returned
|
||||
if err != nil {
|
||||
debuglogger.Logf("invalid value for 'x-amz-mp-objects-size' header: %v", err)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrInvalidRequest)
|
||||
}
|
||||
|
||||
if val < 0 {
|
||||
debuglogger.Logf("value for 'x-amz-mp-objects-size' header is less than 0: %v", val)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetInvalidMpObjectSizeErr(val)
|
||||
}
|
||||
|
||||
mpuObjectSize = &val
|
||||
}
|
||||
|
||||
_, checksums, err := utils.ParseChecksumHeaders(ctx)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = utils.IsChecksumTypeValid(checksumType)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
res, versid, err := c.be.CompleteMultipartUpload(ctx.Context(),
|
||||
&s3.CompleteMultipartUploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
UploadId: &uploadId,
|
||||
MultipartUpload: &types.CompletedMultipartUpload{
|
||||
Parts: data.Parts,
|
||||
},
|
||||
MpuObjectSize: mpuObjectSize,
|
||||
ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]),
|
||||
ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]),
|
||||
ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]),
|
||||
ChecksumSHA256: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha256]),
|
||||
ChecksumCRC64NVME: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc64nvme]),
|
||||
ChecksumType: checksumType,
|
||||
})
|
||||
return &Response{
|
||||
Data: res,
|
||||
Headers: map[string]*string{
|
||||
"x-amz-version-id": &versid,
|
||||
},
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionCompleteMultipartUpload,
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
ObjectETag: res.ETag,
|
||||
EventName: s3event.EventCompleteMultipartUpload,
|
||||
VersionId: &versid,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
@@ -183,3 +183,12 @@ func SetResponseHeaders(ctx *fiber.Ctx, headers map[string]*string) {
|
||||
ctx.Response().Header.Add(key, *val)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns MethodNotAllowed for unmatched routes
|
||||
func (c S3ApiController) HandleUnmatch(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Action: metrics.ActionUndetected,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
||||
}
|
||||
|
||||
107
s3api/router.go
107
s3api/router.go
@@ -54,67 +54,69 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
app.Patch("/list-buckets", middlewares.IsAdmin(logger), adminController.ListBuckets)
|
||||
}
|
||||
|
||||
// ListBuckets
|
||||
// ListBuckets action
|
||||
app.Get("/", controllers.ProcessResponse(ctrl.ListBuckets, logger, evs, mm))
|
||||
|
||||
bucketRouter := app.Group("/:bucket")
|
||||
objectRouter := app.Group("/:bucket/*")
|
||||
|
||||
// PUT bucket operations
|
||||
app.Put("/:bucket", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.PutBucketTagging, logger, evs, mm))
|
||||
app.Put("/:bucket", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.PutBucketOwnershipControls, logger, evs, mm))
|
||||
app.Put("/:bucket", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(ctrl.PutBucketVersioning, logger, evs, mm))
|
||||
app.Put("/:bucket", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(ctrl.PutObjectLockConfiguration, logger, evs, mm))
|
||||
app.Put("/:bucket", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.PutBucketCors, logger, evs, mm))
|
||||
app.Put("/:bucket", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.PutBucketPolicy, logger, evs, mm))
|
||||
app.Put("/:bucket", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.PutBucketAcl, logger, evs, mm))
|
||||
app.Put("/:bucket", controllers.ProcessResponse(ctrl.CreateBucket, logger, evs, mm))
|
||||
bucketRouter.Put("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.PutBucketTagging, logger, evs, mm))
|
||||
bucketRouter.Put("", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.PutBucketOwnershipControls, logger, evs, mm))
|
||||
bucketRouter.Put("", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(ctrl.PutBucketVersioning, logger, evs, mm))
|
||||
bucketRouter.Put("", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(ctrl.PutObjectLockConfiguration, logger, evs, mm))
|
||||
bucketRouter.Put("", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.PutBucketCors, logger, evs, mm))
|
||||
bucketRouter.Put("", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.PutBucketPolicy, logger, evs, mm))
|
||||
bucketRouter.Put("", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.PutBucketAcl, logger, evs, mm))
|
||||
bucketRouter.Put("", controllers.ProcessResponse(ctrl.CreateBucket, logger, evs, mm))
|
||||
|
||||
// HeadBucket
|
||||
app.Head("/:bucket", controllers.ProcessResponse(ctrl.HeadBucket, logger, evs, mm))
|
||||
// HeadBucket action
|
||||
bucketRouter.Head("", controllers.ProcessResponse(ctrl.HeadBucket, logger, evs, mm))
|
||||
|
||||
// Delete bucket operations
|
||||
app.Delete("/:bucket", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.DeleteBucketTagging, logger, evs, mm))
|
||||
app.Delete("/:bucket", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.DeleteBucketOwnershipControls, logger, evs, mm))
|
||||
app.Delete("/:bucket", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.DeleteBucketPolicy, logger, evs, mm))
|
||||
app.Delete("/:bucket", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.DeleteBucketCors, logger, evs, mm))
|
||||
app.Delete("/:bucket", controllers.ProcessResponse(ctrl.DeleteBucket, logger, evs, mm))
|
||||
// DELETE bucket operations
|
||||
bucketRouter.Delete("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.DeleteBucketTagging, logger, evs, mm))
|
||||
bucketRouter.Delete("", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.DeleteBucketOwnershipControls, logger, evs, mm))
|
||||
bucketRouter.Delete("", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.DeleteBucketPolicy, logger, evs, mm))
|
||||
bucketRouter.Delete("", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.DeleteBucketCors, logger, evs, mm))
|
||||
bucketRouter.Delete("", controllers.ProcessResponse(ctrl.DeleteBucket, logger, evs, mm))
|
||||
|
||||
// GET bucket operations
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.GetBucketTagging, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.GetBucketOwnershipControls, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(ctrl.GetBucketVersioning, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.GetBucketPolicy, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.GetBucketCors, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(ctrl.GetObjectLockConfiguration, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.GetBucketAcl, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("uploads"), controllers.ProcessResponse(ctrl.ListMultipartUploads, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgs("versions"), controllers.ProcessResponse(ctrl.ListObjectVersions, logger, evs, mm))
|
||||
app.Get("/:bucket", middlewares.MatchQueryArgWithValue("list-type", "2"), controllers.ProcessResponse(ctrl.ListObjectsV2, logger, evs, mm))
|
||||
app.Get("/:bucket", controllers.ProcessResponse(ctrl.ListObjects, logger, evs, mm))
|
||||
|
||||
// HeadObject
|
||||
app.Head("/:bucket/*", controllers.ProcessResponse(ctrl.HeadObject, logger, evs, mm))
|
||||
|
||||
// GET object operations
|
||||
app.Get("/:bucket/*", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.GetObjectTagging, logger, evs, mm))
|
||||
app.Get("/:bucket/*", middlewares.MatchQueryArgs("retention"), controllers.ProcessResponse(ctrl.GetObjectRetention, logger, evs, mm))
|
||||
app.Get("/:bucket/*", middlewares.MatchQueryArgs("legal-hold"), controllers.ProcessResponse(ctrl.GetObjectLegalHold, logger, evs, mm))
|
||||
app.Get("/:bucket/*", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.GetObjectAcl, logger, evs, mm))
|
||||
app.Get("/:bucket/*", middlewares.MatchQueryArgs("attributes"), controllers.ProcessResponse(ctrl.GetObjectAttributes, logger, evs, mm))
|
||||
app.Get("/:bucket/*", middlewares.MatchQueryArgs("uploadId"), controllers.ProcessResponse(ctrl.ListParts, logger, evs, mm))
|
||||
app.Get("/:bucket/*", controllers.ProcessResponse(ctrl.GetObject, logger, evs, mm))
|
||||
|
||||
// DeleteObject action
|
||||
// AbortMultipartUpload action
|
||||
// DeleteObjectTagging action
|
||||
app.Delete("/:bucket/:key/*", ctrl.DeleteActions)
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.GetBucketTagging, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("ownershipControls"), controllers.ProcessResponse(ctrl.GetBucketOwnershipControls, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("versioning"), controllers.ProcessResponse(ctrl.GetBucketVersioning, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("policy"), controllers.ProcessResponse(ctrl.GetBucketPolicy, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("cors"), controllers.ProcessResponse(ctrl.GetBucketCors, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("object-lock"), controllers.ProcessResponse(ctrl.GetObjectLockConfiguration, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.GetBucketAcl, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("uploads"), controllers.ProcessResponse(ctrl.ListMultipartUploads, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgs("versions"), controllers.ProcessResponse(ctrl.ListObjectVersions, logger, evs, mm))
|
||||
bucketRouter.Get("", middlewares.MatchQueryArgWithValue("list-type", "2"), controllers.ProcessResponse(ctrl.ListObjectsV2, logger, evs, mm))
|
||||
bucketRouter.Get("", controllers.ProcessResponse(ctrl.ListObjects, logger, evs, mm))
|
||||
|
||||
// DeleteObjects action
|
||||
app.Post("/:bucket", ctrl.DeleteObjects)
|
||||
bucketRouter.Post("", ctrl.DeleteObjects)
|
||||
|
||||
// CompleteMultipartUpload action
|
||||
// CreateMultipartUpload
|
||||
// RestoreObject action
|
||||
// SelectObjectContent action
|
||||
app.Post("/:bucket/:key/*", ctrl.CreateActions)
|
||||
// HeadObject
|
||||
objectRouter.Head("", controllers.ProcessResponse(ctrl.HeadObject, logger, evs, mm))
|
||||
|
||||
// GET object operations
|
||||
objectRouter.Get("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.GetObjectTagging, logger, evs, mm))
|
||||
objectRouter.Get("", middlewares.MatchQueryArgs("retention"), controllers.ProcessResponse(ctrl.GetObjectRetention, logger, evs, mm))
|
||||
objectRouter.Get("", middlewares.MatchQueryArgs("legal-hold"), controllers.ProcessResponse(ctrl.GetObjectLegalHold, logger, evs, mm))
|
||||
objectRouter.Get("", middlewares.MatchQueryArgs("acl"), controllers.ProcessResponse(ctrl.GetObjectAcl, logger, evs, mm))
|
||||
objectRouter.Get("", middlewares.MatchQueryArgs("attributes"), controllers.ProcessResponse(ctrl.GetObjectAttributes, logger, evs, mm))
|
||||
objectRouter.Get("", middlewares.MatchQueryArgs("uploadId"), controllers.ProcessResponse(ctrl.ListParts, logger, evs, mm))
|
||||
objectRouter.Get("", controllers.ProcessResponse(ctrl.GetObject, logger, evs, mm))
|
||||
|
||||
// DELETE object operations
|
||||
objectRouter.Delete("", middlewares.MatchQueryArgs("tagging"), controllers.ProcessResponse(ctrl.DeleteObjectTagging, logger, evs, mm))
|
||||
objectRouter.Delete("", middlewares.MatchQueryArgs("uploadId"), controllers.ProcessResponse(ctrl.AbortMultipartUplaod, logger, evs, mm))
|
||||
objectRouter.Delete("", controllers.ProcessResponse(ctrl.DeleteObject, logger, evs, mm))
|
||||
|
||||
objectRouter.Post("", middlewares.MatchQueryArgs("restore"), controllers.ProcessResponse(ctrl.RestoreObject, logger, evs, mm))
|
||||
objectRouter.Post("", middlewares.MatchQueryArgs("list-type"), middlewares.MatchQueryArgWithValue("list-type", "2"), controllers.ProcessResponse(ctrl.RestoreObject, logger, evs, mm))
|
||||
objectRouter.Post("", middlewares.MatchQueryArgs("uploadId"), controllers.ProcessResponse(ctrl.CompleteMultipartUpload, logger, evs, mm))
|
||||
objectRouter.Post("", middlewares.MatchQueryArgs("uploads"), controllers.ProcessResponse(ctrl.CreateMultipartUpload, logger, evs, mm))
|
||||
|
||||
// CopyObject action
|
||||
// PutObject action
|
||||
@@ -123,4 +125,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
// PutObjectTagging action
|
||||
// PutObjectAcl action
|
||||
app.Put("/:bucket/:key/*", ctrl.PutActions)
|
||||
|
||||
// Return MethodNotAllowed for all the unmatched routes
|
||||
app.All("*", controllers.ProcessResponse(ctrl.HandleUnmatch, logger, evs, mm))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user