mirror of
https://github.com/versity/versitygw.git
synced 2025-12-23 05:05:16 +00:00
feat: add get bucket location frontend handlers
GetBucketLocation is being deprecated by AWS, but is still used by some clients. We don't need any backend handlers for this since the region is managed by the frontend. All we need is to test for bucket existence, so we can use HeadBucket for this. Fixes #1499
This commit is contained in:
@@ -87,6 +87,7 @@ const (
|
||||
PutBucketWebsiteAction Action = "s3:PutBucketWebsite"
|
||||
GetBucketWebsiteAction Action = "s3:GetBucketWebsite"
|
||||
GetBucketPolicyStatusAction Action = "s3:GetBucketPolicyStatus"
|
||||
GetBucketLocationAction Action = "s3:GetBucketLocation"
|
||||
|
||||
AllActions Action = "s3:*"
|
||||
)
|
||||
@@ -157,6 +158,7 @@ var supportedActionList = map[Action]struct{}{
|
||||
PutBucketWebsiteAction: {},
|
||||
GetBucketWebsiteAction: {},
|
||||
GetBucketPolicyStatusAction: {},
|
||||
GetBucketLocationAction: {},
|
||||
AllActions: {},
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ var (
|
||||
ActionGetBucketWebsite = "s3_GetBucketWebsite"
|
||||
ActionDeleteBucketWebsite = "s3_DeleteBucketWebsite"
|
||||
ActionGetBucketPolicyStatus = "s3_GetBucketPolicyStatus"
|
||||
ActionGetBucketLocation = "s3_GetBucketLocation"
|
||||
|
||||
// Admin actions
|
||||
ActionAdminCreateUser = "admin_CreateUser"
|
||||
@@ -498,4 +499,8 @@ func init() {
|
||||
Name: "GetBucketPolicyStatus",
|
||||
Service: "s3",
|
||||
}
|
||||
ActionMap[ActionGetBucketLocation] = Action{
|
||||
Name: "GetBucketLocation",
|
||||
Service: "s3",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ var (
|
||||
Access: "root",
|
||||
Role: auth.RoleAdmin,
|
||||
},
|
||||
utils.ContextKeyRegion: "us-east-1",
|
||||
}
|
||||
|
||||
accessDeniedLocals map[utils.ContextKey]any = map[utils.ContextKey]any{
|
||||
|
||||
@@ -619,3 +619,52 @@ func (c S3ApiController) ListObjects(ctx *fiber.Ctx) (*Response, error) {
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
// GetBucketLocation handles GET /:bucket?location
|
||||
func (c S3ApiController) GetBucketLocation(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionRead,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Action: auth.GetBucketLocationAction,
|
||||
IsBucketPublic: isPublicBucket,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
// verify bucket existence/access via backend HeadBucket
|
||||
_, err = c.be.HeadBucket(ctx.Context(), &s3.HeadBucketInput{Bucket: &bucket})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
// pick up configured region from locals (set by router middleware)
|
||||
region, _ := ctx.Locals("region").(string)
|
||||
|
||||
return &Response{
|
||||
Data: s3response.LocationConstraint{
|
||||
Value: region,
|
||||
},
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1266,3 +1266,83 @@ func TestS3ApiController_ListObjects(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiController_GetBucketLocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input testInput
|
||||
output testOutput
|
||||
}{
|
||||
{
|
||||
name: "verify access fails",
|
||||
input: testInput{
|
||||
locals: accessDeniedLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrAccessDenied),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend returns error",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful response",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
Data: s3response.LocationConstraint{
|
||||
Value: "us-east-1",
|
||||
},
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
be := &BackendMock{
|
||||
HeadBucketFunc: func(contextMoqParam context.Context, headBucketInput *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
|
||||
return &s3.HeadBucketOutput{}, tt.input.beErr
|
||||
},
|
||||
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := S3ApiController{
|
||||
be: be,
|
||||
}
|
||||
|
||||
testController(
|
||||
t,
|
||||
ctrl.GetBucketLocation,
|
||||
tt.output.response,
|
||||
tt.output.err,
|
||||
ctxInputs{
|
||||
locals: tt.input.locals,
|
||||
body: tt.input.body,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,6 +611,21 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
))
|
||||
|
||||
// GET bucket operations
|
||||
bucketRouter.Get("",
|
||||
middlewares.MatchQueryArgs("location"),
|
||||
controllers.ProcessHandlers(
|
||||
ctrl.GetBucketLocation,
|
||||
metrics.ActionGetBucketLocation,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketLocation, auth.GetBucketLocationAction, auth.PermissionRead),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
|
||||
middlewares.VerifyV4Signature(root, iam, region, debug),
|
||||
middlewares.VerifyMD5Body(),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ParseAcl(be),
|
||||
),
|
||||
)
|
||||
bucketRouter.Get("",
|
||||
middlewares.MatchQueryArgs("tagging"),
|
||||
controllers.ProcessHandlers(
|
||||
|
||||
@@ -721,3 +721,9 @@ type Checksum struct {
|
||||
SHA256 *string
|
||||
CRC64NVME *string
|
||||
}
|
||||
|
||||
// LocationConstraint represents the GetBucketLocation response
|
||||
type LocationConstraint struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LocationConstraint"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
@@ -135,6 +135,12 @@ func TestDeleteBucketTagging(s *S3Conf) {
|
||||
DeleteBucketTagging_success(s)
|
||||
}
|
||||
|
||||
func TestGetBucketLocation(s *S3Conf) {
|
||||
GetBucketLocation_success(s)
|
||||
GetBucketLocation_non_exist(s)
|
||||
GetBucketLocation_no_access(s)
|
||||
}
|
||||
|
||||
func TestPutObject(s *S3Conf) {
|
||||
PutObject_non_existing_bucket(s)
|
||||
PutObject_special_chars(s)
|
||||
@@ -708,6 +714,7 @@ func TestFullFlow(s *S3Conf) {
|
||||
TestPutBucketTagging(s)
|
||||
TestGetBucketTagging(s)
|
||||
TestDeleteBucketTagging(s)
|
||||
TestGetBucketLocation(s)
|
||||
TestPutObject(s)
|
||||
TestHeadObject(s)
|
||||
TestGetObjectAttributes(s)
|
||||
@@ -791,6 +798,7 @@ func TestScoutfs(s *S3Conf) {
|
||||
TestPutBucketTagging(s)
|
||||
TestGetBucketTagging(s)
|
||||
TestDeleteBucketTagging(s)
|
||||
TestGetBucketLocation(s)
|
||||
TestPutObject(s)
|
||||
TestHeadObject(s)
|
||||
TestGetObjectAttributes(s)
|
||||
@@ -1092,6 +1100,9 @@ func GetIntTests() IntTests {
|
||||
"DeleteBucketTagging_non_existing_object": DeleteBucketTagging_non_existing_object,
|
||||
"DeleteBucketTagging_success_status": DeleteBucketTagging_success_status,
|
||||
"DeleteBucketTagging_success": DeleteBucketTagging_success,
|
||||
"GetBucketLocation_success": GetBucketLocation_success,
|
||||
"GetBucketLocation_non_exist": GetBucketLocation_non_exist,
|
||||
"GetBucketLocation_no_access": GetBucketLocation_no_access,
|
||||
"PutObject_non_existing_bucket": PutObject_non_existing_bucket,
|
||||
"PutObject_special_chars": PutObject_special_chars,
|
||||
"PutObject_tagging": PutObject_tagging,
|
||||
|
||||
@@ -1728,6 +1728,77 @@ func HeadBucket_success(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func GetBucketLocation_success(s *S3Conf) error {
|
||||
testName := "GetBucketLocation_success"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
resp, err := s3client.GetBucketLocation(ctx, &s3.GetBucketLocationInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(resp.LocationConstraint) != s.awsRegion {
|
||||
return fmt.Errorf("expected bucket region to be %v, instead got %v",
|
||||
s.awsRegion, resp.LocationConstraint)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetBucketLocation_non_exist(s *S3Conf) error {
|
||||
testName := "GetBucketLocation_non_exist"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
invalidBucket := "bucket-no-exist"
|
||||
resp, err := s3client.GetBucketLocation(ctx, &s3.GetBucketLocationInput{
|
||||
Bucket: &invalidBucket,
|
||||
})
|
||||
cancel()
|
||||
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp != nil && resp.LocationConstraint != "" {
|
||||
return fmt.Errorf("expected empty location constraint, instead got %v",
|
||||
resp.LocationConstraint)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetBucketLocation_no_access(s *S3Conf) error {
|
||||
testName := "GetBucketLocation_no_access"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
err := createUsers(s, []user{testuser1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userClient := s.getUserClient(testuser1)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
resp, err := userClient.GetBucketLocation(ctx, &s3.GetBucketLocationInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp != nil && resp.LocationConstraint != "" {
|
||||
return fmt.Errorf("expected empty location constraint, instead got %v",
|
||||
resp.LocationConstraint)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListBuckets_as_user(s *S3Conf) error {
|
||||
testName := "ListBuckets_as_user"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
Reference in New Issue
Block a user