forked from TrueCloudLab/frostfs-s3-gw
Denis Kirillov
465eaa816a
Always consider buckets as APE compatible Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
417 lines
14 KiB
Go
417 lines
14 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
|
s3errors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
|
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPutObjectACLErrorAPE(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
bktName, objName := "bucket-for-acl-ape", "object"
|
|
|
|
info := createBucket(hc, bktName)
|
|
|
|
putObjectWithHeadersAssertS3Error(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, s3errors.ErrAccessControlListNotSupported)
|
|
putObjectWithHeaders(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate}) // only `private` canned acl is allowed, that is actually ignored
|
|
putObjectWithHeaders(hc, bktName, objName, nil)
|
|
|
|
aclBody := &AccessControlPolicy{}
|
|
putObjectACLAssertS3Error(hc, bktName, objName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported)
|
|
|
|
aclRes := getObjectACL(hc, bktName, objName)
|
|
checkPrivateACL(t, aclRes, info.Key.PublicKey())
|
|
}
|
|
|
|
func TestCreateObjectACLErrorAPE(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
bktName, objName, objNameCopy := "bucket-for-acl-ape", "object", "copy"
|
|
|
|
createBucket(hc, bktName)
|
|
|
|
putObject(hc, bktName, objName)
|
|
copyObject(hc, bktName, objName, objNameCopy, CopyMeta{Headers: map[string]string{api.AmzACL: basicACLPublic}}, http.StatusBadRequest)
|
|
copyObject(hc, bktName, objName, objNameCopy, CopyMeta{Headers: map[string]string{api.AmzACL: basicACLPrivate}}, http.StatusOK)
|
|
|
|
createMultipartUploadAssertS3Error(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, s3errors.ErrAccessControlListNotSupported)
|
|
createMultipartUpload(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate})
|
|
}
|
|
|
|
func TestBucketACLAPE(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
bktName := "bucket-for-acl-ape"
|
|
|
|
info := createBucket(hc, bktName)
|
|
|
|
aclBody := &AccessControlPolicy{}
|
|
putBucketACLAssertS3Error(hc, bktName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported)
|
|
|
|
aclRes := getBucketACL(hc, bktName)
|
|
checkPrivateACL(t, aclRes, info.Key.PublicKey())
|
|
|
|
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLPrivate})
|
|
aclRes = getBucketACL(hc, bktName)
|
|
checkPrivateACL(t, aclRes, info.Key.PublicKey())
|
|
|
|
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLReadOnly})
|
|
aclRes = getBucketACL(hc, bktName)
|
|
checkPublicReadACL(t, aclRes, info.Key.PublicKey())
|
|
|
|
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLPublic})
|
|
aclRes = getBucketACL(hc, bktName)
|
|
checkPublicReadWriteACL(t, aclRes, info.Key.PublicKey())
|
|
}
|
|
|
|
func checkPrivateACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
|
checkACLOwner(t, aclRes, ownerKey)
|
|
}
|
|
|
|
func checkPublicReadACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
|
checkACLOwner(t, aclRes, ownerKey)
|
|
|
|
require.Equal(t, allUsersGroup, aclRes.AccessControlList[0].Grantee.URI)
|
|
require.Equal(t, aclRead, aclRes.AccessControlList[0].Permission)
|
|
}
|
|
|
|
func checkPublicReadWriteACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
|
checkACLOwner(t, aclRes, ownerKey)
|
|
|
|
require.Equal(t, allUsersGroup, aclRes.AccessControlList[0].Grantee.URI)
|
|
require.Equal(t, aclWrite, aclRes.AccessControlList[0].Permission)
|
|
|
|
require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI)
|
|
require.Equal(t, aclRead, aclRes.AccessControlList[1].Permission)
|
|
}
|
|
|
|
func checkACLOwner(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
|
ownerIDStr := hex.EncodeToString(ownerKey.Bytes())
|
|
ownerNameStr := ownerKey.Address()
|
|
|
|
require.Equal(t, ownerIDStr, aclRes.Owner.ID)
|
|
require.Equal(t, ownerNameStr, aclRes.Owner.DisplayName)
|
|
}
|
|
|
|
func TestBucketPolicy(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
bktName := "bucket-for-policy"
|
|
|
|
createTestBucket(hc, bktName)
|
|
|
|
getBucketPolicy(hc, bktName, s3errors.ErrNoSuchBucketPolicy)
|
|
|
|
newPolicy := engineiam.Policy{
|
|
Version: "2012-10-17",
|
|
Statement: []engineiam.Statement{{
|
|
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
|
|
Effect: engineiam.DenyEffect,
|
|
Action: engineiam.Action{"s3:PutObject"},
|
|
Resource: engineiam.Resource{"arn:aws:s3:::test/*"},
|
|
}},
|
|
}
|
|
|
|
putBucketPolicy(hc, bktName, newPolicy, s3errors.ErrMalformedPolicy)
|
|
|
|
newPolicy.Statement[0].Resource[0] = arnAwsPrefix + bktName + "/*"
|
|
putBucketPolicy(hc, bktName, newPolicy)
|
|
|
|
bktPolicy := getBucketPolicy(hc, bktName)
|
|
require.Equal(t, newPolicy, bktPolicy)
|
|
}
|
|
|
|
func TestBucketPolicyStatus(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
bktName := "bucket-for-policy"
|
|
|
|
createTestBucket(hc, bktName)
|
|
|
|
getBucketPolicy(hc, bktName, s3errors.ErrNoSuchBucketPolicy)
|
|
|
|
newPolicy := engineiam.Policy{
|
|
Version: "2012-10-17",
|
|
Statement: []engineiam.Statement{{
|
|
NotPrincipal: engineiam.Principal{engineiam.Wildcard: {}},
|
|
Effect: engineiam.AllowEffect,
|
|
Action: engineiam.Action{"s3:PutObject"},
|
|
Resource: engineiam.Resource{arnAwsPrefix + bktName + "/*"},
|
|
}},
|
|
}
|
|
|
|
putBucketPolicy(hc, bktName, newPolicy, s3errors.ErrMalformedPolicyNotPrincipal)
|
|
|
|
newPolicy.Statement[0].NotPrincipal = nil
|
|
newPolicy.Statement[0].Principal = map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}}
|
|
putBucketPolicy(hc, bktName, newPolicy)
|
|
bktPolicyStatus := getBucketPolicyStatus(hc, bktName)
|
|
require.True(t, PolicyStatusIsPublicTrue == bktPolicyStatus.IsPublic)
|
|
|
|
key, err := keys.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
hc.Handler().frostfsid.(*frostfsidMock).data["devenv"] = key.PublicKey()
|
|
|
|
newPolicy.Statement[0].Principal = map[engineiam.PrincipalType][]string{engineiam.AWSPrincipalType: {"arn:aws:iam:::user/devenv"}}
|
|
putBucketPolicy(hc, bktName, newPolicy)
|
|
bktPolicyStatus = getBucketPolicyStatus(hc, bktName)
|
|
require.True(t, PolicyStatusIsPublicFalse == bktPolicyStatus.IsPublic)
|
|
}
|
|
|
|
func TestDeleteBucketWithPolicy(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
|
|
bktName := "bucket-for-policy"
|
|
bi := createTestBucket(hc, bktName)
|
|
|
|
newPolicy := engineiam.Policy{
|
|
Version: "2012-10-17",
|
|
Statement: []engineiam.Statement{{
|
|
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
|
|
Effect: engineiam.AllowEffect,
|
|
Action: engineiam.Action{"s3:PutObject"},
|
|
Resource: engineiam.Resource{"arn:aws:s3:::bucket-for-policy/*"},
|
|
}},
|
|
}
|
|
|
|
putBucketPolicy(hc, bktName, newPolicy)
|
|
|
|
require.Len(t, hc.h.ape.(*apeMock).policyMap, 1)
|
|
require.Len(t, hc.h.ape.(*apeMock).chainMap[engine.ContainerTarget(bi.CID.EncodeToString())], 4)
|
|
|
|
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
|
|
|
require.Empty(t, hc.h.ape.(*apeMock).policyMap)
|
|
chains, err := hc.h.ape.(*apeMock).ListChains(engine.ContainerTarget(bi.CID.EncodeToString()))
|
|
require.NoError(t, err)
|
|
require.Empty(t, chains)
|
|
}
|
|
|
|
func TestPutBucketPolicy(t *testing.T) {
|
|
bktPolicy := `
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Principal": "*",
|
|
"Effect": "Deny",
|
|
"Action": "s3:GetObject",
|
|
"Resource": "arn:aws:s3:::bucket-for-policy/*"
|
|
}]
|
|
}
|
|
`
|
|
hc := prepareHandlerContext(t)
|
|
bktName := "bucket-for-policy"
|
|
|
|
createTestBucket(hc, bktName)
|
|
|
|
w, r := prepareTestPayloadRequest(hc, bktName, "", bytes.NewReader([]byte(bktPolicy)))
|
|
hc.Handler().PutBucketPolicyHandler(w, r)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
}
|
|
|
|
func getBucketPolicy(hc *handlerContext, bktName string, errCode ...s3errors.ErrorCode) engineiam.Policy {
|
|
w, r := prepareTestRequest(hc, bktName, "", nil)
|
|
hc.Handler().GetBucketPolicyHandler(w, r)
|
|
|
|
var policy engineiam.Policy
|
|
if len(errCode) == 0 {
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
err := json.NewDecoder(w.Result().Body).Decode(&policy)
|
|
require.NoError(hc.t, err)
|
|
} else {
|
|
assertS3Error(hc.t, w, s3errors.GetAPIError(errCode[0]))
|
|
}
|
|
|
|
return policy
|
|
}
|
|
|
|
func getBucketPolicyStatus(hc *handlerContext, bktName string, errCode ...s3errors.ErrorCode) PolicyStatus {
|
|
w, r := prepareTestRequest(hc, bktName, "", nil)
|
|
hc.Handler().GetBucketPolicyStatusHandler(w, r)
|
|
|
|
var policyStatus PolicyStatus
|
|
if len(errCode) == 0 {
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
err := xml.NewDecoder(w.Result().Body).Decode(&policyStatus)
|
|
require.NoError(hc.t, err)
|
|
} else {
|
|
assertS3Error(hc.t, w, s3errors.GetAPIError(errCode[0]))
|
|
}
|
|
|
|
return policyStatus
|
|
}
|
|
|
|
func putBucketPolicy(hc *handlerContext, bktName string, bktPolicy engineiam.Policy, errCode ...s3errors.ErrorCode) {
|
|
body, err := json.Marshal(bktPolicy)
|
|
require.NoError(hc.t, err)
|
|
|
|
w, r := prepareTestPayloadRequest(hc, bktName, "", bytes.NewReader(body))
|
|
hc.Handler().PutBucketPolicyHandler(w, r)
|
|
|
|
if len(errCode) == 0 {
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
} else {
|
|
assertS3Error(hc.t, w, s3errors.GetAPIError(errCode[0]))
|
|
}
|
|
}
|
|
|
|
func createAccessBox(t *testing.T) (*accessbox.Box, *keys.PrivateKey) {
|
|
key, err := keys.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
var bearerToken bearer.Token
|
|
err = bearerToken.Sign(key.PrivateKey)
|
|
require.NoError(t, err)
|
|
|
|
tok := new(session.Container)
|
|
tok.ForVerb(session.VerbContainerPut)
|
|
err = tok.Sign(key.PrivateKey)
|
|
require.NoError(t, err)
|
|
|
|
box := &accessbox.Box{
|
|
Gate: &accessbox.GateData{
|
|
SessionTokens: []*session.Container{tok},
|
|
BearerToken: &bearerToken,
|
|
},
|
|
}
|
|
|
|
return box, key
|
|
}
|
|
|
|
type createBucketInfo struct {
|
|
BktInfo *data.BucketInfo
|
|
Box *accessbox.Box
|
|
Key *keys.PrivateKey
|
|
}
|
|
|
|
func createBucket(hc *handlerContext, bktName string) *createBucketInfo {
|
|
box, key := createAccessBox(hc.t)
|
|
|
|
w := createBucketBase(hc, bktName, box)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
|
|
bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName)
|
|
require.NoError(hc.t, err)
|
|
|
|
return &createBucketInfo{
|
|
BktInfo: bktInfo,
|
|
Box: box,
|
|
Key: key,
|
|
}
|
|
}
|
|
|
|
func createBucketAssertS3Error(hc *handlerContext, bktName string, box *accessbox.Box, code s3errors.ErrorCode) {
|
|
w := createBucketBase(hc, bktName, box)
|
|
assertS3Error(hc.t, w, s3errors.GetAPIError(code))
|
|
}
|
|
|
|
func createBucketBase(hc *handlerContext, bktName string, box *accessbox.Box) *httptest.ResponseRecorder {
|
|
w, r := prepareTestRequest(hc, bktName, "", nil)
|
|
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
|
|
r = r.WithContext(ctx)
|
|
hc.Handler().CreateBucketHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
func putBucketACL(hc *handlerContext, bktName string, box *accessbox.Box, header map[string]string) {
|
|
w := putBucketACLBase(hc, bktName, box, header, nil)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
}
|
|
|
|
func putBucketACLAssertS3Error(hc *handlerContext, bktName string, box *accessbox.Box, header map[string]string, body *AccessControlPolicy, code s3errors.ErrorCode) {
|
|
w := putBucketACLBase(hc, bktName, box, header, body)
|
|
assertS3Error(hc.t, w, s3errors.GetAPIError(code))
|
|
}
|
|
|
|
func putBucketACLBase(hc *handlerContext, bktName string, box *accessbox.Box, header map[string]string, body *AccessControlPolicy) *httptest.ResponseRecorder {
|
|
w, r := prepareTestRequest(hc, bktName, "", body)
|
|
for key, val := range header {
|
|
r.Header.Set(key, val)
|
|
}
|
|
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
|
|
r = r.WithContext(ctx)
|
|
hc.Handler().PutBucketACLHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
func getBucketACL(hc *handlerContext, bktName string) *AccessControlPolicy {
|
|
w := getBucketACLBase(hc, bktName)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
res := &AccessControlPolicy{}
|
|
parseTestResponse(hc.t, w, res)
|
|
return res
|
|
}
|
|
|
|
func getBucketACLBase(hc *handlerContext, bktName string) *httptest.ResponseRecorder {
|
|
w, r := prepareTestRequest(hc, bktName, "", nil)
|
|
hc.Handler().GetBucketACLHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
func putObjectACLAssertS3Error(hc *handlerContext, bktName, objName string, box *accessbox.Box, header map[string]string, body *AccessControlPolicy, code s3errors.ErrorCode) {
|
|
w := putObjectACLBase(hc, bktName, objName, box, header, body)
|
|
assertS3Error(hc.t, w, s3errors.GetAPIError(code))
|
|
}
|
|
|
|
func putObjectACLBase(hc *handlerContext, bktName, objName string, box *accessbox.Box, header map[string]string, body *AccessControlPolicy) *httptest.ResponseRecorder {
|
|
w, r := prepareTestRequest(hc, bktName, objName, body)
|
|
for key, val := range header {
|
|
r.Header.Set(key, val)
|
|
}
|
|
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
|
|
r = r.WithContext(ctx)
|
|
hc.Handler().PutObjectACLHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
func getObjectACL(hc *handlerContext, bktName, objName string) *AccessControlPolicy {
|
|
w := getObjectACLBase(hc, bktName, objName)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
res := &AccessControlPolicy{}
|
|
parseTestResponse(hc.t, w, res)
|
|
return res
|
|
}
|
|
|
|
func getObjectACLBase(hc *handlerContext, bktName, objName string) *httptest.ResponseRecorder {
|
|
w, r := prepareTestRequest(hc, bktName, objName, nil)
|
|
hc.Handler().GetObjectACLHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
func putObjectWithHeaders(hc *handlerContext, bktName, objName string, headers map[string]string) http.Header {
|
|
w := putObjectWithHeadersBase(hc, bktName, objName, headers, nil, nil)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
return w.Header()
|
|
}
|
|
|
|
func putObjectWithHeadersAssertS3Error(hc *handlerContext, bktName, objName string, headers map[string]string, code s3errors.ErrorCode) {
|
|
w := putObjectWithHeadersBase(hc, bktName, objName, headers, nil, nil)
|
|
assertS3Error(hc.t, w, s3errors.GetAPIError(code))
|
|
}
|
|
|
|
func putObjectWithHeadersBase(hc *handlerContext, bktName, objName string, headers map[string]string, box *accessbox.Box, data []byte) *httptest.ResponseRecorder {
|
|
body := bytes.NewReader(data)
|
|
w, r := prepareTestPayloadRequest(hc, bktName, objName, body)
|
|
|
|
for k, v := range headers {
|
|
r.Header.Set(k, v)
|
|
}
|
|
|
|
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
|
|
r = r.WithContext(ctx)
|
|
|
|
hc.Handler().PutObjectHandler(w, r)
|
|
return w
|
|
}
|