mirror of
https://github.com/versity/versitygw.git
synced 2026-07-02 16:54:25 +00:00
cd0b4e6d9d
Object key validation allowed internal parent-directory segments such as `public/../private.txt`. Bucket policy and auth checks evaluated the raw key, so a policy allowing bucket/public/* could match the request while posix backend later resolved the key with `filepath.Join` and accessed `bucket/private.txt`. Add backend-specific object key normalization to close that mismatch. The Backend interface now exposes `NormalizeObjectKey` so authorization can evaluate resources using the same key shape a backend will use for storage access. Backends that do not collapse object paths, including Azure and the S3 proxy, inherit `BackendUnsupported.NormalizeObjectKey`. That implementation returns the input key unchanged, avoiding unnecessary normalization and keeping policy evaluation unpolluted for object stores where ../ is part of the key name. posix/scoutfs normalize keys with filepath.Join so policy resources and request keys are compared after internal dot segments are collapsed. Bucket policy evaluation now normalizes both the incoming object key and object resource patterns from the policy before matching. Object lock governance bypass policy checks use the same backend normalizer as well, so retention and legal hold authorization cannot diverge from backend path resolution.
203 lines
5.7 KiB
Go
203 lines
5.7 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 auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/versity/versitygw/backend"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
func VerifyObjectCopyAccess(ctx context.Context, be backend.Backend, copySource string, opts AccessOptions) error {
|
|
if opts.IsRoot {
|
|
return nil
|
|
}
|
|
if opts.Acc.Role == RoleAdmin {
|
|
return nil
|
|
}
|
|
|
|
// Verify destination bucket access
|
|
if err := VerifyAccess(ctx, be, opts); err != nil {
|
|
return err
|
|
}
|
|
// Verify source bucket access.
|
|
// URL-decode the copy source before splitting so that clients which send
|
|
// the bucket/key separator as "%2F" are handled correctly.
|
|
// Callers are expected to have already stripped any leading '/'.
|
|
decodedSrc, err := url.QueryUnescape(copySource)
|
|
if err != nil {
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, copySource)
|
|
}
|
|
srcBucket, srcObject, found := strings.Cut(decodedSrc, "/")
|
|
if !found {
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, copySource)
|
|
}
|
|
|
|
// Get source bucket ACL
|
|
srcBucketACLBytes, err := be.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &srcBucket})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var srcBucketAcl ACL
|
|
if err := json.Unmarshal(srcBucketACLBytes, &srcBucketAcl); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := VerifyAccess(ctx, be, AccessOptions{
|
|
Acl: srcBucketAcl,
|
|
AclPermission: PermissionRead,
|
|
IsRoot: opts.IsRoot,
|
|
Acc: opts.Acc,
|
|
Bucket: srcBucket,
|
|
Object: srcObject,
|
|
Actions: []Action{GetObjectAction},
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type AccessOptions struct {
|
|
Acl ACL
|
|
AclPermission Permission
|
|
IsRoot bool
|
|
Acc Account
|
|
Bucket string
|
|
Object string
|
|
Actions []Action
|
|
Readonly bool
|
|
IsPublicRequest bool
|
|
DisableACL bool
|
|
}
|
|
|
|
func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) error {
|
|
if opts.Readonly {
|
|
if opts.AclPermission == PermissionWrite || opts.AclPermission == PermissionWriteAcp {
|
|
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
|
}
|
|
}
|
|
// Skip the access check for public bucket requests
|
|
if opts.IsPublicRequest {
|
|
return nil
|
|
}
|
|
if opts.IsRoot {
|
|
return nil
|
|
}
|
|
if opts.Acc.Role == RoleAdmin {
|
|
return nil
|
|
}
|
|
|
|
policy, policyErr := be.GetBucketPolicy(ctx, opts.Bucket)
|
|
if policyErr != nil {
|
|
if !errors.Is(policyErr, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
|
|
return policyErr
|
|
}
|
|
} else {
|
|
return VerifyBucketPolicy(policy, opts.Acc.Access, opts.Bucket, opts.Object, be.NormalizeObjectKey, opts.Actions...)
|
|
}
|
|
|
|
if err := verifyACL(opts.Acl, opts.Acc.Access, opts.AclPermission, opts.DisableACL); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyPublicAccess checks if the bucket is publically accessible by ACL or Policy
|
|
func VerifyPublicAccess(ctx context.Context, be backend.Backend, action Action, permission Permission, bucket, object string) error {
|
|
// ACL disabled
|
|
policy, err := be.GetBucketPolicy(ctx, bucket)
|
|
if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
|
|
return err
|
|
}
|
|
if err == nil {
|
|
err = VerifyPublicBucketPolicy(policy, bucket, object, be.NormalizeObjectKey, action)
|
|
if errors.Is(err, errExplicitDeny) {
|
|
// Explicit public-policy Deny has higher precedence than any
|
|
// public ACL grant, so do not continue to ACL fallback.
|
|
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
|
}
|
|
if err == nil {
|
|
// if ACLs are disabled, and the bucket grants public access,
|
|
// policy actions should return 'MethodNotAllowed'
|
|
switch action {
|
|
case GetBucketPolicyAction:
|
|
return s3err.GetMethodNotAllowedErr(http.MethodGet, s3err.ResourceTypeBucketPolicy, nil)
|
|
case PutBucketPolicyAction:
|
|
return s3err.GetMethodNotAllowedErr(http.MethodPut, s3err.ResourceTypeBucketPolicy, nil)
|
|
case DeleteBucketPolicyAction:
|
|
return s3err.GetMethodNotAllowedErr(http.MethodDelete, s3err.ResourceTypeBucketPolicy, nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// if the action is not in the ACL whitelist the access is denied
|
|
_, ok := publicACLAllowedActions[action]
|
|
if !ok {
|
|
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
|
}
|
|
|
|
err = VerifyPublicBucketACL(ctx, be, bucket, action, permission)
|
|
if err != nil {
|
|
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func IsAdminOrOwner(acct Account, isRoot bool, acl ACL) error {
|
|
// Owner check
|
|
if acct.Access == acl.Owner {
|
|
return nil
|
|
}
|
|
|
|
// Root user has access over almost everything
|
|
if isRoot {
|
|
return nil
|
|
}
|
|
|
|
// Admin user case
|
|
if acct.Role == RoleAdmin {
|
|
return nil
|
|
}
|
|
|
|
// Return access denied in all other cases
|
|
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
|
}
|
|
|
|
type PublicACLAllowedActions map[Action]struct{}
|
|
|
|
var publicACLAllowedActions PublicACLAllowedActions = PublicACLAllowedActions{
|
|
ListBucketAction: struct{}{},
|
|
PutObjectAction: struct{}{},
|
|
ListBucketMultipartUploadsAction: struct{}{},
|
|
DeleteObjectAction: struct{}{},
|
|
ListBucketVersionsAction: struct{}{},
|
|
GetObjectAction: struct{}{},
|
|
GetObjectAttributesAction: struct{}{},
|
|
GetObjectAclAction: struct{}{},
|
|
}
|