diff --git a/api/handler/acl.go b/api/handler/acl.go index 8ac62035..f0fdc9f0 100644 --- a/api/handler/acl.go +++ b/api/handler/acl.go @@ -8,6 +8,7 @@ import ( "encoding/json" stderrors "errors" "fmt" + "io" "net/http" "sort" "strconv" @@ -24,6 +25,9 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session" + engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "go.uber.org/zap" ) @@ -485,19 +489,45 @@ func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) return } - bucketACL, err := h.obj.GetBucketACL(r.Context(), bktInfo) + resolvedNamespace := h.cfg.ResolveNamespaceAlias(reqInfo.Namespace) + jsonPolicy, err := h.ape.GetPolicy(resolvedNamespace, bktInfo.CID) if err != nil { - h.logAndSendError(w, "could not fetch bucket acl", reqInfo, err) + if strings.Contains(err.Error(), "not found") { + err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error()) + } + h.logAndSendError(w, "failed to get policy from storage", reqInfo, err) return } - ast := tableToAst(bucketACL.EACL, reqInfo.BucketName) - bktPolicy := astToPolicy(ast) - + w.Header().Set(api.ContentType, "application/json") w.WriteHeader(http.StatusOK) - if err = json.NewEncoder(w).Encode(bktPolicy); err != nil { - h.logAndSendError(w, "something went wrong", reqInfo, err) + if _, err = w.Write(jsonPolicy); err != nil { + h.logAndSendError(w, "write json policy to client", reqInfo, err) + } +} + +func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { + reqInfo := middleware.GetReqInfo(r.Context()) + + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket info", reqInfo, err) + return + } + + resolvedNamespace := h.cfg.ResolveNamespaceAlias(reqInfo.Namespace) + + target := engine.NamespaceTarget(resolvedNamespace) + chainID := getBucketChainID(bktInfo) + if err = h.ape.RemoveChain(target, chainID); err != nil { + h.logAndSendError(w, "failed to remove morph rule chain", reqInfo, err) + return + } + + if err = h.ape.DeletePolicy(resolvedNamespace, bktInfo.CID); err != nil { + h.logAndSendError(w, "failed to delete policy from storage", reqInfo, err) + return } } @@ -523,30 +553,52 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) return } - token, err := getSessionTokenSetEACL(r.Context()) + jsonPolicy, err := io.ReadAll(r.Body) if err != nil { - h.logAndSendError(w, "couldn't get eacl token", reqInfo, err) + h.logAndSendError(w, "read body", reqInfo, err) return } - bktPolicy := &bucketPolicy{Bucket: reqInfo.BucketName} - if err = json.NewDecoder(r.Body).Decode(bktPolicy); err != nil { + var bktPolicy engineiam.Policy + if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil { h.logAndSendError(w, "could not parse bucket policy", reqInfo, err) return } - astPolicy, err := policyToAst(bktPolicy) + s3Chain, err := engineiam.ConvertToS3Chain(bktPolicy, h.frostfsid) if err != nil { - h.logAndSendError(w, "could not translate policy to ast", reqInfo, err) + h.logAndSendError(w, "could not convert s3 policy to chain policy", reqInfo, err) + return + } + s3Chain.ID = getBucketChainID(bktInfo) + + for _, rule := range s3Chain.Rules { + for _, resource := range rule.Resources.Names { + if reqInfo.BucketName != strings.Split(resource, "/")[0] { + h.logAndSendError(w, "policy resource mismatched bucket", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicy)) + return + } + } + } + + resolvedNamespace := h.cfg.ResolveNamespaceAlias(reqInfo.Namespace) + + target := engine.NamespaceTarget(resolvedNamespace) + if err = h.ape.AddChain(target, s3Chain); err != nil { + h.logAndSendError(w, "failed to add morph rule chain", reqInfo, err) return } - if _, err = h.updateBucketACL(r, astPolicy, bktInfo, token); err != nil { - h.logAndSendError(w, "could not update bucket acl", reqInfo, err) + if err = h.ape.PutPolicy(resolvedNamespace, bktInfo.CID, jsonPolicy); err != nil { + h.logAndSendError(w, "failed to save policy to storage", reqInfo, err) return } } +func getBucketChainID(bktInfo *data.BucketInfo) chain.ID { + return chain.ID("bkt" + string(bktInfo.CID[:])) +} + func parseACLHeaders(header http.Header, key *keys.PublicKey) (*AccessControlPolicy, error) { var err error acp := &AccessControlPolicy{Owner: Owner{ diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index 64a6c557..c396b5f8 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -23,6 +23,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session" + engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" ) @@ -1316,42 +1317,26 @@ func TestBucketPolicy(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-for-policy" - box, key := createAccessBox(t) - createBucket(t, hc, bktName, box) + createTestBucket(hc, bktName) - bktPolicy := getBucketPolicy(hc, bktName) - for _, st := range bktPolicy.Statement { - if st.Effect == "Allow" { - require.Equal(t, hex.EncodeToString(key.PublicKey().Bytes()), st.Principal.CanonicalUser) - require.Equal(t, []string{arnAwsPrefix + bktName}, st.Resource) - } else { - require.Equal(t, allUsersWildcard, st.Principal.AWS) - require.Equal(t, "Deny", st.Effect) - require.Equal(t, []string{arnAwsPrefix + bktName}, st.Resource) - } - } + getBucketPolicy(hc, bktName, s3errors.ErrNoSuchBucketPolicy) - newPolicy := &bucketPolicy{ - Statement: []statement{{ - Effect: "Allow", - Principal: principal{AWS: allUsersWildcard}, - Action: []string{s3GetObject}, - Resource: []string{arnAwsPrefix + "dummy"}, + newPolicy := engineiam.Policy{ + 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, box, http.StatusInternalServerError) + putBucketPolicy(hc, bktName, newPolicy, s3errors.ErrMalformedPolicy) - newPolicy.Statement[0].Resource[0] = arnAwsPrefix + bktName - putBucketPolicy(hc, bktName, newPolicy, box, http.StatusOK) + newPolicy.Statement[0].Resource[0] = arnAwsPrefix + bktName + "/*" + putBucketPolicy(hc, bktName, newPolicy) - bktPolicy = getBucketPolicy(hc, bktName) - for _, st := range bktPolicy.Statement { - if st.Effect == "Allow" && st.Principal.AWS == allUsersWildcard { - require.Equal(t, []string{arnAwsPrefix + bktName}, st.Resource) - require.ElementsMatch(t, []string{s3GetObject, s3ListBucket}, st.Action) - } - } + bktPolicy := getBucketPolicy(hc, bktName) + require.Equal(t, newPolicy, bktPolicy) } func TestBucketPolicyUnmarshal(t *testing.T) { @@ -1411,9 +1396,7 @@ func TestPutBucketPolicy(t *testing.T) { { "Version": "2012-10-17", "Statement": [{ - "Principal": { - "AWS": "*" - }, + "Principal": "*", "Effect": "Deny", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::bucket-for-policy/*" @@ -1423,36 +1406,41 @@ func TestPutBucketPolicy(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-for-policy" - box, _ := createAccessBox(t) - createBucket(t, hc, bktName, box) + createTestBucket(hc, bktName) w, r := prepareTestPayloadRequest(hc, bktName, "", bytes.NewReader([]byte(bktPolicy))) - ctx := middleware.SetBoxData(r.Context(), box) - r = r.WithContext(ctx) hc.Handler().PutBucketPolicyHandler(w, r) assertStatus(hc.t, w, http.StatusOK) } -func getBucketPolicy(hc *handlerContext, bktName string) *bucketPolicy { +func getBucketPolicy(hc *handlerContext, bktName string, errCode ...s3errors.ErrorCode) engineiam.Policy { w, r := prepareTestRequest(hc, bktName, "", nil) hc.Handler().GetBucketPolicyHandler(w, r) - assertStatus(hc.t, w, http.StatusOK) - policy := &bucketPolicy{} - err := json.NewDecoder(w.Result().Body).Decode(policy) - require.NoError(hc.t, err) + 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 putBucketPolicy(hc *handlerContext, bktName string, bktPolicy *bucketPolicy, box *accessbox.Box, status int) { +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)) - ctx := middleware.SetBoxData(r.Context(), box) - r = r.WithContext(ctx) hc.Handler().PutBucketPolicyHandler(w, r) - assertStatus(hc.t, w, status) + + if len(errCode) == 0 { + assertStatus(hc.t, w, http.StatusOK) + } else { + assertS3Error(hc.t, w, s3errors.GetAPIError(errCode[0])) + } } func checkLastRecords(t *testing.T, tc *handlerContext, bktInfo *data.BucketInfo, action eacl.Action) { diff --git a/api/handler/api.go b/api/handler/api.go index 7f227063..061245cb 100644 --- a/api/handler/api.go +++ b/api/handler/api.go @@ -12,7 +12,10 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "go.uber.org/zap" ) @@ -22,6 +25,8 @@ type ( obj layer.Client notificator Notificator cfg Config + ape APE + frostfsid FrostFSID } Notificator interface { @@ -42,18 +47,52 @@ type ( IsResolveListAllow() bool BypassContentEncodingInChunks() bool MD5Enabled() bool + ResolveNamespaceAlias(namespace string) string } + + FrostFSID interface { + GetUserAddress(account, user string) (string, error) + } + + // APE is Access Policy Engine that needs to save policy and acl info to different places. + APE interface { + MorphRuleChainStorage + PolicyStorage + } + + // MorphRuleChainStorage is a similar to engine.MorphRuleChainStorage + // but doesn't know anything about tx. + MorphRuleChainStorage interface { + AddChain(target engine.Target, c *chain.Chain) error + RemoveChain(target engine.Target, chainID chain.ID) error + ListChains(target engine.Target) ([]*chain.Chain, error) + } + + // PolicyStorage is interface to save intact initial user provided policy. + PolicyStorage interface { + PutPolicy(namespace string, cnrID cid.ID, policy []byte) error + GetPolicy(namespace string, cnrID cid.ID) ([]byte, error) + DeletePolicy(namespace string, cnrID cid.ID) error + } + + frostfsIDDisabled struct{} ) var _ api.Handler = (*handler)(nil) // New creates new api.Handler using given logger and client. -func New(log *zap.Logger, obj layer.Client, notificator Notificator, cfg Config) (api.Handler, error) { +func New(log *zap.Logger, obj layer.Client, notificator Notificator, cfg Config, storage APE, ffsid FrostFSID) (api.Handler, error) { switch { case obj == nil: return nil, errors.New("empty FrostFS Object Layer") case log == nil: return nil, errors.New("empty logger") + case storage == nil: + return nil, errors.New("empty policy storage") + } + + if ffsid == nil { + ffsid = frostfsIDDisabled{} } if !cfg.NotificatorEnabled() { @@ -66,10 +105,16 @@ func New(log *zap.Logger, obj layer.Client, notificator Notificator, cfg Config) log: log, obj: obj, cfg: cfg, + ape: storage, notificator: notificator, + frostfsid: ffsid, }, nil } +func (f frostfsIDDisabled) GetUserAddress(_, _ string) (string, error) { + return "", errors.New("frostfsid disabled") +} + // pickCopiesNumbers chooses the return values following this logic: // 1) array of copies numbers sent in request's header has the highest priority. // 2) array of copies numbers with corresponding location constraint provided in the config file. diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go index 2ec942fe..f94cd573 100644 --- a/api/handler/handlers_test.go +++ b/api/handler/handlers_test.go @@ -5,6 +5,7 @@ import ( "context" "crypto/rand" "encoding/xml" + "errors" "io" "net/http" "net/http/httptest" @@ -26,9 +27,12 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" "go.uber.org/zap" + "golang.org/x/exp/slices" ) type handlerContext struct { @@ -116,6 +120,10 @@ func (c *configMock) MD5Enabled() bool { return c.md5Enabled } +func (c *configMock) ResolveNamespaceAlias(ns string) string { + return ns +} + func prepareHandlerContext(t *testing.T) *handlerContext { return prepareHandlerContextBase(t, false) } @@ -167,6 +175,7 @@ func prepareHandlerContextBase(t *testing.T, minCache bool) *handlerContext { log: l, obj: layer.NewLayer(l, tp, layerCfg), cfg: cfg, + ape: newAPEMock(), } return &handlerContext{ @@ -199,6 +208,60 @@ func getMinCacheConfig(logger *zap.Logger) *layer.CachesConfig { } } +type apeMock struct { + chainMap map[engine.Target][]*chain.Chain + policyMap map[string][]byte +} + +func newAPEMock() *apeMock { + return &apeMock{ + chainMap: map[engine.Target][]*chain.Chain{}, + policyMap: map[string][]byte{}, + } +} + +func (a *apeMock) AddChain(target engine.Target, c *chain.Chain) error { + list := a.chainMap[target] + + ind := slices.IndexFunc(list, func(item *chain.Chain) bool { return item.ID == c.ID }) + if ind != -1 { + list[ind] = c + } else { + list = append(list, c) + } + + a.chainMap[target] = list + return nil +} + +func (a *apeMock) RemoveChain(target engine.Target, chainID chain.ID) error { + a.chainMap[target] = slices.DeleteFunc(a.chainMap[target], func(item *chain.Chain) bool { return item.ID == chainID }) + return nil +} + +func (a *apeMock) ListChains(target engine.Target) ([]*chain.Chain, error) { + return a.chainMap[target], nil +} + +func (a *apeMock) PutPolicy(namespace string, cnrID cid.ID, policy []byte) error { + a.policyMap[namespace+cnrID.EncodeToString()] = policy + return nil +} + +func (a *apeMock) GetPolicy(namespace string, cnrID cid.ID) ([]byte, error) { + policy, ok := a.policyMap[namespace+cnrID.EncodeToString()] + if !ok { + return nil, errors.New("not found") + } + + return policy, nil +} + +func (a *apeMock) DeletePolicy(namespace string, cnrID cid.ID) error { + delete(a.policyMap, namespace+cnrID.EncodeToString()) + return nil +} + func NewTreeServiceMock(t *testing.T) *tree.Tree { memCli, err := tree.NewTreeServiceClientMemory() require.NoError(t, err) diff --git a/api/handler/not_support.go b/api/handler/not_support.go index defd9609..423e840b 100644 --- a/api/handler/not_support.go +++ b/api/handler/not_support.go @@ -7,10 +7,6 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" ) -func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { - h.logAndSendError(w, "not supported", middleware.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotSupported)) -} - func (h *handler) DeleteBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) { h.logAndSendError(w, "not supported", middleware.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotSupported)) } diff --git a/api/middleware/policy.go b/api/middleware/policy.go index 2246b99a..60f7b3ba 100644 --- a/api/middleware/policy.go +++ b/api/middleware/policy.go @@ -7,7 +7,6 @@ import ( "strings" apiErr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" - "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" @@ -52,7 +51,7 @@ func policyCheck(storage engine.ChainRouter, settings PolicySettings, domains [] reqInfo := GetReqInfo(r.Context()) target := engine.NewRequestTargetWithNamespace(settings.ResolveNamespaceAlias(reqInfo.Namespace)) - st, found, err := storage.IsAllowed(policy.S3ChainName, target, req) + st, found, err := storage.IsAllowed(chain.S3, target, req) if err != nil { return 0, err } diff --git a/api/router.go b/api/router.go index e6c7b66b..315a45e4 100644 --- a/api/router.go +++ b/api/router.go @@ -111,7 +111,7 @@ type Config struct { // FrostfsID optional. If nil middleware.FrostfsIDValidation won't be attached. FrostfsID s3middleware.FrostFSID - PolicyStorage engine.LocalOverrideEngine + PolicyChecker engine.ChainRouter } func NewRouter(cfg Config) *chi.Mux { @@ -130,8 +130,8 @@ func NewRouter(cfg Config) *chi.Mux { api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log)) } - if cfg.PolicyStorage != nil { - api.Use(s3middleware.PolicyCheck(cfg.PolicyStorage, cfg.MiddlewareSettings, cfg.Domains, cfg.Log)) + if cfg.PolicyChecker != nil { + api.Use(s3middleware.PolicyCheck(cfg.PolicyChecker, cfg.MiddlewareSettings, cfg.Domains, cfg.Log)) } defaultRouter := chi.NewRouter() @@ -320,7 +320,7 @@ func bucketRouter(h Handler, log *zap.Logger) chi.Router { Handler(named(s3middleware.DeleteBucketTaggingOperation, h.DeleteBucketTaggingHandler))). Add(NewFilter(). Queries(s3middleware.PolicyQuery). - Handler(named(s3middleware.PutBucketPolicyOperation, h.PutBucketPolicyHandler))). + Handler(named(s3middleware.DeleteBucketPolicyOperation, h.DeleteBucketPolicyHandler))). Add(NewFilter(). Queries(s3middleware.LifecycleQuery). Handler(named(s3middleware.PutBucketLifecycleOperation, h.PutBucketLifecycleHandler))). diff --git a/api/router_test.go b/api/router_test.go index 14751b53..327a0586 100644 --- a/api/router_test.go +++ b/api/router_test.go @@ -27,6 +27,7 @@ type routerMock struct { router *chi.Mux cfg Config middlewareSettings *middlewareSettingsMock + policyChecker engine.LocalOverrideEngine } func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -35,6 +36,7 @@ func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) { func prepareRouter(t *testing.T) *routerMock { middlewareSettings := &middlewareSettingsMock{} + policyChecker := inmemory.NewInMemoryLocalOverrides() cfg := Config{ Throttle: middleware.ThrottleOpts{ @@ -46,12 +48,13 @@ func prepareRouter(t *testing.T) *routerMock { Log: zaptest.NewLogger(t), Metrics: &metrics.AppMetrics{}, MiddlewareSettings: middlewareSettings, - PolicyStorage: inmemory.NewInMemoryLocalOverrides(), + PolicyChecker: policyChecker, } return &routerMock{ router: NewRouter(cfg), cfg: cfg, middlewareSettings: middlewareSettings, + policyChecker: policyChecker, } } @@ -164,7 +167,7 @@ func TestPolicyChecker(t *testing.T) { }}, } - _, _, err := chiRouter.cfg.PolicyStorage.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(namespace), ruleChain) + _, _, err := chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(namespace), ruleChain) require.NoError(t, err) // check we can access 'bucket' in default namespace diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go index 8ae511a8..f68965af 100644 --- a/cmd/s3-gw/app.go +++ b/cmd/s3-gw/app.go @@ -43,8 +43,6 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" - "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" - "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" "github.com/go-chi/chi/v5/middleware" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/spf13/viper" @@ -70,7 +68,7 @@ type ( frostfsid *frostfsid.FrostFSID - policyStorage engine.LocalOverrideEngine + policyStorage *policy.Storage servers []Server @@ -144,11 +142,11 @@ func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App { func (a *App) init(ctx context.Context) { a.setRuntimeParameters() - a.initAPI(ctx) - a.initPolicyStorage(ctx) - a.initControlAPI() - a.initMetrics() a.initFrostfsID(ctx) + a.initPolicyStorage(ctx) + a.initAPI(ctx) + a.initMetrics() + a.initControlAPI() a.initServers(ctx) a.initTracing(ctx) } @@ -452,27 +450,29 @@ func (a *App) initFrostfsID(ctx context.Context) { } func (a *App) initPolicyStorage(ctx context.Context) { - if !a.cfg.GetBool(cfgPolicyEnabled) { - a.policyStorage = inmemory.NewInMemoryLocalOverrides() - return + var ( + err error + policyContract policy.Contract + ) + + if a.cfg.GetBool(cfgPolicyEnabled) { + policyContract, err = contract.New(ctx, contract.Config{ + RPCAddress: a.cfg.GetString(cfgRPCEndpoint), + Contract: a.cfg.GetString(cfgPolicyContract), + Key: a.key, + }) + if err != nil { + a.log.Fatal(logs.InitPolicyContractFailed, zap.Error(err)) + } + } else { + policyContract = contract.NewInMemoryContract() } - policyClient, err := contract.New(ctx, contract.Config{ - RPCAddress: a.cfg.GetString(cfgRPCEndpoint), - Contract: a.cfg.GetString(cfgPolicyContract), - Key: a.key, + a.policyStorage = policy.NewStorage(policy.StorageConfig{ + Contract: policyContract, + Cache: cache.NewMorphPolicyCache(getMorphPolicyCacheConfig(a.cfg, a.log)), + Log: a.log, }) - if err != nil { - a.log.Fatal(logs.InitPolicyContractFailed, zap.Error(err)) - } - - cachedMorph := policy.NewCachedMorph(policy.CachedMorphConfig{ - Morph: policyClient, - Cache: cache.NewMorphPolicyCache(getMorphPolicyCacheConfig(a.cfg, a.log)), - Log: a.log, - }) - - a.policyStorage = policy.NewStorage(cachedMorph) } func (a *App) initResolver() { @@ -672,7 +672,7 @@ func (a *App) Serve(ctx context.Context) { Domains: domains, MiddlewareSettings: a.settings, - PolicyStorage: a.policyStorage, + PolicyChecker: a.policyStorage, } // We cannot make direct assignment if frostfsid.FrostFSID is nil @@ -943,8 +943,16 @@ func getMorphPolicyCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config { } func (a *App) initHandler() { - var err error - a.api, err = handler.New(a.log, a.obj, a.nc, a.settings) + var ( + err error + ffsid handler.FrostFSID + ) + + if a.frostfsid != nil { + ffsid = a.frostfsid + } + + a.api, err = handler.New(a.log, a.obj, a.nc, a.settings, a.policyStorage, ffsid) if err != nil { a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err)) } diff --git a/internal/frostfs/frostfsid/frostfsid.go b/internal/frostfs/frostfsid/frostfsid.go index d2f3dc13..263dac7e 100644 --- a/internal/frostfs/frostfsid/frostfsid.go +++ b/internal/frostfs/frostfsid/frostfsid.go @@ -6,9 +6,10 @@ import ( "strings" "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" - "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util" + frostfsutil "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -33,11 +34,12 @@ type Config struct { var ( _ middleware.FrostFSID = (*FrostFSID)(nil) _ authmate.FrostFSID = (*FrostFSID)(nil) + _ handler.FrostFSID = (*FrostFSID)(nil) ) // New creates new FrostfsID contract wrapper that implements auth.FrostFSID interface. func New(ctx context.Context, cfg Config) (*FrostFSID, error) { - contractHash, err := util.ResolveContractHash(cfg.Contract, cfg.RPCAddress) + contractHash, err := frostfsutil.ResolveContractHash(cfg.Contract, cfg.RPCAddress) if err != nil { return nil, fmt.Errorf("resolve frostfs contract hash: %w", err) } @@ -77,3 +79,12 @@ func (f *FrostFSID) RegisterPublicKey(key *keys.PublicKey) error { return nil } + +func (f *FrostFSID) GetUserAddress(namespace, name string) (string, error) { + key, err := f.cli.GetSubjectKeyByName(namespace, name) + if err != nil { + return "", err + } + + return key.Address(), nil +} diff --git a/internal/frostfs/policy/cached_morph.go b/internal/frostfs/policy/cached_morph.go deleted file mode 100644 index fe336609..00000000 --- a/internal/frostfs/policy/cached_morph.go +++ /dev/null @@ -1,61 +0,0 @@ -package policy - -import ( - "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache" - "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" - "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" - "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" - "github.com/nspcc-dev/neo-go/pkg/util" - "go.uber.org/zap" -) - -type CachedMorph struct { - morph engine.MorphRuleChainStorage - cache *cache.MorphPolicyCache - log *zap.Logger -} - -type CachedMorphConfig struct { - Morph engine.MorphRuleChainStorage - Cache *cache.MorphPolicyCache - Log *zap.Logger -} - -var _ engine.MorphRuleChainStorage = (*CachedMorph)(nil) - -func NewCachedMorph(config CachedMorphConfig) *CachedMorph { - return &CachedMorph{ - morph: config.Morph, - cache: config.Cache, - log: config.Log, - } -} - -func (c *CachedMorph) AddMorphRuleChain(name chain.Name, target engine.Target, policyChain *chain.Chain) (util.Uint256, uint32, error) { - c.cache.Delete(cache.MorphPolicyCacheKey{Target: target, Name: name}) - return c.morph.AddMorphRuleChain(name, target, policyChain) -} - -func (c *CachedMorph) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (util.Uint256, uint32, error) { - c.cache.Delete(cache.MorphPolicyCacheKey{Target: target, Name: name}) - return c.morph.RemoveMorphRuleChain(name, target, chainID) -} - -func (c *CachedMorph) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { - key := cache.MorphPolicyCacheKey{Target: target, Name: name} - list := c.cache.Get(key) - if list != nil { - return list, nil - } - - list, err := c.morph.ListMorphRuleChains(name, target) - if err != nil { - return nil, err - } - - if err = c.cache.Put(key, list); err != nil { - c.log.Warn(logs.CouldntCacheListPolicyChains) - } - - return list, nil -} diff --git a/internal/frostfs/policy/contract/contract.go b/internal/frostfs/policy/contract/contract.go index 949427e0..94c72992 100644 --- a/internal/frostfs/policy/contract/contract.go +++ b/internal/frostfs/policy/contract/contract.go @@ -7,9 +7,8 @@ import ( policycontract "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" policyclient "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy" frostfsutil "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util" - "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" - "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" @@ -34,7 +33,7 @@ type Config struct { Key *keys.PrivateKey } -var _ engine.MorphRuleChainStorage = (*Client)(nil) +var _ policy.Contract = (*Client)(nil) // New creates new Policy contract wrapper. func New(ctx context.Context, cfg Config) (*Client, error) { @@ -67,45 +66,36 @@ func New(ctx context.Context, cfg Config) (*Client, error) { }, nil } -func (c *Client) AddMorphRuleChain(name chain.Name, target engine.Target, policyChain *chain.Chain) (util.Uint256, uint32, error) { - chainName := append([]byte(name), []byte(policyChain.ID)...) - return c.policyContract.AddChain(getKind(target), target.Name, chainName, policyChain.Bytes()) +func (c *Client) AddChain(kind policycontract.Kind, entity string, name []byte, chain []byte) (util.Uint256, uint32, error) { + return c.policyContract.AddChain(big.NewInt(int64(kind)), entity, name, chain) } -func (c *Client) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (util.Uint256, uint32, error) { - chainName := append([]byte(name), []byte(chainID)...) - return c.policyContract.RemoveChain(getKind(target), target.Name, chainName) +func (c *Client) GetChain(kind policycontract.Kind, entity string, name []byte) ([]byte, error) { + return c.policyContract.GetChain(big.NewInt(int64(kind)), entity, name) } -func (c *Client) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { - items, err := c.policyContract.ListChainsByPrefix(getKind(target), target.Name, []byte(name)) +func (c *Client) RemoveChain(kind policycontract.Kind, entity string, name []byte) (util.Uint256, uint32, error) { + return c.policyContract.RemoveChain(big.NewInt(int64(kind)), entity, name) +} + +func (c *Client) ListChains(kind policycontract.Kind, entity string, name []byte) ([][]byte, error) { + items, err := c.policyContract.ListChainsByPrefix(big.NewInt(int64(kind)), entity, name) if err != nil { return nil, err } - res := make([]*chain.Chain, len(items)) + res := make([][]byte, len(items)) for i, item := range items { - data, err := item.TryBytes() + res[i], err = item.TryBytes() if err != nil { return nil, err } - - var policyChain chain.Chain - if err = policyChain.DecodeBytes(data); err != nil { - return nil, err - } - - res[i] = &policyChain } return res, nil } -func getKind(target engine.Target) *big.Int { - var kind int64 = policycontract.Container - if target.Type != engine.Container { - kind = policycontract.Namespace - } - - return big.NewInt(kind) +func (c *Client) Wait(tx util.Uint256, vub uint32, err error) error { + _, err = c.actor.Wait(tx, vub, err) + return err } diff --git a/internal/frostfs/policy/contract/inmemory.go b/internal/frostfs/policy/contract/inmemory.go new file mode 100644 index 00000000..b49efbd4 --- /dev/null +++ b/internal/frostfs/policy/contract/inmemory.go @@ -0,0 +1,97 @@ +package contract + +import ( + "errors" + "strings" + "sync" + + policycontract "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +type InMemoryContract struct { + iamChains *syncedMap + containerChains *syncedMap + namespaceChains *syncedMap +} + +type syncedMap struct { + mu sync.RWMutex + data map[string][]byte +} + +var _ policy.Contract = (*InMemoryContract)(nil) + +var ErrChainNotFound = errors.New("chain not found") + +// NewInMemoryContract creates new inmemory Policy contract wrapper. +func NewInMemoryContract() *InMemoryContract { + return &InMemoryContract{ + iamChains: &syncedMap{data: map[string][]byte{}}, + containerChains: &syncedMap{data: map[string][]byte{}}, + namespaceChains: &syncedMap{data: map[string][]byte{}}, + } +} + +func (c *InMemoryContract) AddChain(kind policycontract.Kind, entity string, name []byte, chain []byte) (util.Uint256, uint32, error) { + syncMap := c.getMap(kind) + syncMap.mu.Lock() + syncMap.data[entity+string(name)] = chain + syncMap.mu.Unlock() + + return util.Uint256{}, 0, nil +} + +func (c *InMemoryContract) GetChain(kind policycontract.Kind, entity string, name []byte) ([]byte, error) { + syncMap := c.getMap(kind) + syncMap.mu.RLock() + defer syncMap.mu.RUnlock() + + val, ok := syncMap.data[entity+string(name)] + if !ok { + return nil, ErrChainNotFound + } + return val, nil +} + +func (c *InMemoryContract) RemoveChain(kind policycontract.Kind, entity string, name []byte) (util.Uint256, uint32, error) { + syncMap := c.getMap(kind) + syncMap.mu.Lock() + delete(syncMap.data, entity+string(name)) + syncMap.mu.Unlock() + + return util.Uint256{}, 0, nil +} + +func (c *InMemoryContract) ListChains(kind policycontract.Kind, entity string, name []byte) ([][]byte, error) { + syncMap := c.getMap(kind) + syncMap.mu.RLock() + defer syncMap.mu.RUnlock() + + var res [][]byte + for key, val := range syncMap.data { + if strings.HasPrefix(key, entity+string(name)) { + res = append(res, val) + } + } + + return res, nil +} + +func (c *InMemoryContract) Wait(_ util.Uint256, _ uint32, err error) error { + return err +} + +func (c *InMemoryContract) getMap(kind policycontract.Kind) *syncedMap { + switch kind { + case policycontract.IAM: + return c.iamChains + case policycontract.Container: + return c.containerChains + case policycontract.Namespace: + return c.namespaceChains + default: + return &syncedMap{data: map[string][]byte{}} + } +} diff --git a/internal/frostfs/policy/morph_policy_storage.go b/internal/frostfs/policy/morph_policy_storage.go new file mode 100644 index 00000000..3db093f7 --- /dev/null +++ b/internal/frostfs/policy/morph_policy_storage.go @@ -0,0 +1,46 @@ +package policy + +import ( + policycontract "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" + "go.uber.org/zap" +) + +type MorphPolicyStorage struct { + contract Contract +} + +type MorphPolicyStorageConfig struct { + Contract Contract + Log *zap.Logger +} + +var _ handler.PolicyStorage = (*MorphPolicyStorage)(nil) + +const policyStoragePrefix = 'b' + +func NewMorphPolicyStorage(config *MorphPolicyStorageConfig) *MorphPolicyStorage { + return &MorphPolicyStorage{ + contract: config.Contract, + } +} + +func (c *MorphPolicyStorage) PutPolicy(namespace string, cnrID cid.ID, policy []byte) error { + name := getPolicyStorageName(cnrID) + return c.contract.Wait(c.contract.AddChain(policycontract.IAM, namespace, name, policy)) +} + +func (c *MorphPolicyStorage) GetPolicy(namespace string, cnrID cid.ID) ([]byte, error) { + name := getPolicyStorageName(cnrID) + return c.contract.GetChain(policycontract.IAM, namespace, name) +} + +func (c *MorphPolicyStorage) DeletePolicy(namespace string, cnrID cid.ID) error { + name := getPolicyStorageName(cnrID) + return c.contract.Wait(c.contract.RemoveChain(policycontract.IAM, namespace, name)) +} + +func getPolicyStorageName(cnrID cid.ID) []byte { + return append([]byte{policyStoragePrefix}, cnrID[:]...) +} diff --git a/internal/frostfs/policy/morph_rule_chain_storage.go b/internal/frostfs/policy/morph_rule_chain_storage.go new file mode 100644 index 00000000..9148c566 --- /dev/null +++ b/internal/frostfs/policy/morph_rule_chain_storage.go @@ -0,0 +1,102 @@ +package policy + +import ( + "encoding/json" + "fmt" + + policycontract "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "github.com/nspcc-dev/neo-go/pkg/util" + "go.uber.org/zap" +) + +type MorphRuleChainStorage struct { + contract Contract + cache *cache.MorphPolicyCache + log *zap.Logger +} + +type MorphRuleChainStorageConfig struct { + Contract Contract + Cache *cache.MorphPolicyCache + Log *zap.Logger +} + +var ( + _ engine.MorphRuleChainStorage = (*MorphRuleChainStorage)(nil) + _ handler.MorphRuleChainStorage = (*MorphRuleChainStorage)(nil) +) + +func NewMorphRuleChainStorage(config *MorphRuleChainStorageConfig) *MorphRuleChainStorage { + return &MorphRuleChainStorage{ + contract: config.Contract, + cache: config.Cache, + log: config.Log, + } +} + +func (c *MorphRuleChainStorage) AddChain(target engine.Target, policyChain *chain.Chain) error { + return c.contract.Wait(c.AddMorphRuleChain(chain.S3, target, policyChain)) +} + +func (c *MorphRuleChainStorage) RemoveChain(target engine.Target, chainID chain.ID) error { + return c.contract.Wait(c.RemoveMorphRuleChain(chain.S3, target, chainID)) +} + +func (c *MorphRuleChainStorage) ListChains(target engine.Target) ([]*chain.Chain, error) { + return c.ListMorphRuleChains(chain.S3, target) +} + +func (c *MorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, policyChain *chain.Chain) (util.Uint256, uint32, error) { + c.cache.Delete(cache.MorphPolicyCacheKey{Target: target, Name: name}) + return c.contract.AddChain(getKind(target), target.Name, getName(name, policyChain.ID), policyChain.Bytes()) +} + +func (c *MorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (util.Uint256, uint32, error) { + c.cache.Delete(cache.MorphPolicyCacheKey{Target: target, Name: name}) + return c.contract.RemoveChain(getKind(target), target.Name, getName(name, chainID)) +} + +func (c *MorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { + key := cache.MorphPolicyCacheKey{Target: target, Name: name} + list := c.cache.Get(key) + if list != nil { + return list, nil + } + + listChains, err := c.contract.ListChains(getKind(target), target.Name, []byte(name)) + if err != nil { + return nil, err + } + + list = make([]*chain.Chain, len(listChains)) + for i, listChain := range listChains { + var item chain.Chain + if err = json.Unmarshal(listChain, &item); err != nil { + return nil, fmt.Errorf("unmarshal chain: %w", err) + } + list[i] = &item + } + + if err = c.cache.Put(key, list); err != nil { + c.log.Warn(logs.CouldntCacheListPolicyChains) + } + + return list, nil +} + +func getKind(target engine.Target) policycontract.Kind { + var kind policycontract.Kind = policycontract.Container + if target.Type != engine.Container { + kind = policycontract.Namespace + } + + return kind +} +func getName(name chain.Name, chainID chain.ID) []byte { + return append([]byte(name), []byte(chainID)...) +} diff --git a/internal/frostfs/policy/storage.go b/internal/frostfs/policy/storage.go index 8c9c35be..b53fee78 100644 --- a/internal/frostfs/policy/storage.go +++ b/internal/frostfs/policy/storage.go @@ -1,42 +1,95 @@ package policy import ( + policycontract "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" + "github.com/nspcc-dev/neo-go/pkg/util" + "go.uber.org/zap" ) -const S3ChainName chain.Name = "s3" - type Storage struct { router engine.ChainRouter - morph engine.MorphRuleChainStorage + morph handler.MorphRuleChainStorage local engine.LocalOverrideStorage + + policy handler.PolicyStorage } -var _ engine.LocalOverrideEngine = (*Storage)(nil) +type StorageConfig struct { + Contract Contract + Cache *cache.MorphPolicyCache + Log *zap.Logger +} -func NewStorage(morph engine.MorphRuleChainStorage) *Storage { +type Contract interface { + AddChain(kind policycontract.Kind, entity string, name []byte, chain []byte) (util.Uint256, uint32, error) + GetChain(kind policycontract.Kind, entity string, name []byte) ([]byte, error) + RemoveChain(kind policycontract.Kind, entity string, name []byte) (util.Uint256, uint32, error) + ListChains(kind policycontract.Kind, entity string, name []byte) ([][]byte, error) + Wait(tx util.Uint256, vub uint32, err error) error +} + +var _ handler.APE = (*Storage)(nil) + +func NewStorage(cfg StorageConfig) *Storage { + // todo use thread safe inmemory https://git.frostfs.info/TrueCloudLab/policy-engine/issues/35 local := inmemory.NewInmemoryLocalStorage() + morph := NewMorphRuleChainStorage(&MorphRuleChainStorageConfig{ + Contract: cfg.Contract, + Cache: cfg.Cache, + Log: cfg.Log, + }) + + policyStorage := NewMorphPolicyStorage(&MorphPolicyStorageConfig{ + Contract: cfg.Contract, + Log: cfg.Log, + }) + return &Storage{ router: engine.NewDefaultChainRouterWithLocalOverrides(morph, local), morph: morph, local: local, + policy: policyStorage, } } -func (s Storage) IsAllowed(name chain.Name, target engine.RequestTarget, r resource.Request) (status chain.Status, found bool, err error) { +func (s *Storage) IsAllowed(name chain.Name, target engine.RequestTarget, r resource.Request) (status chain.Status, found bool, err error) { return s.router.IsAllowed(name, target, r) } -func (s Storage) MorphRuleChainStorage() engine.MorphRuleChainStorage { - return s.morph -} - -func (s Storage) LocalStorage() engine.LocalOverrideStorage { +func (s *Storage) LocalStorage() engine.LocalOverrideStorage { return s.local } + +func (s *Storage) AddChain(target engine.Target, policyChain *chain.Chain) error { + return s.morph.AddChain(target, policyChain) +} + +func (s *Storage) RemoveChain(target engine.Target, chainID chain.ID) error { + return s.morph.RemoveChain(target, chainID) +} + +func (s *Storage) ListChains(target engine.Target) ([]*chain.Chain, error) { + return s.morph.ListChains(target) +} + +func (s *Storage) PutPolicy(namespace string, cnrID cid.ID, policy []byte) error { + return s.policy.PutPolicy(namespace, cnrID, policy) +} + +func (s *Storage) GetPolicy(namespace string, cnrID cid.ID) ([]byte, error) { + return s.policy.GetPolicy(namespace, cnrID) +} + +func (s *Storage) DeletePolicy(namespace string, cnrID cid.ID) error { + return s.policy.DeletePolicy(namespace, cnrID) +}