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())
}