mirror of
https://github.com/versity/versitygw.git
synced 2026-07-26 01:52:40 +00:00
Merge pull request #1504 from versity/ben/bucket-location
feat: add get bucket location frontend handlers
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