From 9272f4e10838f49f477bcd98ff451ccc57b69211 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Tue, 5 Dec 2023 12:12:35 +0300 Subject: [PATCH] [#259] Support contract based policies Signed-off-by: Denis Kirillov --- CHANGELOG.md | 1 + api/cache/policy.go | 72 ++++++++++++ api/middleware/policy.go | 3 +- api/router_test.go | 3 +- cmd/s3-gw/app.go | 44 +++++++- cmd/s3-gw/app_settings.go | 10 ++ config/config.env | 9 ++ config/config.yaml | 11 ++ docs/configuration.md | 20 ++++ go.mod | 2 +- go.sum | 4 +- internal/frostfs/frostfsid/frostfsid.go | 28 +---- internal/frostfs/policy/cached_morph.go | 60 ++++++++++ internal/frostfs/policy/contract/contract.go | 112 +++++++++++++++++++ internal/frostfs/policy/storage.go | 42 +++++++ internal/frostfs/util/util.go | 33 ++++++ internal/logs/logs.go | 2 + pkg/service/control/server/server.go | 15 +-- 18 files changed, 428 insertions(+), 43 deletions(-) create mode 100644 api/cache/policy.go create mode 100644 internal/frostfs/policy/cached_morph.go create mode 100644 internal/frostfs/policy/contract/contract.go create mode 100644 internal/frostfs/policy/storage.go create mode 100644 internal/frostfs/util/util.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 94561208..cb8ac958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This document outlines major changes between releases. - Support control api to manage policies. See `control` config section (#258) - Add `namespace` label to billing metrics (#271) - Support policy-engine (#257) +- Support `policy` contract (#259) ### Changed - Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221) diff --git a/api/cache/policy.go b/api/cache/policy.go new file mode 100644 index 00000000..a4ade12d --- /dev/null +++ b/api/cache/policy.go @@ -0,0 +1,72 @@ +package cache + +import ( + "fmt" + "time" + + "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/bluele/gcache" + "go.uber.org/zap" +) + +// MorphPolicyCache provides lru cache for listing policies stored in policy contract. +type MorphPolicyCache struct { + cache gcache.Cache + logger *zap.Logger +} + +type MorphPolicyCacheKey struct { + Target engine.Target + Name chain.Name +} + +const ( + // DefaultMorphPolicyCacheSize is a default maximum number of entries in cache. + DefaultMorphPolicyCacheSize = 1e4 + // DefaultMorphPolicyCacheLifetime is a default lifetime of entries in cache. + DefaultMorphPolicyCacheLifetime = time.Minute +) + +// DefaultMorphPolicyConfig returns new default cache expiration values. +func DefaultMorphPolicyConfig(logger *zap.Logger) *Config { + return &Config{ + Size: DefaultMorphPolicyCacheSize, + Lifetime: DefaultMorphPolicyCacheLifetime, + Logger: logger, + } +} + +// NewMorphPolicyCache creates an object of MorphPolicyCache. +func NewMorphPolicyCache(config *Config) *MorphPolicyCache { + gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build() + return &MorphPolicyCache{cache: gc, logger: config.Logger} +} + +// Get returns a cached object. Returns nil if value is missing. +func (o *MorphPolicyCache) Get(key MorphPolicyCacheKey) []*chain.Chain { + entry, err := o.cache.Get(key) + if err != nil { + return nil + } + + result, ok := entry.([]*chain.Chain) + if !ok { + o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), + zap.String("expected", fmt.Sprintf("%T", result))) + return nil + } + + return result +} + +// Put puts an object to cache. +func (o *MorphPolicyCache) Put(key MorphPolicyCacheKey, list []*chain.Chain) error { + return o.cache.Set(key, list) +} + +// Delete deletes an object from cache. +func (o *MorphPolicyCache) Delete(key MorphPolicyCacheKey) bool { + return o.cache.Remove(key) +} diff --git a/api/middleware/policy.go b/api/middleware/policy.go index c6a34e50..2246b99a 100644 --- a/api/middleware/policy.go +++ b/api/middleware/policy.go @@ -7,6 +7,7 @@ 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" @@ -51,7 +52,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(chain.Ingress, target, req) + st, found, err := storage.IsAllowed(policy.S3ChainName, target, req) if err != nil { return 0, err } diff --git a/api/router_test.go b/api/router_test.go index 56e03d44..2e764c03 100644 --- a/api/router_test.go +++ b/api/router_test.go @@ -13,6 +13,7 @@ import ( apiErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" s3middleware "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" @@ -164,7 +165,7 @@ func TestPolicyChecker(t *testing.T) { }}, } - err := chiRouter.cfg.PolicyStorage.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), ruleChain) + err := chiRouter.cfg.PolicyStorage.MorphRuleChainStorage().AddMorphRuleChain(policy.S3ChainName, 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 0d20be3f..6f1a9bc1 100644 --- a/cmd/s3-gw/app.go +++ b/cmd/s3-gw/app.go @@ -30,6 +30,8 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy/contract" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/services" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version" @@ -144,6 +146,7 @@ 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) @@ -271,10 +274,10 @@ func (s *appSettings) DefaultPlacementPolicy(namespace string) netmap.PlacementP func (s *appSettings) PlacementPolicy(namespace, constraint string) (netmap.PlacementPolicy, bool) { s.mu.RLock() - policy, ok := s.namespaces[namespace].LocationConstraints[constraint] + placementPolicy, ok := s.namespaces[namespace].LocationConstraints[constraint] s.mu.RUnlock() - return policy, ok + return placementPolicy, ok } func (s *appSettings) CopiesNumbers(namespace, constraint string) ([]uint32, bool) { @@ -417,12 +420,10 @@ func (a *App) initAPI(ctx context.Context) { } func (a *App) initControlAPI() { - a.policyStorage = inmemory.NewInMemoryLocalOverrides() - svc := controlSvc.New( controlSvc.WithSettings(a.settings), controlSvc.WithLogger(a.log), - controlSvc.WithChainStorage(a.policyStorage), + controlSvc.WithChainStorage(a.policyStorage.LocalStorage()), ) a.controlAPI = grpc.NewServer() @@ -451,6 +452,30 @@ func (a *App) initFrostfsID(ctx context.Context) { } } +func (a *App) initPolicyStorage(ctx context.Context) { + if !a.cfg.GetBool(cfgPolicyEnabled) { + a.policyStorage = inmemory.NewInMemoryLocalOverrides() + return + } + + policyClient, 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)) + } + + 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() { var err error a.bucketResolver, err = resolver.NewBucketResolver(a.getResolverOrder(), a.getResolverConfig()) @@ -919,6 +944,15 @@ func getAccessBoxCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config { return cacheCfg } +func getMorphPolicyCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config { + cacheCfg := cache.DefaultMorphPolicyConfig(l) + + cacheCfg.Lifetime = fetchCacheLifetime(v, l, cfgMorphPolicyCacheLifetime, cacheCfg.Lifetime) + cacheCfg.Size = fetchCacheSize(v, l, cfgMorphPolicyCacheSize, cacheCfg.Size) + + return cacheCfg +} + func (a *App) initHandler() { var err error a.api, err = handler.New(a.log, a.obj, a.nc, a.settings) diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go index fa05f74b..3715e049 100644 --- a/cmd/s3-gw/app_settings.go +++ b/cmd/s3-gw/app_settings.go @@ -108,6 +108,8 @@ const ( // Settings. cfgAccessBoxCacheSize = "cache.accessbox.size" cfgAccessControlCacheLifetime = "cache.accesscontrol.lifetime" cfgAccessControlCacheSize = "cache.accesscontrol.size" + cfgMorphPolicyCacheLifetime = "cache.morph_policy.lifetime" + cfgMorphPolicyCacheSize = "cache.morph_policy.size" // NATS. cfgEnableNATS = "nats.enabled" @@ -207,6 +209,10 @@ const ( // Settings. cfgFrostfsIDEnabled = "frostfsid.enabled" cfgFrostfsIDContract = "frostfsid.contract" + // Policy. + cfgPolicyEnabled = "policy.enabled" + cfgPolicyContract = "policy.contract" + // envPrefix is an environment variables prefix used for configuration. envPrefix = "S3_GW" ) @@ -689,6 +695,10 @@ func newSettings() *viper.Viper { v.SetDefault(cfgFrostfsIDContract, "frostfsid.frostfs") v.SetDefault(cfgFrostfsIDEnabled, true) + // policy + v.SetDefault(cfgPolicyContract, "policy.frostfs") + v.SetDefault(cfgPolicyEnabled, true) + // resolve v.SetDefault(cfgResolveNamespaceHeader, defaultNamespaceHeader) diff --git a/config/config.env b/config/config.env index bc8498ad..12881423 100644 --- a/config/config.env +++ b/config/config.env @@ -97,6 +97,9 @@ S3_GW_CACHE_ACCESSBOX_SIZE=100 # Cache which stores owner to cache operation mapping S3_GW_CACHE_ACCESSCONTROL_LIFETIME=1m S3_GW_CACHE_ACCESSCONTROL_SIZE=100000 +# Cache which stores list of policy chains +S3_GW_CACHE_MORPH_POLICY_LIFETIME=1m +S3_GW_CACHE_MORPH_POLICY_SIZE=10000 # NATS S3_GW_NATS_ENABLED=true @@ -195,6 +198,12 @@ S3_GW_FROSTFSID_ENABLED=true # FrostfsID contract hash (LE) or name in NNS. S3_GW_FROSTFSID_CONTRACT=frostfsid.frostfs +# Policy contract configuration. To enable this functionality the `rpc_endpoint` param must be also set. +# Enables using policies from Policy contract. +S3_GW_POLICY_ENABLED=true +# Policy contract hash (LE) or name in NNS. +S3_GW_POLICY_CONTRACT=policy.frostfs + # Namespaces configuration S3_GW_NAMESPACES_CONFIG=namespaces.json diff --git a/config/config.yaml b/config/config.yaml index 776e1c04..9255d3dc 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -120,6 +120,10 @@ cache: accesscontrol: lifetime: 1m size: 100000 + # Cache which stores list of policy chains + morph_policy: + lifetime: 1m + size: 10000 nats: enabled: true @@ -229,5 +233,12 @@ frostfsid: # FrostfsID contract hash (LE) or name in NNS. contract: frostfsid.frostfs +# Policy contract configuration. To enable this functionality the `rpc_endpoint` param must be also set. +policy: + # Enables using policies from Policy contract. + enabled: true + # Policy contract hash (LE) or name in NNS. + contract: policy.frostfs + namespaces: config: namespaces.json diff --git a/docs/configuration.md b/docs/configuration.md index 984551d3..9ebacf37 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -190,6 +190,7 @@ There are some custom types used for brevity: | `features` | [Features configuration](#features-section) | | `web` | [Web server configuration](#web-section) | | `frostfsid` | [FrostfsID configuration](#frostfsid-section) | +| `policy` | [Policy contract configuration](#policy-section) | | `namespaces` | [Namespaces configuration](#namespaces-section) | ### General section @@ -409,6 +410,9 @@ cache: accesscontrol: lifetime: 1m size: 100000 + morph_policy: + lifetime: 30s + size: 10000 ``` | Parameter | Type | Default value | Description | @@ -420,6 +424,7 @@ cache: | `system` | [Cache config](#cache-subsection) | `lifetime: 5m`
`size: 10000` | Cache for system objects in a bucket: bucket settings, notification configuration etc. | | `accessbox` | [Cache config](#cache-subsection) | `lifetime: 10m`
`size: 100` | Cache which stores access box with tokens by its address. | | `accesscontrol` | [Cache config](#cache-subsection) | `lifetime: 1m`
`size: 100000` | Cache which stores owner to cache operation mapping. | +| `morph_policy` | [Cache config](#cache-subsection) | `lifetime: 1m`
`size: 10000` | Cache which stores list of policy chains. | #### `cache` subsection @@ -641,6 +646,21 @@ frostfsid: | `enabled` | `bool` | no | true | Enables check that allow requests only users that is registered in FrostfsID contract. | | `contract` | `string` | no | frostfsid.frostfs | FrostfsID contract hash (LE) or name in NNS. | +# `policy` section + +Policy contract configuration. To enable this functionality the `rpc_endpoint` param must be also set. + +```yaml +policy: + enabled: false + contract: policy.frostfs +``` + +| Parameter | Type | SIGHUP reload | Default value | Description | +|------------|----------|---------------|----------------|-------------------------------------------------------------------| +| `enabled` | `bool` | no | true | Enables using policies from Policy contract to check permissions. | +| `contract` | `string` | no | policy.frostfs | Policy contract hash (LE) or name in NNS. | + # `namespaces` section Namespaces configuration. diff --git a/go.mod b/go.mod index 1b981598..7c88e1bb 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20231121085847-241a9f1ad0a4 - git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231109143925-dd5919348da9 + git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20230531082742-c97d21411eb6 git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231107114540-ab75edd70939 git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231205092054-2d4a9fc6dcb3 diff --git a/go.sum b/go.sum index e797cc22..a2b49b9f 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20231121085847-241a9f1ad0a4 h1:wjLfZ3WCt7qNGsQv+Jl0TXnmtg0uVk/jToKPFTBc/jo= git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20231121085847-241a9f1ad0a4/go.mod h1:uY0AYmCznjZdghDnAk7THFIe1Vlg531IxUcus7ZfUJI= -git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231109143925-dd5919348da9 h1:o14uxW6CLyweCdptexXn0ox0zGegdXc8lx8XauJ+b24= -git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231109143925-dd5919348da9/go.mod h1:rQWdsG18NaiFvkJpMguJev913KD/yleHaniRBkUyt0o= +git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 h1:X9yPizADIhD3K/gdKVCthlAnf9aQ3UJJGnZgIwwixRQ= +git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958/go.mod h1:rQWdsG18NaiFvkJpMguJev913KD/yleHaniRBkUyt0o= git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 h1:FxqFDhQYYgpe41qsIHVOcdzSVCB8JNSfPG7Uk4r2oSk= git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0/go.mod h1:RUIKZATQLJ+TaYQa60X2fTDwfuhMfm8Ar60bQ5fr+vU= git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20230531082742-c97d21411eb6 h1:aGQ6QaAnTerQ5Dq5b2/f9DUQtSqPkZZ/bkMx/HKuLCo= diff --git a/internal/frostfs/frostfsid/frostfsid.go b/internal/frostfs/frostfsid/frostfsid.go index 6979b731..d2f3dc13 100644 --- a/internal/frostfs/frostfsid/frostfsid.go +++ b/internal/frostfs/frostfsid/frostfsid.go @@ -8,11 +8,9 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ns" + "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/util" "github.com/nspcc-dev/neo-go/pkg/wallet" ) @@ -39,7 +37,7 @@ var ( // New creates new FrostfsID contract wrapper that implements auth.FrostFSID interface. func New(ctx context.Context, cfg Config) (*FrostFSID, error) { - contractHash, err := fetchContractHash(cfg) + contractHash, err := util.ResolveContractHash(cfg.Contract, cfg.RPCAddress) if err != nil { return nil, fmt.Errorf("resolve frostfs contract hash: %w", err) } @@ -79,25 +77,3 @@ func (f *FrostFSID) RegisterPublicKey(key *keys.PublicKey) error { return nil } - -func fetchContractHash(cfg Config) (util.Uint160, error) { - if hash, err := util.Uint160DecodeStringLE(cfg.Contract); err == nil { - return hash, nil - } - - splitName := strings.Split(cfg.Contract, ".") - if len(splitName) != 2 { - return util.Uint160{}, fmt.Errorf("invalid contract name: '%s'", cfg.Contract) - } - - var domain container.Domain - domain.SetName(splitName[0]) - domain.SetZone(splitName[1]) - - var nns ns.NNS - if err := nns.Dial(cfg.RPCAddress); err != nil { - return util.Uint160{}, fmt.Errorf("dial nns %s: %w", cfg.RPCAddress, err) - } - - return nns.ResolveContractHash(domain) -} diff --git a/internal/frostfs/policy/cached_morph.go b/internal/frostfs/policy/cached_morph.go new file mode 100644 index 00000000..b8f0da43 --- /dev/null +++ b/internal/frostfs/policy/cached_morph.go @@ -0,0 +1,60 @@ +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" + "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) 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) 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 new file mode 100644 index 00000000..56f800fa --- /dev/null +++ b/internal/frostfs/policy/contract/contract.go @@ -0,0 +1,112 @@ +package contract + +import ( + "context" + "fmt" + "math/big" + + 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/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" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +type Client struct { + actor *actor.Actor + policyContract *policyclient.Contract +} + +type Config struct { + // RPCAddress is an endpoint to connect to neo rpc. + RPCAddress string + + // Contract is hash of contract or its name in NNS. + Contract string + + // Key is used to interact with policy contract. + // If this is nil than random key will be generated. + Key *keys.PrivateKey +} + +var _ engine.MorphRuleChainStorage = (*Client)(nil) + +// New creates new Policy contract wrapper. +func New(ctx context.Context, cfg Config) (*Client, error) { + contractHash, err := util.ResolveContractHash(cfg.Contract, cfg.RPCAddress) + if err != nil { + return nil, fmt.Errorf("resolve frostfs contract hash: %w", err) + } + + key := cfg.Key + if key == nil { + if key, err = keys.NewPrivateKey(); err != nil { + return nil, fmt.Errorf("generate anon private key for policy: %w", err) + } + } + + rpcCli, err := rpcclient.New(ctx, cfg.RPCAddress, rpcclient.Options{}) + if err != nil { + return nil, fmt.Errorf("create policy rpc client: %w", err) + } + + acc := wallet.NewAccountFromPrivateKey(key) + act, err := actor.NewSimple(rpcCli, acc) + if err != nil { + return nil, fmt.Errorf("create new actor: %w", err) + } + + return &Client{ + actor: act, + policyContract: policyclient.New(act, contractHash), + }, nil +} + +func (c *Client) AddMorphRuleChain(name chain.Name, target engine.Target, policyChain *chain.Chain) error { + chainName := append([]byte(name), []byte(policyChain.ID)...) + _, err := c.actor.Wait(c.policyContract.AddChain(getKind(target), target.Name, chainName, policyChain.Bytes())) + return err +} + +func (c *Client) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) error { + chainName := append([]byte(name), []byte(chainID)...) + _, err := c.actor.Wait(c.policyContract.RemoveChain(getKind(target), target.Name, chainName)) + return err +} + +func (c *Client) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { + items, err := c.policyContract.ListChainsByPrefix(getKind(target), target.Name, []byte(name)) + if err != nil { + return nil, err + } + + res := make([]*chain.Chain, len(items)) + for i, item := range items { + data, 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) +} diff --git a/internal/frostfs/policy/storage.go b/internal/frostfs/policy/storage.go new file mode 100644 index 00000000..8c9c35be --- /dev/null +++ b/internal/frostfs/policy/storage.go @@ -0,0 +1,42 @@ +package policy + +import ( + "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" +) + +const S3ChainName chain.Name = "s3" + +type Storage struct { + router engine.ChainRouter + + morph engine.MorphRuleChainStorage + + local engine.LocalOverrideStorage +} + +var _ engine.LocalOverrideEngine = (*Storage)(nil) + +func NewStorage(morph engine.MorphRuleChainStorage) *Storage { + local := inmemory.NewInmemoryLocalStorage() + + return &Storage{ + router: engine.NewDefaultChainRouterWithLocalOverrides(morph, local), + morph: morph, + local: local, + } +} + +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 { + return s.local +} diff --git a/internal/frostfs/util/util.go b/internal/frostfs/util/util.go new file mode 100644 index 00000000..3c699559 --- /dev/null +++ b/internal/frostfs/util/util.go @@ -0,0 +1,33 @@ +package util + +import ( + "fmt" + "strings" + + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ns" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// ResolveContractHash determine contract hash by resolving NNS name. +func ResolveContractHash(contractHash, rpcAddress string) (util.Uint160, error) { + if hash, err := util.Uint160DecodeStringLE(contractHash); err == nil { + return hash, nil + } + + splitName := strings.Split(contractHash, ".") + if len(splitName) != 2 { + return util.Uint160{}, fmt.Errorf("invalid contract name: '%s'", contractHash) + } + + var domain container.Domain + domain.SetName(splitName[0]) + domain.SetZone(splitName[1]) + + var nns ns.NNS + if err := nns.Dial(rpcAddress); err != nil { + return util.Uint160{}, fmt.Errorf("dial nns %s: %w", rpcAddress, err) + } + + return nns.ResolveContractHash(domain) +} diff --git a/internal/logs/logs.go b/internal/logs/logs.go index 553211e9..951ba44e 100644 --- a/internal/logs/logs.go +++ b/internal/logs/logs.go @@ -99,6 +99,7 @@ const ( CouldntCacheBucketSettings = "couldn't cache bucket settings" // Warn in ../../api/layer/cache.go CouldntCacheCors = "couldn't cache cors" // Warn in ../../api/layer/cache.go CouldntCacheNotificationConfiguration = "couldn't cache notification configuration" // Warn in ../../api/layer/cache.go + CouldntCacheListPolicyChains = "couldn't cache list policy chains" // Warn in ../../api/layer/cache.go RequestEnd = "request end" // Info in ../../api/middleware/response.go CouldntReceiveAccessBoxForGateKeyRandomKeyWillBeUsed = "couldn't receive access box for gate key, random key will be used" // Debug in ../../api/middleware/auth.go FailedToPassAuthentication = "failed to pass authentication" // Error in ../../api/middleware/auth.go @@ -128,6 +129,7 @@ const ( AnonRequestSkipFrostfsIDValidation = "anon request, skip FrostfsID validation" // Debug in ../../api/middleware/auth.go FrostfsIDValidationFailed = "FrostfsID validation failed" // Error in ../../api/middleware/auth.go InitFrostfsIDContractFailed = "init frostfsid contract failed" // Fatal in ../../cmd/s3-gw/app.go + InitPolicyContractFailed = "init policy contract failed" // Fatal in ../../cmd/s3-gw/app.go ControlAPIHealthcheck = "healthcheck request" ControlAPIPutPolicies = "put policies request" ControlAPIRemovePolicies = "remove policies request" diff --git a/pkg/service/control/server/server.go b/pkg/service/control/server/server.go index 87198730..0fc5a0a4 100644 --- a/pkg/service/control/server/server.go +++ b/pkg/service/control/server/server.go @@ -9,6 +9,7 @@ import ( "fmt" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control" frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto" @@ -43,14 +44,14 @@ type cfg struct { settings Settings - chainStorage engine.LocalOverrideEngine + chainStorage engine.LocalOverrideStorage } func defaultCfg() *cfg { return &cfg{ log: zap.NewNop(), settings: defaultSettings{}, - chainStorage: inmemory.NewInMemoryLocalOverrides(), + chainStorage: inmemory.NewInmemoryLocalStorage(), } } @@ -84,7 +85,7 @@ func WithLogger(log *zap.Logger) Option { } // WithChainStorage returns option to set logger. -func WithChainStorage(chainStorage engine.LocalOverrideEngine) Option { +func WithChainStorage(chainStorage engine.LocalOverrideStorage) Option { return func(c *cfg) { c.chainStorage = chainStorage } @@ -141,7 +142,7 @@ func (s *Server) putPolicy(data *control.PutPoliciesRequest_ChainData) error { } ns := s.settings.ResolveNamespaceAlias(data.GetNamespace()) - if _, err := s.chainStorage.LocalStorage().AddOverride(chain.Ingress, engine.NamespaceTarget(ns), &overrideChain); err != nil { + if _, err := s.chainStorage.AddOverride(policy.S3ChainName, engine.NamespaceTarget(ns), &overrideChain); err != nil { return status.Error(codes.Internal, err.Error()) } @@ -170,7 +171,7 @@ func (s *Server) RemovePolicies(_ context.Context, req *control.RemovePoliciesRe func (s *Server) removePolicy(info *control.RemovePoliciesRequest_ChainInfo) error { ns := s.settings.ResolveNamespaceAlias(info.GetNamespace()) - err := s.chainStorage.LocalStorage().RemoveOverride(chain.Ingress, engine.NamespaceTarget(ns), chain.ID(info.GetChainID())) + err := s.chainStorage.RemoveOverride(policy.S3ChainName, engine.NamespaceTarget(ns), chain.ID(info.GetChainID())) if err != nil { if isNotFoundError(err) { return status.Error(codes.NotFound, err.Error()) @@ -193,7 +194,7 @@ func (s *Server) GetPolicy(_ context.Context, req *control.GetPolicyRequest) (*c } ns := s.settings.ResolveNamespaceAlias(req.GetBody().GetNamespace()) - overrideChain, err := s.chainStorage.LocalStorage().GetOverride(chain.Ingress, engine.NamespaceTarget(ns), chain.ID(req.GetBody().GetChainID())) + overrideChain, err := s.chainStorage.GetOverride(policy.S3ChainName, engine.NamespaceTarget(ns), chain.ID(req.GetBody().GetChainID())) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -214,7 +215,7 @@ func (s *Server) ListPolicies(_ context.Context, req *control.ListPoliciesReques } ns := s.settings.ResolveNamespaceAlias(req.GetBody().GetNamespace()) - chains, err := s.chainStorage.LocalStorage().ListOverrides(chain.Ingress, engine.NamespaceTarget(ns)) + chains, err := s.chainStorage.ListOverrides(policy.S3ChainName, engine.NamespaceTarget(ns)) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) }