mirror of
https://github.com/versity/versitygw.git
synced 2026-08-29 04:06:56 +00:00
Gates bucket listing behind an identity policy, lets browsers reach the standalone IAM API, and turns the WebUI into a dashboard for S3, IAM, or both.
**Bucket listing.** `ListBuckets` is now gated by the new `s3:ListAllMyBuckets` action, evaluated against `arn:aws:s3:::*`. The request names no bucket, so only identity policies apply — there is no resource policy to combine with, which is the same shape `CreateBucket` already had, so both now share one identity-only evaluation path. Root and admin bypass it, and backends with no identity-policy layer keep listing as before since their listing is already narrowed to the caller's own buckets. The action is IAM-only and is deliberately absent from the bucket-policy action list.
**Fixed bucket ownership.** The standalone IAM client has no per-user ownership to express — accounts are all plain users, cannot be enumerated, and access is decided by policy rather than ACL — so it now implements `auth.FixedBucketOwner` and every bucket is owned by root. Bucket creation stops resolving an owner, `ListBuckets` returns every bucket to every caller (what they may then do with one stays a per-request policy decision), and the admin `ChangeBucketOwner` reports method-not-supported. Other IAM backends are untouched.
**IAM service CORS.** `--cors-allow-origin` now applies to the `iam` command: it answers preflights and stamps the CORS headers, mirroring back the requested method and headers rather than enumerating the SigV4 header set. Without it no browser can reach the IAM API at all, so setting `--webui` without it falls back to `*` with a warning. The chart gets `iamServer.corsAllowOrigin`.
**WebUI.** New IAM pages for users, roles and OIDC providers, signing IAM/STS query-form requests directly from the browser. Navigation is capability-gated rather than role-gated: on sign-in the session probes the S3, admin and IAM endpoints independently and each page shows only what those credentials actually reach, so one build serves an IAM-only dashboard, an S3-only dashboard, and a combined one. The login page takes an optional IAM endpoint, seeded from the new `--webui-iam-gateways` (chart: `webui.iamGateways`) — never auto-detected, since the IAM service is a separate process. The WebUI can also be hosted by `versitygw iam` itself, for deployments with no S3 gateway behind it.
**The admin API is ignored once an IAM endpoint is in play.** The IAM service is then the user directory and bucket ownership is fixed, which leaves the admin API no job: the session is given no admin endpoint at all, its login field is hidden, `users.html` redirects to its IAM counterpart, and every admin-only surface stays off screen. Dashboard and Buckets remain available to any S3 session in such a deployment, running on the S3 and IAM APIs alone and surfacing each denial per action instead of redirecting.
Also fixes two WebUI bugs: embedded assets went out with a zero modification time and no `Cache-Control`, so browsers treated them as fresh for centuries and an upgraded gateway served new HTML against stale JS — they now revalidate against an ETag; and the login page's advanced-options section clipped its last field, since it animated to a height named in the stylesheet rather than the one it measures now.
**Usage**
IAM-only dashboard, served by the IAM service:
versitygw iam --port :7076 --webui :8080 --cors-allow-origin http://localhost:8080/
IAM + S3, dashboard served by the IAM service — point it at the gateway with `--webui-gateways`, and let the gateway accept the dashboard's origin:
versitygw iam --port :7076 --webui :8080 --webui-gateways http://localhost:7070/ --cors-allow-origin http://localhost:8080/
versitygw --port :7070 --cors-allow-origin http://localhost:8080/ posix /data
IAM + S3, dashboard served by the S3 gateway — point it at the IAM service with `--webui-iam-gateways`, and let the IAM service accept the dashboard's origin:
versitygw --port :7070 --webui :8080 --webui-iam-gateways http://localhost:7076/ posix /data
versitygw iam --port :7076 --cors-allow-origin http://localhost:8080/
726 lines
21 KiB
Go
726 lines
21 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"
|
|
"errors"
|
|
"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/s3response"
|
|
)
|
|
|
|
func (c S3ApiController) PutBucketTagging(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
|
|
|
|
err := c.verifyAccess(ctx, auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Actions: []auth.Action{auth.PutBucketTaggingAction},
|
|
IsPublicRequest: isPublicBucket,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
tagging, err := utils.ParseTagging(ctx.BodyRaw(), utils.TagLimitBucket)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.PutBucketTagging(ctx.RequestCtx(), bucket, tagging)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
Status: http.StatusNoContent,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) PutBucketOwnershipControls(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
|
|
if err := c.verifyAccess(ctx, auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Actions: []auth.Action{auth.PutBucketOwnershipControlsAction},
|
|
}); err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
var ownershipControls s3response.OwnershipControls
|
|
if err := xml.Unmarshal(ctx.BodyRaw(), &ownershipControls); err != nil {
|
|
debuglogger.Logf("failed to unmarshal request body: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
rulesCount := len(ownershipControls.Rules)
|
|
if rulesCount != 1 {
|
|
debuglogger.Logf("ownership control rules should be 1, got %v", rulesCount)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if !utils.IsValidOwnership(ownershipControls.Rules[0].ObjectOwnership) {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
err := c.be.PutBucketOwnershipControls(ctx.RequestCtx(), bucket, ownershipControls.Rules[0].ObjectOwnership)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) PutBucketVersioning(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
|
|
|
|
err := c.verifyAccess(ctx, auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Actions: []auth.Action{auth.PutBucketVersioningAction},
|
|
IsPublicRequest: isPublicBucket,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
var versioningConf types.VersioningConfiguration
|
|
err = xml.Unmarshal(ctx.BodyRaw(), &versioningConf)
|
|
if err != nil {
|
|
debuglogger.Logf("error unmarshalling versioning configuration: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrInvalidRequest)
|
|
}
|
|
|
|
if versioningConf.Status != types.BucketVersioningStatusEnabled &&
|
|
versioningConf.Status != types.BucketVersioningStatusSuspended {
|
|
debuglogger.Logf("invalid versioning configuration status: %v", versioningConf.Status)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
err = c.be.PutBucketVersioning(ctx.RequestCtx(), bucket, versioningConf.Status)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) PutObjectLockConfiguration(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
|
|
|
|
if err := c.verifyAccess(ctx, auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Actions: []auth.Action{auth.PutBucketObjectLockConfigurationAction},
|
|
IsPublicRequest: isPublicBucket,
|
|
}); err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
config, err := auth.ParseBucketLockConfigurationInput(ctx.BodyRaw())
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.PutObjectLockConfiguration(ctx.RequestCtx(), bucket, config)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) PutBucketCors(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
|
|
|
|
err := c.verifyAccess(ctx, auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Actions: []auth.Action{auth.PutBucketCorsAction},
|
|
IsPublicRequest: isPublicBucket,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
body := ctx.BodyRaw()
|
|
|
|
var corsConfig auth.CORSConfiguration
|
|
err = xml.Unmarshal(body, &corsConfig)
|
|
if err != nil {
|
|
debuglogger.Logf("invalid CORS request body: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
// validate the CORS configuration rules
|
|
err = corsConfig.Validate()
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.PutBucketCors(ctx.RequestCtx(), bucket, body)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) PutBucketWebsite(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
|
|
|
|
err := c.verifyAccess(ctx, auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Actions: []auth.Action{auth.PutBucketWebsiteAction},
|
|
IsPublicRequest: isPublicBucket,
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
body := ctx.BodyRaw()
|
|
if len(body) > maxWebsiteConfigurationBytes {
|
|
debuglogger.Logf("the request size exceeded the 128KB limit: %d", len(body))
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetMaxMessageLengthExceeded(maxWebsiteConfigurationBytes)
|
|
}
|
|
|
|
var websiteConfig s3response.WebsiteConfiguration
|
|
err = xml.Unmarshal(body, &websiteConfig)
|
|
if err != nil {
|
|
debuglogger.Logf("invalid website config request body: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
err = websiteConfig.Validate()
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.PutBucketWebsite(ctx.RequestCtx(), bucket, body)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) PutBucketPolicy(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
|
|
err := c.verifyAccess(ctx, auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWrite,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Actions: []auth.Action{auth.PutBucketPolicyAction},
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = auth.ValidatePolicyDocument(ctx.BodyRaw(), bucket, c.iam)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.PutBucketPolicy(ctx.RequestCtx(), bucket, ctx.BodyRaw())
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
Status: http.StatusNoContent,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) PutBucketAcl(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")
|
|
// context locals
|
|
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
|
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
|
|
grants := grantFullControl + grantRead + grantReadACP + grantWrite + grantWriteACP
|
|
var input *auth.PutBucketAclInput
|
|
|
|
err := c.verifyAccess(ctx,
|
|
auth.AccessOptions{
|
|
Acl: parsedAcl,
|
|
AclPermission: auth.PermissionWriteAcp,
|
|
IsRoot: isRoot,
|
|
Acc: acct,
|
|
Bucket: bucket,
|
|
Actions: []auth.Action{auth.PutBucketAclAction},
|
|
})
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, 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{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
ownership, err := c.be.GetBucketOwnershipControls(ctx.RequestCtx(), bucket)
|
|
if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)) {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
if ownership == types.ObjectOwnershipBucketOwnerEnforced {
|
|
debuglogger.Logf("bucket acls are disabled")
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrAclNotSupported)
|
|
}
|
|
|
|
if len(ctx.BodyRaw()) > 0 {
|
|
var accessControlPolicy auth.AccessControlPolicy
|
|
err := xml.Unmarshal(ctx.BodyRaw(), &accessControlPolicy)
|
|
if err != nil {
|
|
debuglogger.Logf("error unmarshalling access control policy: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedACL)
|
|
}
|
|
|
|
err = accessControlPolicy.Validate()
|
|
if err != nil {
|
|
debuglogger.Logf("invalid access control policy: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
if *accessControlPolicy.Owner.ID != parsedAcl.Owner {
|
|
debuglogger.Logf("invalid access control policy owner id: %v, expected %v", *accessControlPolicy.Owner.ID, parsedAcl.Owner)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.APIError{
|
|
Code: "InvalidArgument",
|
|
Description: "Invalid id",
|
|
HTTPStatusCode: http.StatusBadRequest,
|
|
}
|
|
}
|
|
|
|
if grants+string(acl) != "" {
|
|
debuglogger.Logf("invalid request: %q (grants) %q (acl)",
|
|
grants, acl)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrUnexpectedContent)
|
|
}
|
|
|
|
input = &auth.PutBucketAclInput{
|
|
Bucket: &bucket,
|
|
AccessControlPolicy: &accessControlPolicy,
|
|
}
|
|
} else if acl != "" {
|
|
if grants != "" {
|
|
debuglogger.Logf("invalid request: %q (grants) %q (acl)",
|
|
grants, acl)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants)
|
|
}
|
|
|
|
input = &auth.PutBucketAclInput{
|
|
Bucket: &bucket,
|
|
ACL: types.BucketCannedACL(acl),
|
|
}
|
|
} else if grants != "" {
|
|
input = &auth.PutBucketAclInput{
|
|
Bucket: &bucket,
|
|
GrantFullControl: &grantFullControl,
|
|
GrantRead: &grantRead,
|
|
GrantReadACP: &grantReadACP,
|
|
GrantWrite: &grantWrite,
|
|
GrantWriteACP: &grantWriteACP,
|
|
}
|
|
} else {
|
|
debuglogger.Logf("none of the bucket acl options has been specified: canned, req headers, req body")
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMissingSecurityHeader)
|
|
}
|
|
|
|
updAcl, err := auth.UpdateACL(input, parsedAcl, c.iam)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.PutBucketAcl(ctx.RequestCtx(), bucket, updAcl)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: parsedAcl.Owner,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
func (c S3ApiController) CreateBucket(ctx fiber.Ctx) (*Response, error) {
|
|
bucket := ctx.Params("bucket")
|
|
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"))
|
|
|
|
if c.readonly {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{},
|
|
}, s3err.GetAPIError(s3err.ErrAccessDenied)
|
|
}
|
|
|
|
creator := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
|
// A backend that fixes bucket ownership picks the owner itself; otherwise
|
|
// it is whoever the admin API named, defaulting to the creator. The
|
|
// context is set either way: the storage backend reads the owner back out
|
|
// of it to chown the new bucket.
|
|
bucketOwner, fixedOwner := auth.ResolveFixedBucketOwner(c.iam)
|
|
if !fixedOwner {
|
|
bucketOwner = creator
|
|
if utils.ContextKeyBucketOwner.IsSet(ctx) {
|
|
bucketOwner = utils.ContextKeyBucketOwner.Get(ctx).(auth.Account)
|
|
}
|
|
}
|
|
utils.ContextKeyBucketOwner.Set(ctx, bucketOwner)
|
|
isRoot, _ := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
|
|
|
if err := auth.VerifyCreateBucketAccess(ctx, c.iam, isRoot, creator, bucket); err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{},
|
|
}, err
|
|
}
|
|
|
|
// validate the bucket name
|
|
if ok := utils.IsValidBucketName(bucket); !ok {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
// both bucket canned ACL and acl grants is not allowed
|
|
if acl != "" && grants != "" {
|
|
debuglogger.Logf("invalid request: %q (grants) %q (acl)", grants, acl)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants)
|
|
}
|
|
|
|
// validate bucket canned acl
|
|
err := auth.ValidateCannedACL(acl)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
// if bucket acl is 'private', object ownership should default to 'BucketOwnerPreferred'
|
|
if acl == types.BucketCannedACLPrivate && objectOwnership == "" {
|
|
objectOwnership = types.ObjectOwnershipBucketOwnerPreferred
|
|
}
|
|
|
|
// if object ownership is so far empty, it should default to BucketOwnerEnforced
|
|
if objectOwnership == "" {
|
|
objectOwnership = types.ObjectOwnershipBucketOwnerEnforced
|
|
}
|
|
|
|
// validate the object ownership value
|
|
if ok := utils.IsValidOwnership(objectOwnership); !ok {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, s3err.GetInvalidArgObjectOwnership(string(objectOwnership))
|
|
}
|
|
|
|
// any bucket ACL(canned, grants) is not allowed with object ownership 'BucketOwnerEnforced'
|
|
// but there's an exception for 'private' bucket canned ACL,
|
|
// which is the effective default for all buckets. In this case
|
|
// the ACL(private canned) is allowed with 'BucketOwnerEnforced'
|
|
if acl != types.BucketCannedACLPrivate && string(acl)+grants != "" && objectOwnership == types.ObjectOwnershipBucketOwnerEnforced {
|
|
debuglogger.Logf("bucket acls are disabled for %v object ownership", objectOwnership)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrInvalidBucketAclWithObjectOwnership)
|
|
}
|
|
|
|
var body s3response.CreateBucketConfiguration
|
|
if len(ctx.BodyRaw()) != 0 {
|
|
// request body is optional for CreateBucket
|
|
err := xml.Unmarshal(ctx.BodyRaw(), &body)
|
|
if err != nil {
|
|
debuglogger.Logf("failed to parse the request body: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if body.LocationConstraint != nil {
|
|
region := utils.ContextKeyRegion.Get(ctx).(string)
|
|
if *body.LocationConstraint != region || *body.LocationConstraint == "us-east-1" {
|
|
debuglogger.Logf("invalid location constraint: %s", *body.LocationConstraint)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, s3err.GetInvalidLocationConstraintErr(*body.LocationConstraint)
|
|
}
|
|
}
|
|
}
|
|
|
|
defACL := auth.ACL{
|
|
Owner: bucketOwner.Access,
|
|
}
|
|
|
|
updAcl, err := auth.UpdateACL(&auth.PutBucketAclInput{
|
|
GrantFullControl: &grantFullControl,
|
|
GrantRead: &grantRead,
|
|
GrantReadACP: &grantReadACP,
|
|
GrantWrite: &grantWrite,
|
|
GrantWriteACP: &grantWriteACP,
|
|
AccessControlPolicy: &auth.AccessControlPolicy{
|
|
Owner: &types.Owner{
|
|
ID: &bucketOwner.Access,
|
|
}},
|
|
ACL: types.BucketCannedACL(acl),
|
|
}, defACL, c.iam)
|
|
if err != nil {
|
|
debuglogger.Logf("failed to update bucket acl: %v", err)
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
err = c.be.CreateBucket(ctx.RequestCtx(), &s3.CreateBucketInput{
|
|
Bucket: &bucket,
|
|
ObjectOwnership: objectOwnership,
|
|
ObjectLockEnabledForBucket: &lockEnabled,
|
|
CreateBucketConfiguration: &types.CreateBucketConfiguration{
|
|
Tags: body.TagSet,
|
|
},
|
|
}, updAcl)
|
|
if err != nil {
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
}, err
|
|
}
|
|
|
|
return &Response{
|
|
MetaOpts: &MetaOptions{
|
|
BucketOwner: bucketOwner.Access,
|
|
},
|
|
Headers: map[string]*string{
|
|
"Location": utils.GetStringPtr("/" + bucket),
|
|
"x-amz-bucket-arn": utils.GetStringPtr(auth.ResourceArnPrefix + bucket),
|
|
},
|
|
}, nil
|
|
}
|