[#259] Support contract based policies #276
18 changed files with 428 additions and 43 deletions
|
@ -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)
|
||||
|
|
72
api/cache/policy.go
vendored
Normal file
72
api/cache/policy.go
vendored
Normal file
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`<br>`size: 10000` | Cache for system objects in a bucket: bucket settings, notification configuration etc. |
|
||||
| `accessbox` | [Cache config](#cache-subsection) | `lifetime: 10m`<br>`size: 100` | Cache which stores access box with tokens by its address. |
|
||||
| `accesscontrol` | [Cache config](#cache-subsection) | `lifetime: 1m`<br>`size: 100000` | Cache which stores owner to cache operation mapping. |
|
||||
| `morph_policy` | [Cache config](#cache-subsection) | `lifetime: 1m`<br>`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.
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
60
internal/frostfs/policy/cached_morph.go
Normal file
60
internal/frostfs/policy/cached_morph.go
Normal file
|
@ -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
|
||||
}
|
112
internal/frostfs/policy/contract/contract.go
Normal file
112
internal/frostfs/policy/contract/contract.go
Normal file
|
@ -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)
|
||||
}
|
42
internal/frostfs/policy/storage.go
Normal file
42
internal/frostfs/policy/storage.go
Normal file
|
@ -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
|
||||
}
|
33
internal/frostfs/util/util.go
Normal file
33
internal/frostfs/util/util.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue