Merge pull request #1875 from versity/sis/disable-acl-option

feat: configuration option to disable ACLs
This commit is contained in:
Ben McClelland
2026-02-28 09:32:23 -08:00
committed by GitHub
26 changed files with 1045 additions and 631 deletions
+2 -1
View File
@@ -79,6 +79,7 @@ type AccessOptions struct {
Action Action
Readonly bool
IsPublicRequest bool
DisableACL bool
}
func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) error {
@@ -107,7 +108,7 @@ func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) e
return VerifyBucketPolicy(policy, opts.Acc.Access, opts.Bucket, opts.Object, opts.Action)
}
if err := verifyACL(opts.Acl, opts.Acc.Access, opts.AclPermission); err != nil {
if err := verifyACL(opts.Acl, opts.Acc.Access, opts.AclPermission, opts.DisableACL); err != nil {
return err
}
+11 -1
View File
@@ -414,7 +414,17 @@ func splitUnique(s, divider string) []string {
return result
}
func verifyACL(acl ACL, access string, permission Permission) error {
func verifyACL(acl ACL, access string, permission Permission, disableACL bool) error {
if disableACL {
// only the bucket owner should have access to the bucket
// as bucket ACLs are disabled and no grantee check is necessary
if acl.Owner != access {
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
return nil
}
grantee := Grantee{
Access: access,
Permission: permission,
+11
View File
@@ -100,6 +100,7 @@ var (
webuiNoTLS bool
webuiGateways []string
webuiAdminGateways []string
disableACLs bool
)
var (
@@ -341,6 +342,13 @@ func initFlags() []cli.Flag {
Destination: &virtualDomain,
Aliases: []string{"vd"},
},
&cli.BoolFlag{
Name: "disable-acl",
Usage: "disables gateway ACLs, by ignoring all ACL headers",
EnvVars: []string{"VGW_DISABLE_ACL"},
Destination: &disableACLs,
Aliases: []string{"noacl"},
},
&cli.StringFlag{
Name: "access-log",
Usage: "enable server access logging to specified file",
@@ -833,6 +841,9 @@ func runGateway(ctx context.Context, be backend.Backend) error {
if keepAlive {
opts = append(opts, s3api.WithKeepAlive())
}
if disableACLs {
opts = append(opts, s3api.WithDisableACL())
}
if debug {
debuglogger.SetDebugEnabled()
}
+5
View File
@@ -152,6 +152,11 @@ func initTestCommands() []*cli.Command {
Usage: "Tests gateway access control with bucket ACLs and Policies",
Action: getAction(integration.TestAccessControl),
},
{
Name: "noacl",
Usage: "Tests gateway in ACL-disabled mode",
Action: getAction(integration.TestNoAclMode),
},
{
Name: "bench",
Usage: "Runs download/upload performance test on the gateway",
+7
View File
@@ -111,6 +111,13 @@ ROOT_SECRET_ACCESS_KEY=
# operations will be allowed.
#VGW_READ_ONLY=false
# Disable ACL support at the gateway level. All ACL headers on requests are
# ignored, and no access control is enforced using bucket ACLs.
# Prefer using bucket policies instead of ACLs for access management.
# PutBucketAcl returns an immediate AccessControlListNotSupported error.
# GetBucketAcl returns a successful response containing the default bucket ACL.
#VGW_DISABLE_ACL=false
# The VGW_VIRTUAL_DOMAIN option enables the virtual host style bucket
# addressing. The path style addressing is the default, and remains enabled
# even when virtual host style is enabled. The VGW_VIRTUAL_DOMAIN option
+77 -6
View File
@@ -5,8 +5,15 @@ rm -rf /tmp/gw
mkdir /tmp/gw
rm -rf /tmp/covdata
mkdir /tmp/covdata
rm -rf /tmp/versioing.covdata
rm -rf /tmp/https.covdata
mkdir /tmp/https.covdata
rm -rf /tmp/versioning.covdata
mkdir /tmp/versioning.covdata
rm -rf /tmp/versioning.https.covdata
mkdir /tmp/versioning.https.covdata
rm -rf /tmp/noacl.covdata
mkdir /tmp/noacl.covdata
rm -rf /tmp/versioningdir
mkdir /tmp/versioningdir
@@ -153,10 +160,74 @@ fi
# kill off server
kill $GW_VS_HTTPS_PID
ECHO "Running No ACL integration tests"
# run server in background versioning-enabled
# port: 7073
GOCOVERDIR=/tmp/noacl.covdata ./versitygw -p :7074 -a user -s pass -noacl --iam-dir /tmp/gw posix /tmp/gw &
GW_NO_ACL_PID=$!
# wait a second for server to start up
sleep 1
# check if noacl gateway process is still running
if ! kill -0 $GW_NO_ACL_PID; then
echo "noacl server no longer running"
exit 1
fi
if ! ./versitygw test --allow-insecure -a user -s pass -e http://127.0.0.1:7074 noacl; then
echo "No ACL integration tests failed"
kill $GW_NO_ACL_PID
exit 1
fi
# kill off server
kill $GW_NO_ACL_PID
exit 0
# if the above binary was built with -cover enabled (make testbin),
# then the following can be used for code coverage reports:
# go tool covdata percent -i=/tmp/covdata
# go tool covdata textfmt -i=/tmp/covdata -o profile.txt
# go tool cover -html=profile.txt
# -----------------------------------------------------------------------------
# Coverage Reports (Go 1.20+ Runtime Coverage)
#
# The servers above were started with GOCOVERDIR=<dir>, which causes Go to write
# raw coverage artifacts into those directories (covmeta + covcounters.* files).
# These raw files must be processed with "go tool covdata" to generate
# human-readable coverage reports.
#
# You may generate *per-environment* coverage or a *merged full-suite* report.
#
# -----------------------------------------------------------------------------
# 1) INDIVIDUAL COVERAGE REPORTS
#
# Example for a single environment (e.g. /tmp/covdata):
#
# go tool covdata percent -i=/tmp/covdata
# go tool covdata textfmt -i=/tmp/covdata -o /tmp/profile.txt
# go tool cover -html=/tmp/profile.txt
#
# Repeat using:
# /tmp/covdata
# /tmp/https.covdata
# /tmp/versioning.covdata
# /tmp/versioning.https.covdata
# /tmp/noacl.covdata
#
# This gives you coverage metrics isolated per test suite / server mode.
#
# -----------------------------------------------------------------------------
# 2) MERGED COVERAGE REPORT (RECOMMENDED)
#
# If you want a unified report combining all environments:
#
# go tool covdata merge \
# -i=/tmp/covdata,/tmp/https.covdata,/tmp/versioning.covdata,/tmp/versioning.https.covdata,/tmp/noacl.covdata \
# -o /tmp/allcovdata
#
# go tool covdata percent -i=/tmp/allcovdata
# go tool covdata textfmt -i=/tmp/allcovdata -o /tmp/all_profile.txt
# go tool cover -html=/tmp/all_profile.txt
#
# This produces the full aggregate coverage across all HTTP/HTTPS,
# versioning-enabled, non-versioning, and no-ACL test runs.
#
# -----------------------------------------------------------------------------
+1 -1
View File
@@ -681,7 +681,7 @@ func TestAdminController_CreateBucket(t *testing.T) {
},
}
s3api := New(be, iam, nil, nil, nil, false, "")
s3api := New(be, iam, nil, nil, nil, false, false, "")
ctrl := AdminController{
iam: iam,
+11 -1
View File
@@ -39,6 +39,7 @@ type S3ApiController struct {
evSender s3event.S3EventSender
mm metrics.Manager
readonly bool
disableACL bool
virtualDomain string
}
@@ -59,7 +60,7 @@ var (
xmlhdr = []byte(`<?xml version="1.0" encoding="UTF-8"?>` + "\n")
)
func New(be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, readonly bool, virtualDomain string) S3ApiController {
func New(be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, readonly, disableACL bool, virtualDomain string) S3ApiController {
return S3ApiController{
be: be,
iam: iam,
@@ -67,10 +68,19 @@ func New(be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs
evSender: evs,
readonly: readonly,
mm: mm,
disableACL: disableACL,
virtualDomain: virtualDomain,
}
}
func (c S3ApiController) getAclHeaderValue(ctx *fiber.Ctx, key string, defaultValues ...string) string {
if c.disableACL {
return ""
}
return ctx.Get(key, defaultValues...)
}
// Returns MethodNotAllowed for unmatched routes
func (c S3ApiController) HandleErrorRoute(err error) Controller {
return func(ctx *fiber.Ctx) (*Response, error) {
+5
View File
@@ -39,6 +39,7 @@ func (c S3ApiController) DeleteBucketTagging(ctx *fiber.Ctx) (*Response, error)
Bucket: bucket,
Action: auth.PutBucketTaggingAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -72,6 +73,7 @@ func (c S3ApiController) DeleteBucketOwnershipControls(ctx *fiber.Ctx) (*Respons
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketOwnershipControlsAction,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -105,6 +107,7 @@ func (c S3ApiController) DeleteBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Action: auth.DeleteBucketPolicyAction,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -140,6 +143,7 @@ func (c S3ApiController) DeleteBucketCors(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.PutBucketCorsAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -175,6 +179,7 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.DeleteBucketAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+13
View File
@@ -41,6 +41,7 @@ func (c S3ApiController) GetBucketTagging(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.GetBucketTaggingAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -93,6 +94,7 @@ func (c S3ApiController) GetBucketOwnershipControls(ctx *fiber.Ctx) (*Response,
Bucket: bucket,
Action: auth.GetBucketOwnershipControlsAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -133,6 +135,7 @@ func (c S3ApiController) GetBucketVersioning(ctx *fiber.Ctx) (*Response, error)
Bucket: bucket,
Action: auth.GetBucketVersioningAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -175,6 +178,7 @@ func (c S3ApiController) GetBucketCors(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.GetBucketCorsAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -218,6 +222,7 @@ func (c S3ApiController) GetBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.GetBucketPolicyAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -252,6 +257,7 @@ func (c S3ApiController) GetBucketPolicyStatus(ctx *fiber.Ctx) (*Response, error
Bucket: bucket,
Action: auth.GetBucketPolicyStatusAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -313,6 +319,7 @@ func (c S3ApiController) ListObjectVersions(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.ListBucketVersionsAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -366,6 +373,7 @@ func (c S3ApiController) GetObjectLockConfiguration(ctx *fiber.Ctx) (*Response,
Bucket: bucket,
Action: auth.GetBucketObjectLockConfigurationAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -411,6 +419,7 @@ func (c S3ApiController) GetBucketAcl(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.GetBucketAclAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -462,6 +471,7 @@ func (c S3ApiController) ListMultipartUploads(ctx *fiber.Ctx) (*Response, error)
Bucket: bucket,
Action: auth.ListBucketMultipartUploadsAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -519,6 +529,7 @@ func (c S3ApiController) ListObjectsV2(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.ListBucketAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -576,6 +587,7 @@ func (c S3ApiController) ListObjects(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.ListBucketAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -627,6 +639,7 @@ func (c S3ApiController) GetBucketLocation(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.GetBucketLocationAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+1
View File
@@ -42,6 +42,7 @@ func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.ListBucketAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+1
View File
@@ -47,6 +47,7 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.DeleteObjectAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+22 -6
View File
@@ -47,6 +47,7 @@ func (c S3ApiController) PutBucketTagging(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.PutBucketTaggingAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -88,6 +89,7 @@ func (c S3ApiController) PutBucketOwnershipControls(ctx *fiber.Ctx) (*Response,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketOwnershipControlsAction,
DisableACL: c.disableACL,
}); err != nil {
return &Response{
MetaOpts: &MetaOptions{
@@ -143,6 +145,7 @@ func (c S3ApiController) PutBucketVersioning(ctx *fiber.Ctx) (*Response, error)
Bucket: bucket,
Action: auth.PutBucketVersioningAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -197,6 +200,7 @@ func (c S3ApiController) PutObjectLockConfiguration(ctx *fiber.Ctx) (*Response,
Bucket: bucket,
Action: auth.PutBucketObjectLockConfigurationAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
}); err != nil {
return &Response{
MetaOpts: &MetaOptions{
@@ -238,6 +242,7 @@ func (c S3ApiController) PutBucketCors(ctx *fiber.Ctx) (*Response, error) {
Bucket: bucket,
Action: auth.PutBucketCorsAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -292,6 +297,7 @@ func (c S3ApiController) PutBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketPolicyAction,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -344,6 +350,7 @@ func (c S3ApiController) PutBucketAcl(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketAclAction,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -353,6 +360,15 @@ func (c S3ApiController) PutBucketAcl(ctx *fiber.Ctx) (*Response, error) {
}, err
}
if c.disableACL {
debuglogger.Logf("PutBucketAcl is not available, as ACLs are disabled at gateway level")
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrACLsDisabled)
}
err = auth.ValidateCannedACL(acl)
if err != nil {
return &Response{
@@ -480,12 +496,12 @@ func (c S3ApiController) PutBucketAcl(ctx *fiber.Ctx) (*Response, error) {
func (c S3ApiController) CreateBucket(ctx *fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
acl := types.BucketCannedACL(ctx.Get("X-Amz-Acl"))
grantFullControl := ctx.Get("X-Amz-Grant-Full-Control")
grantRead := ctx.Get("X-Amz-Grant-Read")
grantReadACP := ctx.Get("X-Amz-Grant-Read-Acp")
grantWrite := ctx.Get("X-Amz-Grant-Write")
grantWriteACP := ctx.Get("X-Amz-Grant-Write-Acp")
acl := types.BucketCannedACL(c.getAclHeaderValue(ctx, "X-Amz-Acl"))
grantFullControl := c.getAclHeaderValue(ctx, "X-Amz-Grant-Full-Control")
grantRead := c.getAclHeaderValue(ctx, "X-Amz-Grant-Read")
grantReadACP := c.getAclHeaderValue(ctx, "X-Amz-Grant-Read-Acp")
grantWrite := c.getAclHeaderValue(ctx, "X-Amz-Grant-Write")
grantWriteACP := c.getAclHeaderValue(ctx, "X-Amz-Grant-Write-Acp")
lockEnabled := strings.EqualFold(ctx.Get("X-Amz-Bucket-Object-Lock-Enabled"), "true")
grants := grantFullControl + grantRead + grantReadACP + grantWrite + grantWriteACP
objectOwnership := types.ObjectOwnership(ctx.Get("X-Amz-Object-Ownership"))
+3
View File
@@ -52,6 +52,7 @@ func (c S3ApiController) DeleteObjectTagging(ctx *fiber.Ctx) (*Response, error)
Object: key,
Action: action,
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -104,6 +105,7 @@ func (c S3ApiController) AbortMultipartUpload(ctx *fiber.Ctx) (*Response, error)
Object: key,
Action: auth.AbortMultipartUploadAction,
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -158,6 +160,7 @@ func (c S3ApiController) DeleteObject(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: action,
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+7
View File
@@ -55,6 +55,7 @@ func (c S3ApiController) GetObjectTagging(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: action,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -121,6 +122,7 @@ func (c S3ApiController) GetObjectRetention(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.GetObjectRetentionAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -177,6 +179,7 @@ func (c S3ApiController) GetObjectLegalHold(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.GetObjectLegalHoldAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -223,6 +226,7 @@ func (c S3ApiController) GetObjectAcl(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.GetObjectAclAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -265,6 +269,7 @@ func (c S3ApiController) ListParts(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.ListMultipartUploadPartsAction,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -336,6 +341,7 @@ func (c S3ApiController) GetObjectAttributes(ctx *fiber.Ctx) (*Response, error)
Object: key,
Action: action,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -469,6 +475,7 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: action,
IsPublicRequest: isPublicBucketRequest,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+1
View File
@@ -57,6 +57,7 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: action,
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+5 -1
View File
@@ -50,6 +50,7 @@ func (c S3ApiController) RestoreObject(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.RestoreObjectAction,
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -101,6 +102,7 @@ func (c S3ApiController) SelectObjectContent(ctx *fiber.Ctx) (*Response, error)
Object: key,
Action: auth.GetObjectAction,
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -158,7 +160,7 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
err := utils.ValidateNoACLHeaders(ctx)
err := utils.ValidateNoACLHeaders(ctx, c.disableACL)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
@@ -177,6 +179,7 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
Bucket: bucket,
Object: key,
Action: auth.PutObjectAction,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -261,6 +264,7 @@ func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, err
Object: key,
Action: auth.PutObjectAction,
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+8 -2
View File
@@ -57,6 +57,7 @@ func (c S3ApiController) PutObjectTagging(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: action,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -116,6 +117,7 @@ func (c S3ApiController) PutObjectRetention(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.PutObjectRetentionAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -191,6 +193,7 @@ func (c S3ApiController) PutObjectLegalHold(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.PutObjectLegalHoldAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -269,6 +272,7 @@ func (c S3ApiController) UploadPart(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.PutObjectAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -383,6 +387,7 @@ func (c S3ApiController) UploadPartCopy(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.PutObjectAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
@@ -510,7 +515,7 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
err := utils.ValidateNoACLHeaders(ctx)
err := utils.ValidateNoACLHeaders(ctx, c.disableACL)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
@@ -668,7 +673,7 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
err := utils.ValidateNoACLHeaders(ctx)
err := utils.ValidateNoACLHeaders(ctx, c.disableACL)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
@@ -703,6 +708,7 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
Object: key,
Action: auth.PutObjectAction,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
if err != nil {
return &Response{
+498 -480
View File
File diff suppressed because it is too large Load Diff
+49 -99
View File
@@ -24,7 +24,6 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api/middlewares"
"github.com/versity/versitygw/s3err"
)
@@ -60,20 +59,13 @@ func TestS3ApiRouter_ListBuckets_DefaultCORSAllowOrigin(t *testing.T) {
origin := "https://example.com"
app := fiber.New()
(&S3ApiRouter{}).Init(
app,
backend.BackendUnsupported{},
&auth.IAMServiceInternal{},
nil,
nil,
nil,
nil,
false,
"us-east-1",
"",
middlewares.RootUserConfig{},
origin,
)
(&S3ApiRouter{
app: app,
be: backend.BackendUnsupported{},
iam: &auth.IAMServiceInternal{},
region: "us-east-1",
corsAllowOrigin: origin,
}).Init()
req, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
@@ -97,20 +89,13 @@ func TestS3ApiRouter_ListBuckets_OptionsPreflight_DefaultCORS(t *testing.T) {
origin := "https://example.com"
app := fiber.New()
(&S3ApiRouter{}).Init(
app,
backend.BackendUnsupported{},
&auth.IAMServiceInternal{},
nil,
nil,
nil,
nil,
false,
"us-east-1",
"",
middlewares.RootUserConfig{},
origin,
)
(&S3ApiRouter{
app: app,
be: backend.BackendUnsupported{},
iam: &auth.IAMServiceInternal{},
region: "us-east-1",
corsAllowOrigin: origin,
}).Init()
req, err := http.NewRequest(http.MethodOptions, "/", nil)
if err != nil {
@@ -137,20 +122,13 @@ func TestS3ApiRouter_PutBucketTagging_ErrorStillIncludesFallbackCORS(t *testing.
origin := "http://127.0.0.1:9090"
app := fiber.New()
(&S3ApiRouter{}).Init(
app,
backendWithCorsOnly{},
&auth.IAMServiceInternal{},
nil,
nil,
nil,
nil,
false,
"us-east-1",
"",
middlewares.RootUserConfig{},
origin,
)
(&S3ApiRouter{
app: app,
be: backendWithCorsOnly{},
iam: &auth.IAMServiceInternal{},
region: "us-east-1",
corsAllowOrigin: origin,
}).Init()
req, err := http.NewRequest(http.MethodPut, "/testing?tagging", nil)
if err != nil {
@@ -172,20 +150,13 @@ func TestS3ApiRouter_PutObjectTagging_ErrorStillIncludesFallbackCORS(t *testing.
origin := "http://127.0.0.1:9090"
app := fiber.New()
(&S3ApiRouter{}).Init(
app,
backendWithCorsOnly{},
&auth.IAMServiceInternal{},
nil,
nil,
nil,
nil,
false,
"us-east-1",
"",
middlewares.RootUserConfig{},
origin,
)
(&S3ApiRouter{
app: app,
be: backendWithCorsOnly{},
iam: &auth.IAMServiceInternal{},
region: "us-east-1",
corsAllowOrigin: origin,
}).Init()
req, err := http.NewRequest(http.MethodPut, "/testing/myobj?tagging", nil)
if err != nil {
@@ -207,20 +178,13 @@ func TestS3ApiRouter_CopyObject_ErrorStillIncludesFallbackCORS(t *testing.T) {
origin := "http://127.0.0.1:9090"
app := fiber.New()
(&S3ApiRouter{}).Init(
app,
backendWithCorsOnly{},
&auth.IAMServiceInternal{},
nil,
nil,
nil,
nil,
false,
"us-east-1",
"",
middlewares.RootUserConfig{},
origin,
)
(&S3ApiRouter{
app: app,
be: backendWithCorsOnly{},
iam: &auth.IAMServiceInternal{},
region: "us-east-1",
corsAllowOrigin: origin,
}).Init()
req, err := http.NewRequest(http.MethodPut, "/testing/myobj", nil)
if err != nil {
@@ -243,20 +207,13 @@ func TestS3ApiRouter_PutObject_ErrorStillIncludesFallbackCORS(t *testing.T) {
origin := "http://127.0.0.1:9090"
app := fiber.New()
(&S3ApiRouter{}).Init(
app,
backendWithCorsOnly{},
&auth.IAMServiceInternal{},
nil,
nil,
nil,
nil,
false,
"us-east-1",
"",
middlewares.RootUserConfig{},
origin,
)
(&S3ApiRouter{
app: app,
be: backendWithCorsOnly{},
iam: &auth.IAMServiceInternal{},
region: "us-east-1",
corsAllowOrigin: origin,
}).Init()
req, err := http.NewRequest(http.MethodPut, "/testing/myobj", nil)
if err != nil {
@@ -294,20 +251,13 @@ func TestS3ApiRouter_OptionsWithBucketCORS_NoDuplicateHeaders(t *testing.T) {
</CORSConfiguration>`)
app := fiber.New()
(&S3ApiRouter{}).Init(
app,
backendWithBucketCors{corsConfig: corsConfig},
&auth.IAMServiceInternal{},
nil,
nil,
nil,
nil,
false,
"us-east-1",
"",
middlewares.RootUserConfig{},
fallbackOrigin,
)
(&S3ApiRouter{
app: app,
be: backendWithBucketCors{corsConfig: corsConfig},
iam: &auth.IAMServiceInternal{},
region: "us-east-1",
corsAllowOrigin: fallbackOrigin,
}).Init()
req, err := http.NewRequest(http.MethodOptions, "/xyz/upload/test.txt", nil)
if err != nil {
+2 -10
View File
@@ -20,24 +20,16 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api/middlewares"
)
func TestS3ApiRouter_Init(t *testing.T) {
type args struct {
app *fiber.App
be backend.Backend
iam auth.IAMService
}
tests := []struct {
name string
sa *S3ApiRouter
args args
}{
{
name: "Initialize S3 api router",
sa: &S3ApiRouter{},
args: args{
sa: &S3ApiRouter{
app: fiber.New(),
be: backend.BackendUnsupported{},
iam: &auth.IAMServiceInternal{},
@@ -46,7 +38,7 @@ func TestS3ApiRouter_Init(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, nil, nil, nil, nil, false, "us-east-1", "", middlewares.RootUserConfig{}, "")
tt.sa.Init()
})
}
}
+32 -22
View File
@@ -42,18 +42,15 @@ const (
)
type S3ApiServer struct {
Router *S3ApiRouter
app *fiber.App
backend backend.Backend
CertStorage *utils.CertStorage
quiet bool
readonly bool
keepAlive bool
health string
virtualDomain string
corsAllowOrigin string
maxConnections int
maxRequests int
Router *S3ApiRouter
app *fiber.App
backend backend.Backend
CertStorage *utils.CertStorage
quiet bool
keepAlive bool
health string
maxConnections int
maxRequests int
}
func New(
@@ -69,7 +66,16 @@ func New(
) (*S3ApiServer, error) {
server := &S3ApiServer{
backend: be,
Router: new(S3ApiRouter),
Router: &S3ApiRouter{
be: be,
iam: iam,
logger: l,
aLogger: adminLogger,
evs: evs,
mm: mm,
root: root,
region: region,
},
}
for _, opt := range opts {
@@ -88,6 +94,7 @@ func New(
})
server.app = app
server.Router.app = app
// initialize the panic recovery middleware
app.Use(recover.New(
@@ -119,17 +126,12 @@ func New(
// path unescapes the url
app.Use(controllers.WrapMiddleware(middlewares.DecodeURL, l, mm))
// initialize host-style parser in virtual domain is specified
if server.virtualDomain != "" {
app.Use(middlewares.HostStyleParser(server.virtualDomain))
}
// initialize the debug logger in debug mode
if debuglogger.IsDebugEnabled() {
app.Use(middlewares.DebugLogger())
}
server.Router.Init(app, be, iam, l, adminLogger, evs, mm, server.readonly, region, server.virtualDomain, root, server.corsAllowOrigin)
server.Router.Init()
return server, nil
}
@@ -158,12 +160,14 @@ func WithHealth(health string) Option {
}
func WithReadOnly() Option {
return func(s *S3ApiServer) { s.readonly = true }
return func(s *S3ApiServer) { s.Router.readonly = true }
}
// WithHostStyle enabled host-style bucket addressing on the server
func WithHostStyle(virtualDomain string) Option {
return func(s *S3ApiServer) { s.virtualDomain = virtualDomain }
return func(s *S3ApiServer) {
s.Router.virtualDomain = virtualDomain
}
}
// WithKeepAlive enables the server keep alive
@@ -174,7 +178,7 @@ func WithKeepAlive() Option {
// WithCORSAllowOrigin sets the default CORS Access-Control-Allow-Origin value.
// This is applied when no bucket CORS configuration exists, and for admin APIs.
func WithCORSAllowOrigin(origin string) Option {
return func(s *S3ApiServer) { s.corsAllowOrigin = origin }
return func(s *S3ApiServer) { s.Router.corsAllowOrigin = origin }
}
// WithConcurrencyLimiter sets the server's maximum connection limit
@@ -186,6 +190,12 @@ func WithConcurrencyLimiter(maxConnections, maxRequests int) Option {
}
}
// WithDisableACL disables the s3 api server ACLs, by ignoring all
// bucket/object ACL headers
func WithDisableACL() Option {
return func(s *S3ApiServer) { s.Router.disableACL = true }
}
// ServeMultiPort creates listeners for multiple port specifications and serves
// on all of them simultaneously. This supports listening on multiple ports and/or
// addresses (e.g., [":7070", "localhost:8080", "0.0.0.0:9090"]).
+5 -1
View File
@@ -1011,7 +1011,11 @@ func NewTLSListener(network string, address string, getCertificateFunc func(*tls
// since ACL operations are not supported on objects, the presence of any ACL headers
// results in a NotImplemented error. It returns nil only when all ACL headers
// are absent.
func ValidateNoACLHeaders(ctx *fiber.Ctx) error {
func ValidateNoACLHeaders(ctx *fiber.Ctx, disableACL bool) error {
if disableACL {
return nil
}
for _, header := range []string{
"x-amz-acl",
"x-amz-grant-full-control",
+6
View File
@@ -193,6 +193,7 @@ const (
ErrDirectoryNotEmpty
ErrQuotaExceeded
ErrVersioningNotConfigured
ErrACLsDisabled
// Admin api errors
ErrAdminAccessDenied
@@ -867,6 +868,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Versioning has not been configured for the gateway.",
HTTPStatusCode: http.StatusNotImplemented,
},
ErrACLsDisabled: {
Code: "AccessControlListNotSupported",
Description: "Access control lists are disabled at the gateway level",
HTTPStatusCode: http.StatusBadRequest,
},
// Admin api errors
ErrAdminAccessDenied: {
+17
View File
@@ -1171,6 +1171,16 @@ func TestSignedStreaminPayloadTrailer(ts *TestState) {
}
}
func TestNoAclMode(ts *TestState) {
ts.Run(NoAclMode_CreateBucket_with_acl)
ts.Run(NoAclMode_PutObject_with_acl)
ts.Run(NoAclMode_CopyObject_with_acl)
ts.Run(NoAclMode_multipart_upload_with_acl)
ts.Run(NoAclMode_PutBucketAcl)
ts.Run(NoAclMode_PutObjectAcl_not_implemented)
ts.Run(NoAclMode_GetObjectAcl_not_implemented)
}
type IntTest func(s3 *S3Conf) error
type IntTests map[string]IntTest
@@ -1867,5 +1877,12 @@ func GetIntTests() IntTests {
"SignedStreamingPayloadTrailer_invalid_checksum": SignedStreamingPayloadTrailer_invalid_checksum,
"SignedStreamingPayloadTrailer_bad_digest": SignedStreamingPayloadTrailer_bad_digest,
"SignedStreamingPayloadTrailer_success": SignedStreamingPayloadTrailer_success,
"NoAclMode_CreateBucket_with_acl": NoAclMode_CreateBucket_with_acl,
"NoAclMode_PutObject_with_acl": NoAclMode_PutObject_with_acl,
"NoAclMode_CopyObject_with_acl": NoAclMode_CopyObject_with_acl,
"NoAclMode_multipart_upload_with_acl": NoAclMode_multipart_upload_with_acl,
"NoAclMode_PutBucketAcl": NoAclMode_PutBucketAcl,
"NoAclMode_PutObjectAcl_not_implemented": NoAclMode_PutObjectAcl_not_implemented,
"NoAclMode_GetObjectAcl_not_implemented": NoAclMode_GetObjectAcl_not_implemented,
}
}
+245
View File
@@ -0,0 +1,245 @@
// Copyright 2026 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 integration
import (
"context"
"fmt"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
func NoAclMode_CreateBucket_with_acl(s *S3Conf) error {
testName := "NoAclMode_CreateBucket_with_acl"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
u := getUser("user")
err := createUsers(s, []user{u})
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
ACL: types.BucketCannedACLPublicReadWrite,
GrantFullControl: &u.access,
GrantRead: &u.access,
GrantReadACP: &u.access,
GrantWrite: &u.access,
GrantWriteACP: &u.access,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetBucketAcl(ctx, &s3.GetBucketAclInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if getString(out.Owner.ID) != s.awsID {
return fmt.Errorf("expected bucket owner to be %v, instead got %v",
s.awsID, getString(out.Owner.ID))
}
if len(out.Grants) != 1 {
return fmt.Errorf("expected grants length to be 1, instead got %v",
len(out.Grants))
}
grt := out.Grants[0]
if grt.Permission != types.PermissionFullControl {
return fmt.Errorf("expected the grantee to have full-control permission, instead got %v",
grt.Permission)
}
if getString(grt.Grantee.ID) != s.awsID {
return fmt.Errorf("expected the grantee id to be %v, instead got %v",
s.awsID, getString(grt.Grantee.ID))
}
return teardown(s, bucket)
})
}
func NoAclMode_PutObject_with_acl(s *S3Conf) error {
testName := "NoAclMode_PutObject_with_acl"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-object"
u := getUser("user")
err := createUsers(s, []user{u})
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ACL: types.ObjectCannedACLBucketOwnerFullControl,
GrantFullControl: &u.access,
GrantRead: &u.access,
GrantReadACP: &u.access,
GrantWriteACP: &u.access,
Body: strings.NewReader("dummy data"),
})
cancel()
return err
})
}
func NoAclMode_CopyObject_with_acl(s *S3Conf) error {
testName := "NoAclMode_CopyObject_with_acl"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
u := getUser("user")
err := createUsers(s, []user{u})
if err != nil {
return err
}
srcObj, dstObj := "source-object", "destination-object"
_, err = putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
ACL: types.ObjectCannedACLAuthenticatedRead,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
})
cancel()
return err
})
}
func NoAclMode_multipart_upload_with_acl(s *S3Conf) error {
testName := "NoAclMode_CreateMultipartUpload_with_acl"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-object"
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
mp, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ACL: types.ObjectCannedACLAuthenticatedRead,
GrantFullControl: getPtr("non_existing_user_1"),
GrantRead: getPtr("non_existing_user_2"),
GrantReadACP: getPtr("non_existing_user_3"),
GrantWriteACP: getPtr("non_existing_user_4"),
})
cancel()
if err != nil {
return err
}
parts, _, err := uploadParts(s3client, 100, 1, bucket, obj, *mp.UploadId)
if err != nil {
return err
}
compParts := []types.CompletedPart{}
for _, el := range parts {
compParts = append(compParts, types.CompletedPart{
ETag: el.ETag,
PartNumber: el.PartNumber,
})
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
UploadId: mp.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: compParts,
},
})
cancel()
return err
})
}
func NoAclMode_PutBucketAcl(s *S3Conf) error {
testName := "NoAclMode_PutBucketAcl"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketAcl(ctx, &s3.PutBucketAclInput{
Bucket: &bucket,
ACL: types.BucketCannedACLPrivate,
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrACLsDisabled))
})
}
func NoAclMode_PutObjectAcl_not_implemented(s *S3Conf) error {
testName := "NoAclMode_PutObjectAcl_not_implemented"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-object"
_, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectAcl(ctx, &s3.PutObjectAclInput{
Bucket: &bucket,
Key: &obj,
ACL: types.ObjectCannedACLAuthenticatedRead,
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrNotImplemented))
})
}
func NoAclMode_GetObjectAcl_not_implemented(s *S3Conf) error {
testName := "NoAclMode_GetObjectAcl_not_implemented"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-object"
_, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectAcl(ctx, &s3.GetObjectAclInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrNotImplemented))
})
}