mirror of
https://github.com/versity/versitygw.git
synced 2026-10-22 06:12:39 +00:00
Fixes #2533 `UploadPartCopy` and `CopyObject` returned `500 InternalError` when a non-root user copied from a source bucket with no stored ACL (such as a posix bucket directory created outside the gateway): `VerifyObjectCopyAccess` ran `json.Unmarshal` on the empty ACL bytes posix returns for such a bucket. It now parses them with `ParseACL`, like every other ACL reader. The source checks also ran before the backend looked at the upload ID, so a malformed `uploadId` got `AccessDenied`, `NoSuchBucket` or `InvalidArgument` instead of `NoSuchUpload`. S3 rejects an upload ID that is malformed, or bound to another key, right after the bucket existence and expected-bucket-owner checks, before authorization and nearly all request validation. That applies to every multipart operation that takes an upload ID. `Backend` gains `ValidateUploadID`, a format-only check that doesn't look up the upload. `BackendUnsupported` accepts every ID, so s3proxy and azure are unchanged, and posix's existing canonical UUID check becomes the method. `UploadPart`, `UploadPartCopy` and `AbortMultipartUpload` call it first. `ListParts` calls it after rejecting a `part-number-marker` or `max-parts` that isn't an integer (new `ValidateLimiterInteger`); negative values are still rejected after it. `CompleteMultipartUpload` calls it after rejecting unsupported `If-Match`/`If-None-Match` combinations, which are split out of `EvaluateObjectPutPreconditions` as `ValidateObjectPutPreconditionHeaders` and now checked before access verification and body parsing, as on S3.
457 lines
14 KiB
Go
457 lines
14 KiB
Go
// 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/v3"
|
|
"github.com/versity/versitygw/auth"
|
|
"github.com/versity/versitygw/backend"
|
|
"github.com/versity/versitygw/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)
|
|
|
|
err := c.verifyAccess(ctx,
|
|
auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Object: key,
|
|
Actions: []auth.Action{auth.RestoreObjectAction},
|
|
IsPublicRequest: isBucketPublic,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
var restoreRequest types.RestoreRequest
|
|
if err := xml.Unmarshal(ctx.BodyRaw(), &restoreRequest); err != nil {
|
|
debuglogger.Logf("failed to parse the request body: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
err = c.be.RestoreObject(ctx.RequestCtx(), &s3.RestoreObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
RestoreRequest: &restoreRequest,
|
|
})
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
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)
|
|
|
|
err := c.verifyAccess(ctx,
|
|
auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionRead,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Object: key,
|
|
Actions: []auth.Action{auth.GetObjectAction},
|
|
IsPublicRequest: isBucketPublic,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
var payload s3response.SelectObjectContentPayload
|
|
err = xml.Unmarshal(ctx.BodyRaw(), &payload)
|
|
if err != nil {
|
|
debuglogger.Logf("error unmarshalling select object content: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
sw := c.be.SelectObjectContent(ctx.RequestCtx(),
|
|
&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.RequestCtx().SetBodyStreamWriter(sw)
|
|
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
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", defaultContentType)
|
|
contentDisposition := ctx.Get("Content-Disposition")
|
|
contentLanguage := ctx.Get("Content-Language")
|
|
cacheControl := ctx.Get("Cache-Control")
|
|
contentEncoding := utils.ParseContentEncoding(ctx)
|
|
tagging := ctx.Get("X-Amz-Tagging")
|
|
expires := ctx.Get("Expires")
|
|
websiteRedirectLocation := ctx.Get("X-Amz-Website-Redirect-Location")
|
|
storageClass := ctx.Get("X-Amz-Storage-Class")
|
|
legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold")
|
|
lockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode")
|
|
objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date")
|
|
|
|
// context locals
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
|
|
actions := auth.ObjectUploadActions(tagging, legalHoldHdr, lockModeHdr, objLockDate)
|
|
|
|
err := c.verifyAccess(ctx,
|
|
auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Object: key,
|
|
Actions: actions,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
ssec, err := c.parseSSECHeaders(ctx)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
metadata, err := utils.GetUserMetaData(&ctx.Request().Header)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = utils.ValidateWebsiteRedirectLocation(websiteRedirectLocation)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
objLockState, err := utils.ParsObjectLockHdrs(ctx)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
checksumAlgorithm, checksumType, err := utils.ParseCreateMpChecksumHeaders(ctx)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
// lock headers need a bucket with Object Lock
|
|
if objLockState.LegalHoldStatus != "" || objLockState.ObjectLockMode != "" {
|
|
if _, err := auth.VerifyWriteObjectLock(ctx.RequestCtx(), c.be, bucket, true); err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
}
|
|
|
|
res, err := c.be.CreateMultipartUpload(ctx.RequestCtx(),
|
|
s3response.CreateMultipartUploadInput{
|
|
SSECustomerAlgorithm: ssec.Algorithm,
|
|
SSECustomerKey: ssec.Key,
|
|
SSECustomerKeyMD5: ssec.KeyMD5,
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
Tagging: &tagging,
|
|
ContentType: &contentType,
|
|
ContentEncoding: &contentEncoding,
|
|
ContentDisposition: &contentDisposition,
|
|
ContentLanguage: &contentLanguage,
|
|
CacheControl: &cacheControl,
|
|
Expires: &expires,
|
|
WebsiteRedirectLocation: &websiteRedirectLocation,
|
|
ObjectLockRetainUntilDate: &objLockState.RetainUntilDate,
|
|
ObjectLockMode: objLockState.ObjectLockMode,
|
|
ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus,
|
|
Metadata: metadata,
|
|
StorageClass: types.StorageClass(storageClass),
|
|
ChecksumAlgorithm: checksumAlgorithm,
|
|
ChecksumType: checksumType,
|
|
})
|
|
var headers map[string]*string
|
|
if err == nil {
|
|
headers = map[string]*string{
|
|
"x-amz-checksum-algorithm": utils.ConvertToStringPtr(checksumAlgorithm),
|
|
"x-amz-checksum-type": utils.ConvertToStringPtr(checksumType),
|
|
"x-amz-server-side-encryption-customer-algorithm": res.SSECustomerAlgorithm,
|
|
"x-amz-server-side-encryption-customer-key-MD5": res.SSECustomerKeyMD5,
|
|
}
|
|
}
|
|
return &Response{
|
|
Headers: headers,
|
|
Data: res,
|
|
MetaOpts: &MetaOptions{
|
|
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(strings.ToUpper(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)
|
|
|
|
// Unsupported conditional headers are rejected first, then a malformed
|
|
// upload ID, both before access is verified or the body is parsed.
|
|
ifMatch, ifNoneMatch := utils.ParsePreconditionMatchHeaders(ctx)
|
|
err := backend.ValidateObjectPutPreconditionHeaders(ifMatch, ifNoneMatch)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.ValidateUploadID(uploadId)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.verifyAccess(ctx,
|
|
auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Object: key,
|
|
Actions: []auth.Action{auth.PutObjectAction},
|
|
IsPublicRequest: isBucketPublic,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
ssec, err := c.parseSSECHeaders(ctx)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
var body s3response.CompleteMultipartUploadRequestBody
|
|
err = xml.Unmarshal(ctx.BodyRaw(), &body)
|
|
if err != nil {
|
|
debuglogger.Logf("error unmarshalling complete multipart upload: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if len(body.Parts) == 0 {
|
|
debuglogger.Logf("empty parts provided for complete multipart upload")
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
var mpuObjectSize *int64
|
|
if mpuObjSizeHdr != "" {
|
|
val, err := strconv.ParseInt(mpuObjSizeHdr, 10, 64)
|
|
if err != nil {
|
|
debuglogger.Logf("invalid value for 'x-amz-mp-object-size' header: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetInvalidMpObjectSizeErr(mpuObjSizeHdr)
|
|
}
|
|
|
|
if val < 0 {
|
|
debuglogger.Logf("value for 'x-amz-mp-object-size' header is less than 0: %v", val)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetNegatvieMpObjectSizeErr(val)
|
|
}
|
|
|
|
mpuObjectSize = &val
|
|
}
|
|
|
|
checksums, err := utils.ParseCompleteMpChecksumHeaders(ctx)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = utils.IsChecksumTypeValid(checksumType)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = auth.CheckObjectAccess(ctx, bucket, acct, []types.ObjectIdentifier{{Key: &key}}, auth.BypassOverwrite, isBucketPublic, c.be, c.iam, true)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
res, versid, err := c.be.CompleteMultipartUpload(ctx.RequestCtx(),
|
|
&s3.CompleteMultipartUploadInput{
|
|
SSECustomerAlgorithm: ssec.Algorithm,
|
|
SSECustomerKey: ssec.Key,
|
|
SSECustomerKeyMD5: ssec.KeyMD5,
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
UploadId: &uploadId,
|
|
MultipartUpload: &types.CompletedMultipartUpload{
|
|
Parts: body.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]),
|
|
ChecksumSHA512: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha512]),
|
|
ChecksumMD5: utils.GetStringPtr(checksums[types.ChecksumAlgorithmMd5]),
|
|
ChecksumXXHASH64: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash64]),
|
|
ChecksumXXHASH3: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash3]),
|
|
ChecksumXXHASH128: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash128]),
|
|
ChecksumType: checksumType,
|
|
IfMatch: ifMatch,
|
|
IfNoneMatch: ifNoneMatch,
|
|
})
|
|
if err == nil {
|
|
objUrl := utils.GenerateObjectLocation(ctx, c.virtualDomain, bucket, key)
|
|
res.Location = &objUrl
|
|
}
|
|
return &Response{
|
|
Data: res,
|
|
Headers: map[string]*string{
|
|
"x-amz-version-id": &versid,
|
|
},
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
ObjectETag: res.ETag,
|
|
EventName: s3event.EventCompleteMultipartUpload,
|
|
VersionId: &versid,
|
|
},
|
|
}, err
|
|
}
|