diff --git a/api/cache/cache_test.go b/api/cache/cache_test.go
index 825c4e67..095a8fce 100644
--- a/api/cache/cache_test.go
+++ b/api/cache/cache_test.go
@@ -3,10 +3,13 @@ package cache
import (
"testing"
+ "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
+ "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+ "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
@@ -194,6 +197,44 @@ func TestNotificationConfigurationCacheType(t *testing.T) {
assertInvalidCacheEntry(t, cache.GetNotificationConfiguration(key), observedLog)
}
+func TestFrostFSIDSubjectCacheType(t *testing.T) {
+ logger, observedLog := getObservedLogger()
+ cache := NewFrostfsIDCache(DefaultFrostfsIDConfig(logger))
+
+ key, err := util.Uint160DecodeStringLE("4ea976429703418ef00fc4912a409b6a0b973034")
+ require.NoError(t, err)
+ value := &client.SubjectExtended{}
+
+ err = cache.PutSubject(key, value)
+ require.NoError(t, err)
+ val := cache.GetSubject(key)
+ require.Equal(t, value, val)
+ require.Equal(t, 0, observedLog.Len())
+
+ err = cache.cache.Set(key, "tmp")
+ require.NoError(t, err)
+ assertInvalidCacheEntry(t, cache.GetSubject(key), observedLog)
+}
+
+func TestFrostFSIDUserKeyCacheType(t *testing.T) {
+ logger, observedLog := getObservedLogger()
+ cache := NewFrostfsIDCache(DefaultFrostfsIDConfig(logger))
+
+ ns, name := "ns", "name"
+ value, err := keys.NewPrivateKey()
+ require.NoError(t, err)
+
+ err = cache.PutUserKey(ns, name, value.PublicKey())
+ require.NoError(t, err)
+ val := cache.GetUserKey(ns, name)
+ require.Equal(t, value.PublicKey(), val)
+ require.Equal(t, 0, observedLog.Len())
+
+ err = cache.cache.Set(ns+"/"+name, "tmp")
+ require.NoError(t, err)
+ assertInvalidCacheEntry(t, cache.GetUserKey(ns, name), observedLog)
+}
+
func assertInvalidCacheEntry(t *testing.T, val interface{}, observedLog *observer.ObservedLogs) {
require.Nil(t, val)
require.Equal(t, 1, observedLog.Len())
diff --git a/api/cache/frostfsid.go b/api/cache/frostfsid.go
new file mode 100644
index 00000000..da093ecd
--- /dev/null
+++ b/api/cache/frostfsid.go
@@ -0,0 +1,84 @@
+package cache
+
+import (
+ "fmt"
+ "time"
+
+ "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
+ "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"
+ "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+ "github.com/nspcc-dev/neo-go/pkg/util"
+ "go.uber.org/zap"
+)
+
+// FrostfsIDCache provides lru cache for frostfsid contract.
+type FrostfsIDCache struct {
+ cache gcache.Cache
+ logger *zap.Logger
+}
+
+type FrostfsIDCacheKey struct {
+ Target engine.Target
+ Name chain.Name
+}
+
+const (
+ // DefaultFrostfsIDCacheSize is a default maximum number of entries in cache.
+ DefaultFrostfsIDCacheSize = 1e4
+ // DefaultFrostfsIDCacheLifetime is a default lifetime of entries in cache.
+ DefaultFrostfsIDCacheLifetime = time.Minute
+)
+
+// DefaultFrostfsIDConfig returns new default cache expiration values.
+func DefaultFrostfsIDConfig(logger *zap.Logger) *Config {
+ return &Config{
+ Size: DefaultFrostfsIDCacheSize,
+ Lifetime: DefaultFrostfsIDCacheLifetime,
+ Logger: logger,
+ }
+}
+
+// NewFrostfsIDCache creates an object of FrostfsIDCache.
+func NewFrostfsIDCache(config *Config) *FrostfsIDCache {
+ gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build()
+ return &FrostfsIDCache{cache: gc, logger: config.Logger}
+}
+
+// GetSubject returns a cached client.SubjectExtended. Returns nil if value is missing.
+func (c *FrostfsIDCache) GetSubject(key util.Uint160) *client.SubjectExtended {
+ return get[client.SubjectExtended](c, key)
+}
+
+// PutSubject puts a client.SubjectExtended to cache.
+func (c *FrostfsIDCache) PutSubject(key util.Uint160, subject *client.SubjectExtended) error {
+ return c.cache.Set(key, subject)
+}
+
+// GetUserKey returns a cached *keys.PublicKey. Returns nil if value is missing.
+func (c *FrostfsIDCache) GetUserKey(ns, name string) *keys.PublicKey {
+ return get[keys.PublicKey](c, ns+"/"+name)
+}
+
+// PutUserKey puts a client.SubjectExtended to cache.
+func (c *FrostfsIDCache) PutUserKey(ns, name string, userKey *keys.PublicKey) error {
+ return c.cache.Set(ns+"/"+name, userKey)
+}
+
+func get[T any](c *FrostfsIDCache, key any) *T {
+ entry, err := c.cache.Get(key)
+ if err != nil {
+ return nil
+ }
+
+ result, ok := entry.(*T)
+ if !ok {
+ c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
+ zap.String("expected", fmt.Sprintf("%T", result)))
+ return nil
+ }
+
+ return result
+}
diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go
index af7e35a3..182e719a 100644
--- a/cmd/s3-gw/app.go
+++ b/cmd/s3-gw/app.go
@@ -958,6 +958,15 @@ func getMorphPolicyCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config {
return cacheCfg
}
+func getFrostfsIDCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config {
+ cacheCfg := cache.DefaultFrostfsIDConfig(l)
+
+ cacheCfg.Lifetime = fetchCacheLifetime(v, l, cfgFrostfsIDCacheLifetime, cacheCfg.Lifetime)
+ cacheCfg.Size = fetchCacheSize(v, l, cfgFrostfsIDCacheSize, cacheCfg.Size)
+
+ return cacheCfg
+}
+
func (a *App) initHandler() {
var err error
diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go
index 2025765f..79bbb228 100644
--- a/cmd/s3-gw/app_settings.go
+++ b/cmd/s3-gw/app_settings.go
@@ -116,6 +116,8 @@ const ( // Settings.
cfgAccessControlCacheSize = "cache.accesscontrol.size"
cfgMorphPolicyCacheLifetime = "cache.morph_policy.lifetime"
cfgMorphPolicyCacheSize = "cache.morph_policy.size"
+ cfgFrostfsIDCacheLifetime = "cache.frostfsid.lifetime"
+ cfgFrostfsIDCacheSize = "cache.frostfsid.size"
cfgAccessBoxCacheRemovingCheckInterval = "cache.accessbox.removing_check_interval"
diff --git a/config/config.env b/config/config.env
index 7110985a..a35dda6c 100644
--- a/config/config.env
+++ b/config/config.env
@@ -107,6 +107,9 @@ 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
+# Cache which stores frostfsid subject info
+S3_GW_CACHE_FROSTFSID_LIFETIME=1m
+S3_GW_CACHE_FROSTFSID_SIZE=10000
# NATS
S3_GW_NATS_ENABLED=true
diff --git a/config/config.yaml b/config/config.yaml
index 16c8513f..cfc66c6f 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -131,6 +131,10 @@ cache:
morph_policy:
lifetime: 1m
size: 10000
+ # Cache which stores frostfsid subject info
+ frostfsid:
+ lifetime: 1m
+ size: 10000
nats:
enabled: true
diff --git a/docs/configuration.md b/docs/configuration.md
index 280d9dc9..a9decf3e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -421,6 +421,9 @@ cache:
morph_policy:
lifetime: 30s
size: 10000
+ frostfsid:
+ lifetime: 1m
+ size: 10000
```
| Parameter | Type | Default value | Description |
@@ -434,6 +437,7 @@ cache:
| `accessbox` | [Accessbox cache config](#accessbox-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. |
+| `frostfsid` | [Cache config](#cache-subsection) | `lifetime: 1m`
`size: 10000` | Cache which stores FrostfsID subject info. |
#### `cache` subsection