forked from TrueCloudLab/frostfs-s3-gw
[#306] policy: Change default access strategy
Use access strategy based on bucket type and/or config flags. Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
1bfea006b0
commit
3285a2e105
4 changed files with 334 additions and 98 deletions
|
@ -1,6 +1,7 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -19,27 +20,29 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PolicySettings interface {
|
type PolicySettings interface {
|
||||||
ResolveNamespaceAlias(ns string) string
|
|
||||||
PolicyDenyByDefault() bool
|
PolicyDenyByDefault() bool
|
||||||
|
ACLEnabled() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type FrostFSIDInformer interface {
|
type FrostFSIDInformer interface {
|
||||||
GetUserGroupIDs(userHash util.Uint160) ([]string, error)
|
GetUserGroupIDs(userHash util.Uint160) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PolicyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, settings PolicySettings, domains []string, log *zap.Logger) Func {
|
type PolicyConfig struct {
|
||||||
|
Storage engine.ChainRouter
|
||||||
|
FrostfsID FrostFSIDInformer
|
||||||
|
Settings PolicySettings
|
||||||
|
Domains []string
|
||||||
|
Log *zap.Logger
|
||||||
|
BucketResolver BucketResolveFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func PolicyCheck(cfg PolicyConfig) Func {
|
||||||
return func(h http.Handler) http.Handler {
|
return func(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
if err := policyCheck(r, cfg); err != nil {
|
||||||
st, err := policyCheck(storage, frostfsid, settings, domains, r)
|
reqLogOrDefault(ctx, cfg.Log).Error(logs.PolicyValidationFailed, zap.Error(err))
|
||||||
if err == nil {
|
|
||||||
if st != chain.Allow && (st != chain.NoRuleFound || settings.PolicyDenyByDefault()) {
|
|
||||||
err = apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
reqLogOrDefault(ctx, log).Error(logs.PolicyValidationFailed, zap.Error(err))
|
|
||||||
WriteErrorResponse(w, GetReqInfo(ctx), err)
|
WriteErrorResponse(w, GetReqInfo(ctx), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -49,27 +52,58 @@ func PolicyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, settin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func policyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, settings PolicySettings, domains []string, r *http.Request) (chain.Status, error) {
|
func policyCheck(r *http.Request, cfg PolicyConfig) error {
|
||||||
req, err := getPolicyRequest(r, frostfsid, domains)
|
reqType, bktName, objName := getBucketObject(r, cfg.Domains)
|
||||||
|
req, err := getPolicyRequest(r, cfg.FrostfsID, reqType, bktName, objName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
reqInfo := GetReqInfo(r.Context())
|
reqInfo := GetReqInfo(r.Context())
|
||||||
target := engine.NewRequestTargetWithNamespace(settings.ResolveNamespaceAlias(reqInfo.Namespace))
|
target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace)
|
||||||
st, found, err := storage.IsAllowed(chain.S3, target, req)
|
st, found, err := cfg.Storage.IsAllowed(chain.S3, target, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
st = chain.NoRuleFound
|
st = chain.NoRuleFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return st, nil
|
switch {
|
||||||
|
case st == chain.Allow:
|
||||||
|
return nil
|
||||||
|
case st != chain.NoRuleFound:
|
||||||
|
return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, domains []string) (*testutil.Request, error) {
|
isAPE, err := isAPEBehavior(r.Context(), req, cfg, reqType, bktName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAPE && cfg.Settings.PolicyDenyByDefault() {
|
||||||
|
return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAPEBehavior(ctx context.Context, req *testutil.Request, cfg PolicyConfig, reqType ReqType, bktName string) (bool, error) {
|
||||||
|
if reqType == noneType ||
|
||||||
|
strings.HasSuffix(req.Operation(), CreateBucketOperation) {
|
||||||
|
return !cfg.Settings.ACLEnabled(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bktInfo, err := cfg.BucketResolver(ctx, bktName) // we cannot use reqInfo.BucketName because it hasn't been set yet
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bktInfo.APEEnabled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, reqType ReqType, bktName string, objName string) (*testutil.Request, error) {
|
||||||
var (
|
var (
|
||||||
owner string
|
owner string
|
||||||
groups []string
|
groups []string
|
||||||
|
@ -90,7 +124,14 @@ func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, domains []st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
op, res := determineOperationAndResource(r, domains)
|
op := determineOperation(r, reqType)
|
||||||
|
var res string
|
||||||
|
switch reqType {
|
||||||
|
case objectType:
|
||||||
|
res = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName)
|
||||||
|
default:
|
||||||
|
res = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName)
|
||||||
|
}
|
||||||
|
|
||||||
return testutil.NewRequest(op, testutil.NewResource(res, nil),
|
return testutil.NewRequest(op, testutil.NewResource(res, nil),
|
||||||
map[string]string{
|
map[string]string{
|
||||||
|
@ -108,45 +149,34 @@ const (
|
||||||
objectType
|
objectType
|
||||||
)
|
)
|
||||||
|
|
||||||
func determineOperationAndResource(r *http.Request, domains []string) (operation string, resource string) {
|
func getBucketObject(r *http.Request, domains []string) (reqType ReqType, bktName string, objName string) {
|
||||||
var (
|
|
||||||
reqType ReqType
|
|
||||||
matchDomain bool
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
ind := strings.Index(r.Host, "."+domain)
|
ind := strings.Index(r.Host, "."+domain)
|
||||||
if ind == -1 {
|
if ind == -1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
matchDomain = true
|
|
||||||
reqType = bucketType
|
|
||||||
bkt := r.Host[:ind]
|
bkt := r.Host[:ind]
|
||||||
if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" {
|
if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" {
|
||||||
reqType = objectType
|
return objectType, bkt, obj
|
||||||
resource = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bkt, obj)
|
|
||||||
} else {
|
|
||||||
resource = fmt.Sprintf(s3.ResourceFormatS3Bucket, bkt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
return bucketType, bkt, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matchDomain {
|
|
||||||
bktObj := strings.TrimPrefix(r.URL.Path, "/")
|
bktObj := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
if ind := strings.IndexByte(bktObj, '/'); ind == -1 {
|
|
||||||
reqType = bucketType
|
|
||||||
resource = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktObj)
|
|
||||||
if bktObj == "" {
|
if bktObj == "" {
|
||||||
reqType = noneType
|
return noneType, "", ""
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reqType = objectType
|
|
||||||
resource = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktObj[:ind], bktObj[ind+1:])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ind := strings.IndexByte(bktObj, '/'); ind != -1 {
|
||||||
|
return objectType, bktObj[:ind], bktObj[ind+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucketType, bktObj, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func determineOperation(r *http.Request, reqType ReqType) (operation string) {
|
||||||
switch reqType {
|
switch reqType {
|
||||||
case objectType:
|
case objectType:
|
||||||
operation = determineObjectOperation(r)
|
operation = determineObjectOperation(r)
|
||||||
|
@ -156,7 +186,7 @@ func determineOperationAndResource(r *http.Request, domains []string) (operation
|
||||||
operation = determineGeneralOperation(r)
|
operation = determineGeneralOperation(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "s3:" + operation, resource
|
return "s3:" + operation
|
||||||
}
|
}
|
||||||
|
|
||||||
func determineBucketOperation(r *http.Request) string {
|
func determineBucketOperation(r *http.Request) string {
|
||||||
|
|
|
@ -136,7 +136,14 @@ func NewRouter(cfg Config) *chi.Mux {
|
||||||
api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log))
|
api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log))
|
||||||
}
|
}
|
||||||
|
|
||||||
api.Use(s3middleware.PolicyCheck(cfg.PolicyChecker, cfg.FrostfsID, cfg.MiddlewareSettings, cfg.Domains, cfg.Log))
|
api.Use(s3middleware.PolicyCheck(s3middleware.PolicyConfig{
|
||||||
|
Storage: cfg.PolicyChecker,
|
||||||
|
FrostfsID: cfg.FrostfsID,
|
||||||
|
Settings: cfg.MiddlewareSettings,
|
||||||
|
Domains: cfg.Domains,
|
||||||
|
Log: cfg.Log,
|
||||||
|
BucketResolver: cfg.Handler.ResolveBucket,
|
||||||
|
}))
|
||||||
|
|
||||||
defaultRouter := chi.NewRouter()
|
defaultRouter := chi.NewRouter()
|
||||||
defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler, cfg.Log))
|
defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler, cfg.Log))
|
||||||
|
|
|
@ -3,6 +3,7 @@ package api
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
|
||||||
|
|
||||||
type middlewareSettingsMock struct {
|
type middlewareSettingsMock struct {
|
||||||
denyByDefault bool
|
denyByDefault bool
|
||||||
|
aclEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *middlewareSettingsMock) NamespaceHeader() string {
|
func (r *middlewareSettingsMock) NamespaceHeader() string {
|
||||||
|
@ -67,6 +69,10 @@ func (r *middlewareSettingsMock) PolicyDenyByDefault() bool {
|
||||||
return r.denyByDefault
|
return r.denyByDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *middlewareSettingsMock) ACLEnabled() bool {
|
||||||
|
return r.aclEnabled
|
||||||
|
}
|
||||||
|
|
||||||
type frostFSIDMock struct {
|
type frostFSIDMock struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +86,8 @@ func (f *frostFSIDMock) GetUserGroupIDs(util.Uint160) ([]string, error) {
|
||||||
|
|
||||||
type handlerMock struct {
|
type handlerMock struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
|
cfg *middlewareSettingsMock
|
||||||
|
buckets map[string]*data.BucketInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
type handlerResult struct {
|
type handlerResult struct {
|
||||||
|
@ -339,9 +347,20 @@ func (h *handlerMock) PutBucketNotificationHandler(http.ResponseWriter, *http.Re
|
||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handlerMock) CreateBucketHandler(http.ResponseWriter, *http.Request) {
|
func (h *handlerMock) CreateBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
//TODO implement me
|
reqInfo := middleware.GetReqInfo(r.Context())
|
||||||
panic("implement me")
|
|
||||||
|
h.buckets[reqInfo.Namespace+reqInfo.BucketName] = &data.BucketInfo{
|
||||||
|
Name: reqInfo.BucketName,
|
||||||
|
APEEnabled: !h.cfg.ACLEnabled(),
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &handlerResult{
|
||||||
|
Method: middleware.CreateBucketOperation,
|
||||||
|
ReqInfo: middleware.GetReqInfo(r.Context()),
|
||||||
|
}
|
||||||
|
|
||||||
|
h.writeResponse(w, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handlerMock) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *handlerMock) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -443,8 +462,13 @@ func (h *handlerMock) ListMultipartUploadsHandler(w http.ResponseWriter, r *http
|
||||||
h.writeResponse(w, res)
|
h.writeResponse(w, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handlerMock) ResolveBucket(context.Context, string) (*data.BucketInfo, error) {
|
func (h *handlerMock) ResolveBucket(ctx context.Context, name string) (*data.BucketInfo, error) {
|
||||||
return &data.BucketInfo{}, nil
|
reqInfo := middleware.GetReqInfo(ctx)
|
||||||
|
bktInfo, ok := h.buckets[reqInfo.Namespace+name]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
return bktInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handlerMock) writeResponse(w http.ResponseWriter, resp *handlerResult) {
|
func (h *handlerMock) writeResponse(w http.ResponseWriter, resp *handlerResult) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||||
apiErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
apiErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||||
s3middleware "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
s3middleware "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
|
||||||
|
@ -27,6 +28,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type routerMock struct {
|
type routerMock struct {
|
||||||
|
t *testing.T
|
||||||
router *chi.Mux
|
router *chi.Mux
|
||||||
cfg Config
|
cfg Config
|
||||||
middlewareSettings *middlewareSettingsMock
|
middlewareSettings *middlewareSettingsMock
|
||||||
|
@ -55,7 +57,7 @@ func prepareRouter(t *testing.T) *routerMock {
|
||||||
Limit: 10,
|
Limit: 10,
|
||||||
BacklogTimeout: 30 * time.Second,
|
BacklogTimeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
Handler: &handlerMock{t: t},
|
Handler: &handlerMock{t: t, cfg: middlewareSettings, buckets: map[string]*data.BucketInfo{}},
|
||||||
Center: ¢erMock{t: t},
|
Center: ¢erMock{t: t},
|
||||||
Log: logger,
|
Log: logger,
|
||||||
Metrics: metrics.NewAppMetrics(metricsConfig),
|
Metrics: metrics.NewAppMetrics(metricsConfig),
|
||||||
|
@ -65,6 +67,7 @@ func prepareRouter(t *testing.T) *routerMock {
|
||||||
FrostfsID: &frostFSIDMock{},
|
FrostfsID: &frostFSIDMock{},
|
||||||
}
|
}
|
||||||
return &routerMock{
|
return &routerMock{
|
||||||
|
t: t,
|
||||||
router: NewRouter(cfg),
|
router: NewRouter(cfg),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
middlewareSettings: middlewareSettings,
|
middlewareSettings: middlewareSettings,
|
||||||
|
@ -75,6 +78,8 @@ func prepareRouter(t *testing.T) *routerMock {
|
||||||
func TestRouterUploadPart(t *testing.T) {
|
func TestRouterUploadPart(t *testing.T) {
|
||||||
chiRouter := prepareRouter(t)
|
chiRouter := prepareRouter(t)
|
||||||
|
|
||||||
|
createBucket(chiRouter, "", "dkirillov")
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r := httptest.NewRequest(http.MethodPut, "/dkirillov/fix-object", nil)
|
r := httptest.NewRequest(http.MethodPut, "/dkirillov/fix-object", nil)
|
||||||
query := make(url.Values)
|
query := make(url.Values)
|
||||||
|
@ -90,6 +95,8 @@ func TestRouterUploadPart(t *testing.T) {
|
||||||
func TestRouterListMultipartUploads(t *testing.T) {
|
func TestRouterListMultipartUploads(t *testing.T) {
|
||||||
chiRouter := prepareRouter(t)
|
chiRouter := prepareRouter(t)
|
||||||
|
|
||||||
|
createBucket(chiRouter, "", "test-bucket")
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r := httptest.NewRequest(http.MethodGet, "/test-bucket", nil)
|
r := httptest.NewRequest(http.MethodGet, "/test-bucket", nil)
|
||||||
query := make(url.Values)
|
query := make(url.Values)
|
||||||
|
@ -104,22 +111,18 @@ func TestRouterListMultipartUploads(t *testing.T) {
|
||||||
func TestRouterObjectWithSlashes(t *testing.T) {
|
func TestRouterObjectWithSlashes(t *testing.T) {
|
||||||
chiRouter := prepareRouter(t)
|
chiRouter := prepareRouter(t)
|
||||||
|
|
||||||
bktName, objName := "dkirillov", "/fix/object"
|
ns, bktName, objName := "", "dkirillov", "/fix/object"
|
||||||
target := fmt.Sprintf("/%s/%s", bktName, objName)
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
createBucket(chiRouter, ns, bktName)
|
||||||
r := httptest.NewRequest(http.MethodPut, target, nil)
|
resp := putObject(chiRouter, ns, bktName, objName)
|
||||||
|
|
||||||
chiRouter.ServeHTTP(w, r)
|
|
||||||
resp := readResponse(t, w)
|
|
||||||
require.Equal(t, "PutObject", resp.Method)
|
|
||||||
require.Equal(t, objName, resp.ReqInfo.ObjectName)
|
require.Equal(t, objName, resp.ReqInfo.ObjectName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouterObjectEscaping(t *testing.T) {
|
func TestRouterObjectEscaping(t *testing.T) {
|
||||||
chiRouter := prepareRouter(t)
|
chiRouter := prepareRouter(t)
|
||||||
|
|
||||||
bktName := "dkirillov"
|
ns, bktName := "", "dkirillov"
|
||||||
|
createBucket(chiRouter, ns, bktName)
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -153,14 +156,7 @@ func TestRouterObjectEscaping(t *testing.T) {
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
target := fmt.Sprintf("/%s/%s", bktName, tc.objName)
|
resp := putObject(chiRouter, ns, bktName, tc.objName)
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
r := httptest.NewRequest(http.MethodPut, target, nil)
|
|
||||||
|
|
||||||
chiRouter.ServeHTTP(w, r)
|
|
||||||
resp := readResponse(t, w)
|
|
||||||
require.Equal(t, "PutObject", resp.Method)
|
|
||||||
require.Equal(t, tc.expectedObjName, resp.ReqInfo.ObjectName)
|
require.Equal(t, tc.expectedObjName, resp.ReqInfo.ObjectName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -168,40 +164,33 @@ func TestRouterObjectEscaping(t *testing.T) {
|
||||||
|
|
||||||
func TestPolicyChecker(t *testing.T) {
|
func TestPolicyChecker(t *testing.T) {
|
||||||
chiRouter := prepareRouter(t)
|
chiRouter := prepareRouter(t)
|
||||||
namespace := "custom-ns"
|
ns1, bktName1, objName1 := "", "bucket", "object"
|
||||||
bktName, objName := "bucket", "object"
|
ns2, bktName2, objName2 := "custom-ns", "other-bucket", "object"
|
||||||
target := fmt.Sprintf("/%s/%s", bktName, objName)
|
|
||||||
|
createBucket(chiRouter, ns1, bktName1)
|
||||||
|
createBucket(chiRouter, ns2, bktName1)
|
||||||
|
createBucket(chiRouter, ns2, bktName2)
|
||||||
|
|
||||||
ruleChain := &chain.Chain{
|
ruleChain := &chain.Chain{
|
||||||
ID: chain.ID("id"),
|
ID: chain.ID("id"),
|
||||||
Rules: []chain.Rule{{
|
Rules: []chain.Rule{{
|
||||||
Status: chain.AccessDenied,
|
Status: chain.AccessDenied,
|
||||||
Actions: chain.Actions{Names: []string{"*"}},
|
Actions: chain.Actions{Names: []string{"*"}},
|
||||||
Resources: chain.Resources{Names: []string{fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName)}},
|
Resources: chain.Resources{Names: []string{fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName1)}},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err := chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(namespace), ruleChain)
|
_, _, err := chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(ns2), ruleChain)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// check we can access 'bucket' in default namespace
|
// check we can access 'bucket' in default namespace
|
||||||
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
|
putObject(chiRouter, ns1, bktName1, objName1)
|
||||||
chiRouter.ServeHTTP(w, r)
|
|
||||||
resp := readResponse(t, w)
|
|
||||||
require.Equal(t, s3middleware.PutObjectOperation, resp.Method)
|
|
||||||
|
|
||||||
// check we can access 'other-bucket' in custom namespace
|
// check we can access 'other-bucket' in custom namespace
|
||||||
w, r = httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/other-bucket/object", nil)
|
putObject(chiRouter, ns2, bktName2, objName2)
|
||||||
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
|
||||||
chiRouter.ServeHTTP(w, r)
|
|
||||||
resp = readResponse(t, w)
|
|
||||||
require.Equal(t, s3middleware.PutObjectOperation, resp.Method)
|
|
||||||
|
|
||||||
// check we cannot access 'bucket' in custom namespace
|
// check we cannot access 'bucket' in custom namespace
|
||||||
w, r = httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
|
putObjectErr(chiRouter, ns2, bktName1, objName2, apiErrors.ErrAccessDenied)
|
||||||
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
|
||||||
chiRouter.ServeHTTP(w, r)
|
|
||||||
assertAPIError(t, w, apiErrors.ErrAccessDenied)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolicyCheckerReqTypeDetermination(t *testing.T) {
|
func TestPolicyCheckerReqTypeDetermination(t *testing.T) {
|
||||||
|
@ -248,20 +237,206 @@ func TestPolicyCheckerReqTypeDetermination(t *testing.T) {
|
||||||
|
|
||||||
func TestDefaultBehaviorPolicyChecker(t *testing.T) {
|
func TestDefaultBehaviorPolicyChecker(t *testing.T) {
|
||||||
chiRouter := prepareRouter(t)
|
chiRouter := prepareRouter(t)
|
||||||
bktName, objName := "bucket", "object"
|
ns, bktName := "", "bucket"
|
||||||
target := fmt.Sprintf("/%s/%s", bktName, objName)
|
|
||||||
|
|
||||||
// check we can access bucket if rules not found
|
// check we can access bucket if rules not found
|
||||||
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
|
createBucket(chiRouter, ns, bktName)
|
||||||
chiRouter.ServeHTTP(w, r)
|
|
||||||
resp := readResponse(t, w)
|
|
||||||
require.Equal(t, s3middleware.PutObjectOperation, resp.Method)
|
|
||||||
|
|
||||||
// check we cannot access if rules not found when settings is enabled
|
// check we cannot access if rules not found when settings is enabled
|
||||||
chiRouter.middlewareSettings.denyByDefault = true
|
chiRouter.middlewareSettings.denyByDefault = true
|
||||||
w, r = httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
|
createBucketErr(chiRouter, ns, bktName, apiErrors.ErrAccessDenied)
|
||||||
chiRouter.ServeHTTP(w, r)
|
}
|
||||||
assertAPIError(t, w, apiErrors.ErrAccessDenied)
|
|
||||||
|
func TestACLAPE(t *testing.T) {
|
||||||
|
t.Run("acl disabled, ape deny by default", func(t *testing.T) {
|
||||||
|
router := prepareRouter(t)
|
||||||
|
|
||||||
|
ns, bktName, objName := "", "bucket", "object"
|
||||||
|
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
|
||||||
|
createOldBucket(router, bktNameOld)
|
||||||
|
createNewBucket(router, bktNameNew)
|
||||||
|
|
||||||
|
router.middlewareSettings.aclEnabled = false
|
||||||
|
router.middlewareSettings.denyByDefault = true
|
||||||
|
|
||||||
|
// Allow because of using old bucket
|
||||||
|
putObject(router, ns, bktNameOld, objName)
|
||||||
|
// Deny because of deny by default
|
||||||
|
putObjectErr(router, ns, bktNameNew, objName, apiErrors.ErrAccessDenied)
|
||||||
|
|
||||||
|
// Deny because of deny by default
|
||||||
|
createBucketErr(router, ns, bktName, apiErrors.ErrAccessDenied)
|
||||||
|
listBucketsErr(router, ns, apiErrors.ErrAccessDenied)
|
||||||
|
|
||||||
|
// Allow operations and check
|
||||||
|
allowOperations(router, ns, []string{"s3:CreateBucket", "s3:ListBuckets"})
|
||||||
|
createBucket(router, ns, bktName)
|
||||||
|
listBuckets(router, ns)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("acl disabled, ape allow by default", func(t *testing.T) {
|
||||||
|
router := prepareRouter(t)
|
||||||
|
|
||||||
|
ns, bktName, objName := "", "bucket", "object"
|
||||||
|
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
|
||||||
|
createOldBucket(router, bktNameOld)
|
||||||
|
createNewBucket(router, bktNameNew)
|
||||||
|
|
||||||
|
router.middlewareSettings.aclEnabled = false
|
||||||
|
router.middlewareSettings.denyByDefault = false
|
||||||
|
|
||||||
|
// Allow because of using old bucket
|
||||||
|
putObject(router, ns, bktNameOld, objName)
|
||||||
|
// Allow because of allow by default
|
||||||
|
putObject(router, ns, bktNameNew, objName)
|
||||||
|
|
||||||
|
// Allow because of deny by default
|
||||||
|
createBucket(router, ns, bktName)
|
||||||
|
listBuckets(router, ns)
|
||||||
|
|
||||||
|
// Deny operations and check
|
||||||
|
denyOperations(router, ns, []string{"s3:CreateBucket", "s3:ListBuckets"})
|
||||||
|
createBucketErr(router, ns, bktName, apiErrors.ErrAccessDenied)
|
||||||
|
listBucketsErr(router, ns, apiErrors.ErrAccessDenied)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("acl enabled, ape deny by default", func(t *testing.T) {
|
||||||
|
router := prepareRouter(t)
|
||||||
|
|
||||||
|
ns, bktName, objName := "", "bucket", "object"
|
||||||
|
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
|
||||||
|
createOldBucket(router, bktNameOld)
|
||||||
|
createNewBucket(router, bktNameNew)
|
||||||
|
|
||||||
|
router.middlewareSettings.aclEnabled = true
|
||||||
|
router.middlewareSettings.denyByDefault = true
|
||||||
|
|
||||||
|
// Allow because of using old bucket
|
||||||
|
putObject(router, ns, bktNameOld, objName)
|
||||||
|
// Deny because of deny by default
|
||||||
|
putObjectErr(router, ns, bktNameNew, objName, apiErrors.ErrAccessDenied)
|
||||||
|
|
||||||
|
// Allow because of old behavior
|
||||||
|
createBucket(router, ns, bktName)
|
||||||
|
listBuckets(router, ns)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("acl enabled, ape allow by default", func(t *testing.T) {
|
||||||
|
router := prepareRouter(t)
|
||||||
|
|
||||||
|
ns, bktName, objName := "", "bucket", "object"
|
||||||
|
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
|
||||||
|
createOldBucket(router, bktNameOld)
|
||||||
|
createNewBucket(router, bktNameNew)
|
||||||
|
|
||||||
|
router.middlewareSettings.aclEnabled = true
|
||||||
|
router.middlewareSettings.denyByDefault = false
|
||||||
|
|
||||||
|
// Allow because of using old bucket
|
||||||
|
putObject(router, ns, bktNameOld, objName)
|
||||||
|
// Allow because of allow by default
|
||||||
|
putObject(router, ns, bktNameNew, objName)
|
||||||
|
|
||||||
|
// Allow because of old behavior
|
||||||
|
createBucket(router, ns, bktName)
|
||||||
|
listBuckets(router, ns)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func allowOperations(router *routerMock, ns string, operations []string) {
|
||||||
|
addPolicy(router, ns, "allow", engineiam.AllowEffect, operations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func denyOperations(router *routerMock, ns string, operations []string) {
|
||||||
|
addPolicy(router, ns, "deny", engineiam.DenyEffect, operations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPolicy(router *routerMock, ns string, id string, effect engineiam.Effect, operations []string) {
|
||||||
|
policy := engineiam.Policy{
|
||||||
|
Statement: []engineiam.Statement{{
|
||||||
|
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
|
||||||
|
Effect: effect,
|
||||||
|
Action: engineiam.Action(operations),
|
||||||
|
Resource: engineiam.Resource{fmt.Sprintf(s3.ResourceFormatS3All)},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleChain, err := engineiam.ConvertToS3Chain(policy, nil)
|
||||||
|
require.NoError(router.t, err)
|
||||||
|
ruleChain.ID = chain.ID(id)
|
||||||
|
|
||||||
|
_, _, err = router.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(ns), ruleChain)
|
||||||
|
require.NoError(router.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createOldBucket(router *routerMock, bktName string) {
|
||||||
|
createSpecificBucket(router, bktName, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewBucket(router *routerMock, bktName string) {
|
||||||
|
createSpecificBucket(router, bktName, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSpecificBucket(router *routerMock, bktName string, old bool) {
|
||||||
|
aclEnabled := router.middlewareSettings.ACLEnabled()
|
||||||
|
router.middlewareSettings.aclEnabled = old
|
||||||
|
createBucket(router, "", bktName)
|
||||||
|
router.middlewareSettings.aclEnabled = aclEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBucket(router *routerMock, namespace, bktName string) {
|
||||||
|
w := createBucketBase(router, namespace, bktName)
|
||||||
|
resp := readResponse(router.t, w)
|
||||||
|
require.Equal(router.t, s3middleware.CreateBucketOperation, resp.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBucketErr(router *routerMock, namespace, bktName string, errCode apiErrors.ErrorCode) {
|
||||||
|
w := createBucketBase(router, namespace, bktName)
|
||||||
|
assertAPIError(router.t, w, errCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBucketBase(router *routerMock, namespace, bktName string) *httptest.ResponseRecorder {
|
||||||
|
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName, nil)
|
||||||
|
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func listBuckets(router *routerMock, namespace string) {
|
||||||
|
w := listBucketsBase(router, namespace)
|
||||||
|
resp := readResponse(router.t, w)
|
||||||
|
require.Equal(router.t, s3middleware.ListBucketsOperation, resp.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listBucketsErr(router *routerMock, namespace string, errCode apiErrors.ErrorCode) {
|
||||||
|
w := listBucketsBase(router, namespace)
|
||||||
|
assertAPIError(router.t, w, errCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listBucketsBase(router *routerMock, namespace string) *httptest.ResponseRecorder {
|
||||||
|
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func putObject(router *routerMock, namespace, bktName, objName string) handlerResult {
|
||||||
|
w := putObjectBase(router, namespace, bktName, objName)
|
||||||
|
resp := readResponse(router.t, w)
|
||||||
|
require.Equal(router.t, s3middleware.PutObjectOperation, resp.Method)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func putObjectErr(router *routerMock, namespace, bktName, objName string, errCode apiErrors.ErrorCode) {
|
||||||
|
w := putObjectBase(router, namespace, bktName, objName)
|
||||||
|
assertAPIError(router.t, w, errCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putObjectBase(router *routerMock, namespace, bktName, objName string) *httptest.ResponseRecorder {
|
||||||
|
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName+"/"+objName, nil)
|
||||||
|
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOwnerIDRetrieving(t *testing.T) {
|
func TestOwnerIDRetrieving(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue