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.
257 lines
7.0 KiB
Go
257 lines
7.0 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 (
|
|
"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/v3"
|
|
"github.com/versity/versitygw/auth"
|
|
"github.com/versity/versitygw/debuglogger"
|
|
"github.com/versity/versitygw/s3api/utils"
|
|
"github.com/versity/versitygw/s3err"
|
|
"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))
|
|
versionId := ctx.Query("versionId")
|
|
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)
|
|
|
|
if err := utils.ValidateVersionId(ctx); err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
action := auth.DeleteObjectTaggingAction
|
|
if versionId != "" {
|
|
action = auth.DeleteObjectVersionTaggingAction
|
|
}
|
|
|
|
err := c.verifyAccess(ctx,
|
|
auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Object: key,
|
|
Actions: []auth.Action{action},
|
|
IsPublicRequest: isBucketPublic,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.DeleteObjectTagging(ctx.RequestCtx(), bucket, key, versionId)
|
|
return &Response{
|
|
Headers: map[string]*string{
|
|
"x-amz-version-id": &versionId,
|
|
},
|
|
MetaOpts: &MetaOptions{
|
|
Status: http.StatusNoContent,
|
|
BucketOwner: parsedAcl.Owner,
|
|
EventName: s3event.EventObjectTaggingDelete,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) AbortMultipartUpload(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
|
uploadId := ctx.Query("uploadId")
|
|
ifMatchInitiatedTime := utils.ParsePreconditionDateHeader(ctx.Get("X-Amz-If-Match-Initiated-Time"))
|
|
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)
|
|
|
|
// A malformed upload ID is rejected before access is verified.
|
|
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.AbortMultipartUploadAction},
|
|
IsPublicRequest: isBucketPublic,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.AbortMultipartUpload(ctx.RequestCtx(),
|
|
&s3.AbortMultipartUploadInput{
|
|
UploadId: &uploadId,
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
IfMatchInitiatedTime: ifMatchInitiatedTime,
|
|
})
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
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 := auth.BypassModeForRequest(strings.EqualFold(ctx.Get("X-Amz-Bypass-Governance-Retention"), "true"))
|
|
ifMatch := utils.GetStringPtr(strings.Trim(ctx.Get("If-Match"), `"`))
|
|
ifMatchLastModTime := utils.ParsePreconditionDateHeader(ctx.Get("X-Amz-If-Match-Last-Modified-Time"))
|
|
ifMatchSize := utils.ParseIfMatchSize(ctx)
|
|
// 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)
|
|
|
|
if err := utils.ValidateVersionId(ctx); err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
// If-Match only applies to the current version of the object, and is
|
|
// rejected ahead of authorization when a version is named
|
|
if versionId != "" && ifMatch != nil {
|
|
debuglogger.Logf("conditional delete with versionId %q", versionId)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetNotImplementedErr("If-Match", s3err.NmpAdditionalMessageVersionedDelete)
|
|
}
|
|
|
|
action := auth.DeleteObjectAction
|
|
if versionId != "" {
|
|
action = auth.DeleteObjectVersionAction
|
|
}
|
|
|
|
err := c.verifyAccess(ctx,
|
|
auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Object: key,
|
|
Actions: []auth.Action{action},
|
|
IsPublicRequest: isBucketPublic,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = auth.CheckObjectAccess(
|
|
ctx,
|
|
bucket,
|
|
acct,
|
|
[]types.ObjectIdentifier{
|
|
{
|
|
Key: &key,
|
|
VersionId: &versionId,
|
|
},
|
|
},
|
|
bypass,
|
|
isBucketPublic,
|
|
c.be,
|
|
c.iam,
|
|
false,
|
|
)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
res, err := c.be.DeleteObject(ctx.RequestCtx(),
|
|
&s3.DeleteObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
VersionId: &versionId,
|
|
IfMatch: ifMatch,
|
|
IfMatchLastModifiedTime: ifMatchLastModTime,
|
|
IfMatchSize: ifMatchSize,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
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{
|
|
BucketOwner: parsedAcl.Owner,
|
|
EventName: s3event.EventObjectRemovedDelete,
|
|
Status: http.StatusNoContent,
|
|
},
|
|
}, nil
|
|
}
|