From 987b8cd562a194b2d7e3ef812dd5f56426b7df12 Mon Sep 17 00:00:00 2001 From: Andrey Butusov Date: Thu, 12 Feb 2026 23:22:39 +0300 Subject: [PATCH 1/2] go.mod: update SDK Contains obtaining auth key from session token. Also contains fix #3805. Signed-off-by: Andrey Butusov --- CHANGELOG.md | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2174caa93c..c9752bf209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Changelog for NeoFS Node ### Removed ### Updated -- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.17.0.20260211130529-740a11a64a87` (#3785) +- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.17.0.20260219114115-99622c87f029` (#3785, #3817) ### Updating from v0.51.1 diff --git a/go.mod b/go.mod index f420a5492f..4060c81670 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/nspcc-dev/neo-go v0.117.0 github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea github.com/nspcc-dev/neofs-contract v0.26.1 - github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.17.0.20260211130529-740a11a64a87 + github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.17.0.20260219114115-99622c87f029 github.com/nspcc-dev/tzhash v1.8.3 github.com/panjf2000/ants/v2 v2.11.3 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index ea8f3ab954..e70bdde921 100644 --- a/go.sum +++ b/go.sum @@ -199,8 +199,8 @@ github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea/go.mod h1:YzhD4EZmC9Z/PNyd7ysC7WXgIgURc9uCG1UWDeV027Y= github.com/nspcc-dev/neofs-contract v0.26.1 h1:7Ii7Q4L3au408LOsIWKiSgfnT1g8G9jo3W7381d41T8= github.com/nspcc-dev/neofs-contract v0.26.1/go.mod h1:pevVF9OWdEN5bweKxOu6ryZv9muCEtS1ppzYM4RfBIo= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.17.0.20260211130529-740a11a64a87 h1:RlOzZGS7925Dz5F6OckibMSLv/awqhla2D8PeO9nIHI= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.17.0.20260211130529-740a11a64a87/go.mod h1:y2vNz9DVTqBkR7ctYb6taLnabWTtG7xtCHlGofEpKOM= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.17.0.20260219114115-99622c87f029 h1:7EvHXn5LAzbZ25pwz+4qwLxG2f1H8jqjCOe0E1aR9gw= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.17.0.20260219114115-99622c87f029/go.mod h1:y2vNz9DVTqBkR7ctYb6taLnabWTtG7xtCHlGofEpKOM= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= github.com/nspcc-dev/rfc6979 v0.2.4/go.mod h1:86ylDw6Kss+P6v4QAJqo1Sp3mC0/Zr9G97xSjQ9TuFg= github.com/nspcc-dev/tzhash v1.8.3 h1:EWJMOL/ppdqNBvkKjHECljusopcsNu4i4kH8KctTv10= From 18553f8fb65245991952713d95604cd07bb4e0cb Mon Sep 17 00:00:00 2001 From: Andrey Butusov Date: Thu, 12 Feb 2026 23:49:13 +0300 Subject: [PATCH 2/2] session: access session keys by account only There is no need to store session information in two formats in session key storage. An ID is not required, as it can be retrieved using the public key account of the open session. Closes #3793. Signed-off-by: Andrey Butusov --- CHANGELOG.md | 1 + cmd/neofs-node/config.go | 3 + cmd/neofs-node/object.go | 9 +- cmd/neofs-node/session.go | 4 +- pkg/services/object/delete/delete.go | 13 +- pkg/services/object/get/exec.go | 12 +- pkg/services/object/get/service.go | 4 +- pkg/services/object/get/service_test.go | 4 +- pkg/services/object/put/remote.go | 10 +- pkg/services/object/put/service_test.go | 4 +- pkg/services/object/put/streamer.go | 11 +- pkg/services/object/search/util.go | 12 +- pkg/services/object/server.go | 17 +- pkg/services/object/server_test.go | 11 +- pkg/services/object/util/key.go | 39 ++--- pkg/services/object/util/key_test.go | 34 ++-- pkg/services/session/server.go | 14 +- pkg/util/state/executor.go | 13 +- pkg/util/state/executor_test.go | 77 +++----- pkg/util/state/migratation.go | 142 +++++++++++++++ pkg/util/state/migration_test.go | 222 +++++++++++++++++++----- pkg/util/state/token.go | 55 ++---- pkg/util/state/util.go | 18 -- 23 files changed, 453 insertions(+), 276 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9752bf209..13923f5d23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Changelog for NeoFS Node ### Changed - SN returns unsigned responses to requests with API >= `v2.22` (#3785) - Move lens `write-cache` `get`/`list` commands into `fstree` section in CLI (#3838) +- Session key storage access session keys by account only (#3817) ### Removed diff --git a/cmd/neofs-node/config.go b/cmd/neofs-node/config.go index 8fb4c7d7f4..b61aaf9667 100644 --- a/cmd/neofs-node/config.go +++ b/cmd/neofs-node/config.go @@ -407,6 +407,9 @@ func initCfg(appCfg *config.Config) *cfg { fatalOnErr(err) } + err = persistate.MigrateSessionTokensToAccounts() + fatalOnErr(err) + basicSharedConfig := initBasics(c, key, persistate) streamTimeout := appCfg.APIClient.StreamTimeout minConnTimeout := appCfg.APIClient.MinConnectionTime diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index 046a0df6bf..7d1bd3f09a 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -11,7 +11,6 @@ import ( "sync/atomic" "time" - "github.com/google/uuid" lru "github.com/hashicorp/golang-lru/v2" iec "github.com/nspcc-dev/neofs-node/internal/ec" coreclient "github.com/nspcc-dev/neofs-node/pkg/core/client" @@ -627,16 +626,16 @@ func (x storageForObjectService) VerifyAndStoreObjectLocally(obj object.Object) return x.putSvc.ValidateAndStoreObjectLocally(obj) } -func (x storageForObjectService) GetSessionPrivateKey(usr user.ID, uid uuid.UUID) (ecdsa.PrivateKey, error) { - k, err := x.keys.GetKey(&util.SessionInfo{ID: uid, Owner: usr}) +func (x storageForObjectService) GetSessionPrivateKey(account user.ID) (ecdsa.PrivateKey, error) { + k, err := x.keys.GetKey(&account) if err != nil { return ecdsa.PrivateKey{}, err } return *k, nil } -func (x storageForObjectService) GetSessionV2PrivateKey(issuer user.ID, subjects []sessionv2.Target) (ecdsa.PrivateKey, error) { - k, err := x.keys.GetKeyBySubjects(issuer, subjects) +func (x storageForObjectService) GetSessionV2PrivateKey(subjects []sessionv2.Target) (ecdsa.PrivateKey, error) { + k, err := x.keys.GetKeyBySubjects(subjects) if err != nil { return ecdsa.PrivateKey{}, err } diff --git a/cmd/neofs-node/session.go b/cmd/neofs-node/session.go index 68d1243e26..253d117112 100644 --- a/cmd/neofs-node/session.go +++ b/cmd/neofs-node/session.go @@ -12,8 +12,8 @@ import ( type sessionStorage interface { sessionSvc.KeyStorage - GetToken(ownerID user.ID, tokenID []byte) *session.PrivateToken - FindTokenBySubjects(owner user.ID, subjects []sessionv2.Target) *session.PrivateToken + GetToken(account user.ID) *session.PrivateToken + FindTokenBySubjects(subjects []sessionv2.Target) *session.PrivateToken RemoveOldTokens(epoch uint64) Close() error diff --git a/pkg/services/object/delete/delete.go b/pkg/services/object/delete/delete.go index 0f4dc17e42..975d8f2c7a 100644 --- a/pkg/services/object/delete/delete.go +++ b/pkg/services/object/delete/delete.go @@ -2,8 +2,8 @@ package deletesvc import ( "context" + "fmt" - "github.com/nspcc-dev/neofs-node/pkg/services/object/util" "github.com/nspcc-dev/neofs-sdk-go/user" "go.uber.org/zap" ) @@ -13,7 +13,7 @@ func (s *Service) Delete(ctx context.Context, prm Prm) error { // If session token is not found we will fail during tombstone PUT. // Here we fail immediately to ensure no unnecessary network communication is done. if tokV2 := prm.common.SessionTokenV2(); tokV2 != nil { - if _, err := s.keyStorage.GetKeyBySubjects(tokV2.Issuer(), tokV2.Subjects()); err != nil { + if _, err := s.keyStorage.GetKeyBySubjects(tokV2.Subjects()); err != nil { if s.nnsResolver == nil { return err } @@ -31,10 +31,11 @@ func (s *Service) Delete(ctx context.Context, prm Prm) error { } } } else if tok := prm.common.SessionToken(); tok != nil { - _, err := s.keyStorage.GetKey(&util.SessionInfo{ - ID: tok.ID(), - Owner: tok.Issuer(), - }) + authUser, err := tok.AuthUser() + if err != nil { + return fmt.Errorf("can't get auth user from token: %w", err) + } + _, err = s.keyStorage.GetKey(&authUser) if err != nil { return err } diff --git a/pkg/services/object/get/exec.go b/pkg/services/object/get/exec.go index dfc65d2d25..8409ff47cb 100644 --- a/pkg/services/object/get/exec.go +++ b/pkg/services/object/get/exec.go @@ -8,7 +8,6 @@ import ( "io" clientcore "github.com/nspcc-dev/neofs-node/pkg/core/client" - "github.com/nspcc-dev/neofs-node/pkg/services/object/util" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/object" @@ -172,7 +171,7 @@ func (exec execCtx) key() (*ecdsa.PrivateKey, error) { } if tokV2 := exec.prm.common.SessionTokenV2(); tokV2 != nil { // For V2 tokens, the key is stored as the subjects - if keyForSession, err := exec.svc.keyStore.GetKeyBySubjects(tokV2.Issuer(), tokV2.Subjects()); err == nil { + if keyForSession, err := exec.svc.keyStore.GetKeyBySubjects(tokV2.Subjects()); err == nil { key = keyForSession } else if exec.svc.nnsResolver != nil { nodeUser := user.NewFromECDSAPublicKey(key.PublicKey) @@ -188,10 +187,11 @@ func (exec execCtx) key() (*ecdsa.PrivateKey, error) { return nil, fmt.Errorf("get key for session v2 token: %w", err) } } else if tok := exec.prm.common.SessionToken(); tok != nil { - key, err = exec.svc.keyStore.GetKey(&util.SessionInfo{ - ID: tok.ID(), - Owner: tok.Issuer(), - }) + authUser, err := tok.AuthUser() + if err != nil { + return nil, fmt.Errorf("could not get session auth user: %w", err) + } + key, err = exec.svc.keyStore.GetKey(&authUser) if err != nil { return nil, err } diff --git a/pkg/services/object/get/service.go b/pkg/services/object/get/service.go index eb530819ff..965caea32a 100644 --- a/pkg/services/object/get/service.go +++ b/pkg/services/object/get/service.go @@ -105,8 +105,8 @@ type cfg struct { } keyStore interface { - GetKey(*util.SessionInfo) (*ecdsa.PrivateKey, error) - GetKeyBySubjects(user.ID, []sessionv2.Target) (*ecdsa.PrivateKey, error) + GetKey(*user.ID) (*ecdsa.PrivateKey, error) + GetKeyBySubjects([]sessionv2.Target) (*ecdsa.PrivateKey, error) } nnsResolver sessionv2.NNSResolver diff --git a/pkg/services/object/get/service_test.go b/pkg/services/object/get/service_test.go index 6458a0d2d1..07a67aebf0 100644 --- a/pkg/services/object/get/service_test.go +++ b/pkg/services/object/get/service_test.go @@ -144,11 +144,11 @@ type mockKeyStorage struct { privKey ecdsa.PrivateKey } -func (x *mockKeyStorage) GetKey(*util.SessionInfo) (*ecdsa.PrivateKey, error) { +func (x *mockKeyStorage) GetKey(*user.ID) (*ecdsa.PrivateKey, error) { return &x.privKey, nil } -func (x *mockKeyStorage) GetKeyBySubjects(user.ID, []sessionv2.Target) (*ecdsa.PrivateKey, error) { +func (x *mockKeyStorage) GetKeyBySubjects([]sessionv2.Target) (*ecdsa.PrivateKey, error) { return &x.privKey, nil } diff --git a/pkg/services/object/put/remote.go b/pkg/services/object/put/remote.go index 4c1cbb886b..bda8fc047a 100644 --- a/pkg/services/object/put/remote.go +++ b/pkg/services/object/put/remote.go @@ -43,16 +43,16 @@ func putObjectToNode(ctx context.Context, nodeInfo clientcore.NodeInfo, obj *obj if tokV2 := commonPrm.SessionTokenV2(); tokV2 != nil { // For V2 tokens, the key is stored as the subjects - if keyForSession, err := keyStorage.GetKeyBySubjects(tokV2.Issuer(), tokV2.Subjects()); err == nil { + if keyForSession, err := keyStorage.GetKeyBySubjects(tokV2.Subjects()); err == nil { key = keyForSession } opts.WithinSessionV2(*tokV2) } else if tok := commonPrm.SessionToken(); tok != nil { - sessionInfo := &util.SessionInfo{ - ID: tok.ID(), - Owner: tok.Issuer(), + authUser, err := tok.AuthUser() + if err != nil { + return fmt.Errorf("could not get session auth user: %w", err) } - key, err = keyStorage.GetKey(sessionInfo) + key, err = keyStorage.GetKey(&authUser) if err != nil { return fmt.Errorf("could not receive private key: %w", err) } diff --git a/pkg/services/object/put/service_test.go b/pkg/services/object/put/service_test.go index 67a94d8865..9e6416272c 100644 --- a/pkg/services/object/put/service_test.go +++ b/pkg/services/object/put/service_test.go @@ -893,11 +893,11 @@ type mockNodeSession struct { expiresAt uint64 } -func (x mockNodeSession) FindTokenBySubjects(user.ID, []sessionv2.Target) *storage.PrivateToken { +func (x mockNodeSession) FindTokenBySubjects([]sessionv2.Target) *storage.PrivateToken { return storage.NewPrivateToken(&x.signer.ECDSAPrivateKey, x.expiresAt) } -func (x mockNodeSession) GetToken(user.ID, []byte) *storage.PrivateToken { +func (x mockNodeSession) GetToken(user.ID) *storage.PrivateToken { return storage.NewPrivateToken(&x.signer.ECDSAPrivateKey, x.expiresAt) } diff --git a/pkg/services/object/put/streamer.go b/pkg/services/object/put/streamer.go index 6c4fc0a1f1..96fbfbda76 100644 --- a/pkg/services/object/put/streamer.go +++ b/pkg/services/object/put/streamer.go @@ -8,7 +8,6 @@ import ( iec "github.com/nspcc-dev/neofs-node/internal/ec" "github.com/nspcc-dev/neofs-node/pkg/core/client" "github.com/nspcc-dev/neofs-node/pkg/services/object/internal" - "github.com/nspcc-dev/neofs-node/pkg/services/object/util" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/container" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" @@ -104,7 +103,7 @@ func (p *Streamer) initTarget(prm *PutInitPrm) error { } if sTokenV2 != nil { // For V2 tokens, the key is stored as the subjects - if keyForSession, err := p.keyStorage.GetKeyBySubjects(sTokenV2.Issuer(), sTokenV2.Subjects()); err == nil { + if keyForSession, err := p.keyStorage.GetKeyBySubjects(sTokenV2.Subjects()); err == nil { sessionKey = keyForSession } else if p.nnsResolver != nil { nodeUser := user.NewFromECDSAPublicKey(sessionKey.PublicKey) @@ -120,11 +119,11 @@ func (p *Streamer) initTarget(prm *PutInitPrm) error { return fmt.Errorf("get key for session v2 token: %w", err) } } else if sToken != nil { - sessionInfo := &util.SessionInfo{ - ID: sToken.ID(), - Owner: sToken.Issuer(), + authUser, err := sToken.AuthUser() + if err != nil { + return fmt.Errorf("could not get session auth user: %w", err) } - sessionKey, err = p.keyStorage.GetKey(sessionInfo) + sessionKey, err = p.keyStorage.GetKey(&authUser) if err != nil { return fmt.Errorf("(%T) could not receive session key: %w", p, err) } diff --git a/pkg/services/object/search/util.go b/pkg/services/object/search/util.go index 2e25239b55..4517f5fbbf 100644 --- a/pkg/services/object/search/util.go +++ b/pkg/services/object/search/util.go @@ -7,7 +7,6 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/core/client" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" - "github.com/nspcc-dev/neofs-node/pkg/services/object/util" sdkclient "github.com/nspcc-dev/neofs-sdk-go/client" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" sessionv2 "github.com/nspcc-dev/neofs-sdk-go/session/v2" @@ -83,7 +82,7 @@ func (c *clientWrapper) searchObjects(ctx context.Context, exec *execCtx) ([]oid } if tokV2 := exec.prm.common.SessionTokenV2(); tokV2 != nil { // For V2 tokens, the key is stored as the subjects - if keyForSession, err := exec.svc.keyStore.GetKeyBySubjects(tokV2.Issuer(), tokV2.Subjects()); err == nil { + if keyForSession, err := exec.svc.keyStore.GetKeyBySubjects(tokV2.Subjects()); err == nil { key = keyForSession } else if exec.svc.nnsResolver != nil { nodeUser := user.NewFromECDSAPublicKey(key.PublicKey) @@ -99,10 +98,11 @@ func (c *clientWrapper) searchObjects(ctx context.Context, exec *execCtx) ([]oid return nil, fmt.Errorf("get key for session v2 token: %w", err) } } else if tok := exec.prm.common.SessionToken(); tok != nil { - key, err = exec.svc.keyStore.GetKey(&util.SessionInfo{ - ID: tok.ID(), - Owner: tok.Issuer(), - }) + authUser, err := tok.AuthUser() + if err != nil { + return nil, fmt.Errorf("could not get session auth user: %w", err) + } + key, err = exec.svc.keyStore.GetKey(&authUser) if err != nil { return nil, err } diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 5e6da9b715..f1f48a98ce 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -15,7 +15,6 @@ import ( "sync/atomic" "time" - "github.com/google/uuid" icrypto "github.com/nspcc-dev/neofs-node/internal/crypto" "github.com/nspcc-dev/neofs-node/pkg/core/client" "github.com/nspcc-dev/neofs-node/pkg/core/container" @@ -122,15 +121,15 @@ type FSChain interface { } type sessions interface { - // GetSessionPrivateKey reads private session key by user and session IDs. + // GetSessionPrivateKey reads private session key by his account. // Returns [apistatus.ErrSessionTokenNotFound] if there is no data for the // referenced session. - GetSessionPrivateKey(usr user.ID, uid uuid.UUID) (ecdsa.PrivateKey, error) + GetSessionPrivateKey(account user.ID) (ecdsa.PrivateKey, error) - // GetSessionV2PrivateKey reads private session key by user ID and session + // GetSessionV2PrivateKey reads private session key by session // subject. Returns [apistatus.ErrSessionTokenNotFound] if there is no data // for the referenced session. - GetSessionV2PrivateKey(issuer user.ID, subject []sessionv2.Target) (ecdsa.PrivateKey, error) + GetSessionV2PrivateKey(subject []sessionv2.Target) (ecdsa.PrivateKey, error) } // Storage groups ops of the node's storage required to serve NeoFS API Object @@ -895,7 +894,7 @@ func convertHashPrm(signer ecdsa.PrivateKey, ss sessions, req *protoobject.GetRa } if tokV2 := cp.SessionTokenV2(); tokV2 != nil { - signerKey, err := ss.GetSessionV2PrivateKey(tokV2.Issuer(), tokV2.Subjects()) + signerKey, err := ss.GetSessionV2PrivateKey(tokV2.Subjects()) if err != nil { if !errors.Is(err, apistatus.ErrSessionTokenNotFound) { return getsvc.RangeHashPrm{}, fmt.Errorf("fetching session v2 key: %w", err) @@ -905,7 +904,11 @@ func convertHashPrm(signer ecdsa.PrivateKey, ss sessions, req *protoobject.GetRa } p.WithCachedSignerKey(&signerKey) } else if tok := cp.SessionToken(); tok != nil { - signerKey, err := ss.GetSessionPrivateKey(tok.Issuer(), tok.ID()) + authUser, err := tok.AuthUser() + if err != nil { + return getsvc.RangeHashPrm{}, fmt.Errorf("getting auth user from token: %w", err) + } + signerKey, err := ss.GetSessionPrivateKey(authUser) if err != nil { if !errors.Is(err, apistatus.ErrSessionTokenNotFound) { return getsvc.RangeHashPrm{}, fmt.Errorf("fetching session key: %w", err) diff --git a/pkg/services/object/server_test.go b/pkg/services/object/server_test.go index c5ca504f2d..1d90f95c89 100644 --- a/pkg/services/object/server_test.go +++ b/pkg/services/object/server_test.go @@ -12,7 +12,6 @@ import ( "testing" "time" - "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" @@ -112,11 +111,11 @@ func (noCallTestStorage) SearchObjects(cid.ID, []objectcore.SearchFilter, []stri func (noCallTestStorage) VerifyAndStoreObjectLocally(object.Object) error { panic("must not be called") } -func (noCallTestStorage) GetSessionPrivateKey(user.ID, uuid.UUID) (ecdsa.PrivateKey, error) { +func (noCallTestStorage) GetSessionPrivateKey(user.ID) (ecdsa.PrivateKey, error) { panic("implement me") } -func (s noCallTestStorage) GetSessionV2PrivateKey(user.ID, []sessionv2.Target) (ecdsa.PrivateKey, error) { +func (s noCallTestStorage) GetSessionV2PrivateKey([]sessionv2.Target) (ecdsa.PrivateKey, error) { panic("implement me") } @@ -284,7 +283,7 @@ func (x *testStorage) VerifyAndStoreObjectLocally(obj object.Object) error { return x.storeErr } -func (x *testStorage) GetSessionPrivateKey(user.ID, uuid.UUID) (ecdsa.PrivateKey, error) { +func (x *testStorage) GetSessionPrivateKey(user.ID) (ecdsa.PrivateKey, error) { return ecdsa.PrivateKey{}, apistatus.ErrSessionTokenNotFound } @@ -634,10 +633,10 @@ func (nopFSChain) LocalNodeUnderMaintenance() bool { return false } type nopStorage struct{} func (nopStorage) VerifyAndStoreObjectLocally(object.Object) error { return nil } -func (nopStorage) GetSessionPrivateKey(user.ID, uuid.UUID) (ecdsa.PrivateKey, error) { +func (nopStorage) GetSessionPrivateKey(user.ID) (ecdsa.PrivateKey, error) { return ecdsa.PrivateKey{}, apistatus.ErrSessionTokenNotFound } -func (s nopStorage) GetSessionV2PrivateKey(user.ID, []sessionv2.Target) (ecdsa.PrivateKey, error) { +func (s nopStorage) GetSessionV2PrivateKey([]sessionv2.Target) (ecdsa.PrivateKey, error) { return ecdsa.PrivateKey{}, apistatus.ErrSessionTokenNotFound } func (nopStorage) SearchObjects(cid.ID, []objectcore.SearchFilter, []string, *objectcore.SearchCursor, uint16) ([]client.SearchResultItem, []byte, error) { diff --git a/pkg/services/object/util/key.go b/pkg/services/object/util/key.go index dbf0b4d0ca..8ced30ca51 100644 --- a/pkg/services/object/util/key.go +++ b/pkg/services/object/util/key.go @@ -2,9 +2,7 @@ package util import ( "crypto/ecdsa" - "fmt" - "github.com/google/uuid" "github.com/nspcc-dev/neofs-node/pkg/core/netmap" "github.com/nspcc-dev/neofs-node/pkg/util/state/session" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" @@ -17,16 +15,16 @@ import ( // tokens. type SessionSource interface { // GetToken must return non-expired private token that - // corresponds with passed owner and tokenID. If + // corresponds to his account. If // token has not been created, has been expired // of it is impossible to get information about the // token Get must return nil. - GetToken(owner user.ID, tokenID []byte) *session.PrivateToken + GetToken(account user.ID) *session.PrivateToken // FindTokenBySubjects searches for a non-expired private token whose public key // matches any of the given Target. Used for V2 session tokens where keys // are identified by their Target. Returns nil if no matching token is found. - FindTokenBySubjects(owner user.ID, subjects []session2.Target) *session.PrivateToken + FindTokenBySubjects(subjects []session2.Target) *session.PrivateToken } // KeyStorage represents private key storage of the local node. @@ -47,31 +45,16 @@ func NewKeyStorage(localKey *ecdsa.PrivateKey, tokenStore SessionSource, net net } } -// SessionInfo groups information about NeoFS Object session -// which is reflected in KeyStorage. -type SessionInfo struct { - // Session unique identifier. - ID uuid.UUID - - // Session issuer. - Owner user.ID -} - -// GetKey fetches private key depending on the SessionInfo. +// GetKey fetches private key depending on his account. // -// If info is not `nil`, searches for dynamic session token through the +// If account is not `nil`, searches for dynamic session token through the // underlying token storage. Returns apistatus.SessionTokenNotFound if // token storage does not contain information about provided dynamic session. // -// If info is `nil`, returns node's private key. -func (s *KeyStorage) GetKey(info *SessionInfo) (*ecdsa.PrivateKey, error) { - if info != nil { - binID, err := info.ID.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("marshal ID: %w", err) - } - - pToken := s.tokenStore.GetToken(info.Owner, binID) +// If account is `nil`, returns node's private key. +func (s *KeyStorage) GetKey(account *user.ID) (*ecdsa.PrivateKey, error) { + if account != nil { + pToken := s.tokenStore.GetToken(*account) if pToken != nil { if pToken.ExpiredAt() <= s.networkState.CurrentEpoch() { var errExpired apistatus.SessionTokenExpired @@ -93,11 +76,11 @@ func (s *KeyStorage) GetKey(info *SessionInfo) (*ecdsa.PrivateKey, error) { // // Returns apistatus.SessionTokenNotFound if no matching key is found // or apistatus.SessionTokenExpired if the found token is expired. -func (s *KeyStorage) GetKeyBySubjects(issuer user.ID, subjects []session2.Target) (*ecdsa.PrivateKey, error) { +func (s *KeyStorage) GetKeyBySubjects(subjects []session2.Target) (*ecdsa.PrivateKey, error) { if len(subjects) == 0 { return nil, apistatus.ErrSessionTokenNotFound } - pToken := s.tokenStore.FindTokenBySubjects(issuer, subjects) + pToken := s.tokenStore.FindTokenBySubjects(subjects) if pToken != nil { if pToken.ExpiredAt() <= s.networkState.CurrentEpoch() { return nil, apistatus.ErrSessionTokenExpired diff --git a/pkg/services/object/util/key_test.go b/pkg/services/object/util/key_test.go index d0d0a18607..7e95aafc59 100644 --- a/pkg/services/object/util/key_test.go +++ b/pkg/services/object/util/key_test.go @@ -4,14 +4,12 @@ import ( "path" "testing" - "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-node/pkg/services/object/util" "github.com/nspcc-dev/neofs-node/pkg/util/state" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/nspcc-dev/neofs-sdk-go/session" - "github.com/nspcc-dev/neofs-sdk-go/user" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) @@ -24,8 +22,6 @@ func TestNewKeyStorage(t *testing.T) { require.NoError(t, err) stor := util.NewKeyStorage(&nodeKey.PrivateKey, tokenStor, mockedNetworkState{42}) - owner := usertest.ID() - t.Run("node key", func(t *testing.T) { key, err := stor.GetKey(nil) require.NoError(t, err) @@ -33,43 +29,37 @@ func TestNewKeyStorage(t *testing.T) { }) t.Run("unknown token", func(t *testing.T) { - _, err = stor.GetKey(&util.SessionInfo{ - ID: uuid.New(), - Owner: usertest.ID(), - }) + unknownUser := usertest.ID() + _, err = stor.GetKey(&unknownUser) require.Error(t, err) }) t.Run("known token", func(t *testing.T) { - tok := createToken(t, tokenStor, owner, 100) + tok := createToken(t, tokenStor, 100) - key, err := stor.GetKey(&util.SessionInfo{ - ID: tok.ID(), - Owner: owner, - }) + authUser, err := tok.AuthUser() + require.NoError(t, err) + key, err := stor.GetKey(&authUser) require.NoError(t, err) require.True(t, tok.AssertAuthKey((*neofsecdsa.PublicKey)(&key.PublicKey))) }) t.Run("expired token", func(t *testing.T) { - tok := createToken(t, tokenStor, owner, 30) - _, err := stor.GetKey(&util.SessionInfo{ - ID: tok.ID(), - Owner: owner, - }) + tok := createToken(t, tokenStor, 30) + authUser, err := tok.AuthUser() + require.NoError(t, err) + _, err = stor.GetKey(&authUser) require.Error(t, err) }) } -func createToken(t *testing.T, store *state.PersistentStorage, owner user.ID, exp uint64) session.Object { +func createToken(t *testing.T, store *state.PersistentStorage, exp uint64) session.Object { key := neofscryptotest.ECDSAPrivateKey() - id := uuid.New() - err := store.Store(key, owner, id[:], exp) + err := store.Store(key, exp) require.NoError(t, err) var tok session.Object tok.SetAuthKey((*neofsecdsa.PublicKey)(&key.PublicKey)) - tok.SetID(id) return tok } diff --git a/pkg/services/session/server.go b/pkg/services/session/server.go index 14c0cc6645..98e6e91c3d 100644 --- a/pkg/services/session/server.go +++ b/pkg/services/session/server.go @@ -22,9 +22,9 @@ import ( // KeyStorage represents private keys stored on the local node side. type KeyStorage interface { - // Store saves given private key by specified user and locally unique IDs along + // Store saves given private key by its account along // with its expiration time. - Store(key ecdsa.PrivateKey, _ user.ID, id []byte, exp uint64) error + Store(key ecdsa.PrivateKey, exp uint64) error } type server struct { @@ -85,17 +85,11 @@ func (s *server) Create(_ context.Context, req *protosession.CreateRequest) (*pr return s.makeFailedCreateResponse(fmt.Errorf("generate private key: %w", err), req) } - uid := uuid.New() - if err := s.keys.Store(*key, usr, uid[:], reqBody.Expiration); err != nil { + if err := s.keys.Store(*key, reqBody.Expiration); err != nil { return s.makeFailedCreateResponse(fmt.Errorf("store private key locally: %w", err), req) } - // also store the key using account as key ID - keyUser := user.NewFromECDSAPublicKey(key.PublicKey) - if err := s.keys.Store(*key, usr, keyUser[:], reqBody.Expiration); err != nil { - return s.makeFailedCreateResponse(fmt.Errorf("store private key with public key locally: %w", err), req) - } - + uid := uuid.New() body := &protosession.CreateResponse_Body{ Id: uid[:], SessionKey: neofscrypto.PublicKeyBytes((*neofsecdsa.PublicKey)(&key.PublicKey)), diff --git a/pkg/util/state/executor.go b/pkg/util/state/executor.go index 45ad74fee0..66a05793fb 100644 --- a/pkg/util/state/executor.go +++ b/pkg/util/state/executor.go @@ -10,7 +10,7 @@ import ( // Store saves parameterized private key in the underlying Bolt database. // Private keys are encrypted if token store has been configured to. -func (p PersistentStorage) Store(sk ecdsa.PrivateKey, usr user.ID, id []byte, exp uint64) error { +func (p PersistentStorage) Store(sk ecdsa.PrivateKey, exp uint64) error { value, err := p.packToken(exp, &sk) if err != nil { return err @@ -19,15 +19,10 @@ func (p PersistentStorage) Store(sk ecdsa.PrivateKey, usr user.ID, id []byte, ex err = p.db.Update(func(tx *bbolt.Tx) error { rootBucket := tx.Bucket(sessionsBucket) - ownerBucket, err := rootBucket.CreateBucketIfNotExists(usr[:]) + usr := user.NewFromECDSAPublicKey(sk.PublicKey) + err = rootBucket.Put(usr[:], value) if err != nil { - return fmt.Errorf( - "could not get/create %s owner bucket: %w", usr, err) - } - - err = ownerBucket.Put(id, value) - if err != nil { - return fmt.Errorf("could not put session token for %s oid: %w", id, err) + return fmt.Errorf("could not put session token for %s: %w", usr, err) } return nil diff --git a/pkg/util/state/executor_test.go b/pkg/util/state/executor_test.go index b3caadfbbc..b2e489698b 100644 --- a/pkg/util/state/executor_test.go +++ b/pkg/util/state/executor_test.go @@ -3,7 +3,6 @@ package state import ( "bytes" "crypto/ecdsa" - "crypto/rand" "path/filepath" "testing" @@ -20,34 +19,28 @@ func TestTokenStore(t *testing.T) { const tokenNumber = 5 type tok struct { - owner user.ID - id []byte - key ecdsa.PrivateKey + user usertest.UserSigner } tokens := make([]tok, 0, tokenNumber) for i := range tokenNumber { usr := usertest.User() - sessionID := make([]byte, 32) // any len - _, _ = rand.Read(sessionID) - err := ts.Store(usr.ECDSAPrivateKey, usr.ID, sessionID, uint64(i)) + err := ts.Store(usr.ECDSAPrivateKey, uint64(i)) require.NoError(t, err) tokens = append(tokens, tok{ - owner: usr.ID, - id: sessionID, - key: usr.ECDSAPrivateKey, + user: usr, }) } for i, token := range tokens { - savedToken := ts.GetToken(token.owner, token.id) + savedToken := ts.GetToken(token.user.UserID()) require.Equal(t, uint64(i), savedToken.ExpiredAt()) require.NotNil(t, savedToken.SessionKey()) - require.Equal(t, token.key, *savedToken.SessionKey()) + require.Equal(t, token.user.ECDSAPrivateKey, *savedToken.SessionKey()) } } @@ -55,11 +48,10 @@ func TestTokenStore_Persistent(t *testing.T) { path := filepath.Join(t.TempDir(), ".storage") ts := newStorageWithSession(t, path) - sessionID := make([]byte, 64) // any len - owner := usertest.User() + account := usertest.User() const exp = 12345 - err := ts.Store(owner.ECDSAPrivateKey, owner.ID, sessionID, exp) + err := ts.Store(account.ECDSAPrivateKey, exp) require.NoError(t, err) // close db (stop the node) @@ -68,19 +60,18 @@ func TestTokenStore_Persistent(t *testing.T) { // open persistent storage again ts = newStorageWithSession(t, path) - savedToken := ts.GetToken(owner.ID, sessionID) + savedToken := ts.GetToken(account.ID) require.EqualValues(t, exp, savedToken.ExpiredAt()) require.NotNil(t, savedToken.SessionKey()) - require.Equal(t, owner.ECDSAPrivateKey, *savedToken.SessionKey()) + require.Equal(t, account.ECDSAPrivateKey, *savedToken.SessionKey()) } func TestTokenStore_RemoveOld(t *testing.T) { tests := []*struct { - epoch uint64 - owner user.ID - id []byte - key ecdsa.PrivateKey + epoch uint64 + account user.ID + key ecdsa.PrivateKey }{ { epoch: 1, @@ -105,15 +96,13 @@ func TestTokenStore_RemoveOld(t *testing.T) { ts := newStorageWithSession(t, filepath.Join(t.TempDir(), ".storage")) for _, test := range tests { - test.id = make([]byte, 32) // any len - _, _ = rand.Read(test.id) - owner := usertest.User() + acc := usertest.User() - err := ts.Store(owner.ECDSAPrivateKey, owner.ID, test.id, test.epoch) + err := ts.Store(acc.ECDSAPrivateKey, test.epoch) require.NoError(t, err) - test.owner = owner.ID - test.key = owner.ECDSAPrivateKey + test.account = acc.ID + test.key = acc.ECDSAPrivateKey } const currEpoch = 3 @@ -121,7 +110,7 @@ func TestTokenStore_RemoveOld(t *testing.T) { ts.RemoveOldTokens(currEpoch) for _, test := range tests { - token := ts.GetToken(test.owner, test.id) + token := ts.GetToken(test.account) if test.epoch <= currEpoch { require.Nil(t, token) @@ -200,25 +189,17 @@ func TestTokenStore_FindTokenBySubjects(t *testing.T) { const tokenNumber = 3 tokens := make([]struct { - owner user.ID - key ecdsa.PrivateKey + account user.ID + key ecdsa.PrivateKey }, tokenNumber) - multiUsr := usertest.ID() for i := range tokenNumber { - var usr user.ID - if i == 0 { - usr = usertest.ID() - } else { - usr = multiUsr - } - subject := usertest.User() - err := ts.Store(subject.ECDSAPrivateKey, usr, subject.ID[:], uint64(100+i)) + err := ts.Store(subject.ECDSAPrivateKey, uint64(100+i)) require.NoError(t, err) - tokens[i].owner = usr + tokens[i].account = subject.ID tokens[i].key = subject.ECDSAPrivateKey } @@ -229,27 +210,21 @@ func TestTokenStore_FindTokenBySubjects(t *testing.T) { } for i, tok := range tokens { - foundToken := ts.FindTokenBySubjects(tok.owner, []sessionv2.Target{subjects[i]}) + foundToken := ts.FindTokenBySubjects([]sessionv2.Target{subjects[i]}) require.NotNil(t, foundToken) require.EqualValues(t, 100+i, foundToken.ExpiredAt()) require.Equal(t, tok.key, *foundToken.SessionKey()) } - foundToken := ts.FindTokenBySubjects(tokens[0].owner, []sessionv2.Target{subjects[2], subjects[1]}) - require.Nil(t, foundToken) - foundToken = ts.FindTokenBySubjects(tokens[0].owner, []sessionv2.Target{subjects[2], subjects[0]}) - require.NotNil(t, foundToken) - require.EqualValues(t, 100, foundToken.ExpiredAt()) - // first matching subject in db - foundToken = ts.FindTokenBySubjects(tokens[1].owner, subjects) + foundToken := ts.FindTokenBySubjects(subjects) require.NotNil(t, foundToken) - require.EqualValues(t, 101, foundToken.ExpiredAt()) + require.EqualValues(t, 100, foundToken.ExpiredAt()) nonExistentSubject := sessionv2.NewTargetUser(usertest.ID()) - foundToken = ts.FindTokenBySubjects(tokens[0].owner, []sessionv2.Target{nonExistentSubject}) + foundToken = ts.FindTokenBySubjects([]sessionv2.Target{nonExistentSubject}) require.Nil(t, foundToken) - foundToken = ts.FindTokenBySubjects(tokens[0].owner, []sessionv2.Target{}) + foundToken = ts.FindTokenBySubjects([]sessionv2.Target{}) require.Nil(t, foundToken) } diff --git a/pkg/util/state/migratation.go b/pkg/util/state/migratation.go index 6e1078b279..85266266ce 100644 --- a/pkg/util/state/migratation.go +++ b/pkg/util/state/migratation.go @@ -1,11 +1,16 @@ package state import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/hex" "errors" "fmt" "os" "github.com/nspcc-dev/bbolt" + "github.com/nspcc-dev/neofs-sdk-go/user" + "go.uber.org/zap" ) func (p PersistentStorage) MigrateOldTokenStorage(oldPath string) error { @@ -69,3 +74,140 @@ func (p PersistentStorage) MigrateOldTokenStorage(oldPath string) error { }) }) } + +// MigrateSessionTokensToAccounts removes id-keyed tokens with owners and keeps only account based tokens. +func (p PersistentStorage) MigrateSessionTokensToAccounts() error { + var isMigrated bool + err := p.db.View(func(tx *bbolt.Tx) error { + rootBucket := tx.Bucket(sessionsBucket) + if rootBucket == nil { + return nil + } + + if _, v := rootBucket.Cursor().First(); v != nil { + // if there is value for the first key, + // it means that there are no nested buckets + // and migration is not needed + isMigrated = true + } + return nil + }) + if err != nil { + return fmt.Errorf("could not check if migration is needed: %w", err) + } + + if isMigrated { + p.l.Debug("session token storage migration is not needed, already migrated") + return nil + } + + var migratedCount, deletedCount int + err = p.db.Update(func(tx *bbolt.Tx) error { + rootBucket := tx.Bucket(sessionsBucket) + if rootBucket == nil { + return nil + } + + owners := make(map[user.ID]int) + c := rootBucket.Cursor() + for ownerKeyBytes, v := c.First(); ownerKeyBytes != nil; ownerKeyBytes, v = c.Next() { + if v != nil { + continue + } + + ownerBucket := rootBucket.Bucket(ownerKeyBytes) + if ownerBucket == nil { + continue + } + + var ownerID user.ID + if len(ownerKeyBytes) != len(ownerID) { + continue + } + copy(ownerID[:], ownerKeyBytes) + + tokenCount := 0 + tokenCursor := ownerBucket.Cursor() + for tokenID, tokenData := tokenCursor.First(); tokenID != nil; tokenID, tokenData = tokenCursor.Next() { + if tokenData == nil { + continue + } + privKey, err := p.extractKeyFromPackedToken(tokenData) + if err != nil { + p.l.Warn("could not extract key from token during migration", + zap.Stringer("ownerID", ownerID), + zap.String("tokenID", hex.EncodeToString(tokenID)), + zap.Error(err), + ) + continue + } + + pubKeyID := user.NewFromECDSAPublicKey(privKey.PublicKey) + + existingToken := rootBucket.Get(pubKeyID[:]) + if existingToken == nil { + err = rootBucket.Put(pubKeyID[:], tokenData) + if err != nil { + p.l.Warn("could not store migrated token", + zap.Stringer("ownerID", ownerID), + zap.String("oldTokenID", hex.EncodeToString(tokenID)), + zap.Stringer("account key", pubKeyID), + zap.Error(err), + ) + continue + } + migratedCount++ + } + tokenCount++ + } + owners[ownerID] = tokenCount + } + + for ownerID, tokenCount := range owners { + err := rootBucket.DeleteBucket(ownerID[:]) + if err != nil { + p.l.Warn("could not delete old token bucket during migration", + zap.Stringer("ownerID", ownerID), + zap.Int("tokenCount", tokenCount), + zap.Error(err), + ) + } else { + deletedCount += tokenCount + } + } + return nil + }) + if err != nil { + return fmt.Errorf("could not migrate session token storage: %w", err) + } + + p.l.Info("session token storage migration completed", + zap.Int("migrated", migratedCount), + zap.Int("deleted", deletedCount), + ) + return nil +} + +// extractKeyFromPackedToken extracts the ECDSA private key from a packed token. +func (p PersistentStorage) extractKeyFromPackedToken(packedToken []byte) (*ecdsa.PrivateKey, error) { + if len(packedToken) < 8 { + return nil, errors.New("packed token too short") + } + + rawKey := packedToken[8:] // skip 8-byte expiration timestamp + + var err error + if p.gcm != nil { + rawKey, err = p.decrypt(rawKey) + if err != nil { + return nil, fmt.Errorf("could not decrypt session key: %w", err) + } + } + + privKey, err := x509.ParseECPrivateKey(rawKey) + if err != nil { + return nil, fmt.Errorf("could not parse private key: %w", err) + } + + return privKey, nil +} diff --git a/pkg/util/state/migration_test.go b/pkg/util/state/migration_test.go index 0297fc5894..ac1e9b930a 100644 --- a/pkg/util/state/migration_test.go +++ b/pkg/util/state/migration_test.go @@ -4,13 +4,16 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "encoding/json" "path/filepath" "testing" "github.com/nspcc-dev/bbolt" + "github.com/nspcc-dev/neofs-node/internal/testutil" "github.com/nspcc-dev/neofs-sdk-go/user" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" + "go.uber.org/zap" ) func TestMigrateOldTokenStorage(t *testing.T) { @@ -20,11 +23,9 @@ func TestMigrateOldTokenStorage(t *testing.T) { ownerID1 := usertest.ID() ownerID2 := usertest.ID() - exOwnerID := usertest.ID() tokenID1 := []byte("token-id-1") tokenID2 := []byte("token-id-2") tokenID3 := []byte("token-id-3") - exTokenID := []byte("existing-token") key1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) @@ -32,13 +33,10 @@ func TestMigrateOldTokenStorage(t *testing.T) { require.NoError(t, err) key3, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - exKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) epoch1 := uint64(100) epoch2 := uint64(200) epoch3 := uint64(300) - exEpoch := uint64(250) createOldSessionDB(t, oldDBPath, map[user.ID]map[string]tokenData{ ownerID1: { @@ -51,43 +49,27 @@ func TestMigrateOldTokenStorage(t *testing.T) { }) newStorage := newStorageWithSession(t, newDBPath) - require.NoError(t, newStorage.Store(*exKey, exOwnerID, exTokenID, exEpoch)) err = newStorage.MigrateOldTokenStorage(oldDBPath) require.NoError(t, err) - t.Run("verify token 1", func(t *testing.T) { - token := newStorage.GetToken(ownerID1, tokenID1) - require.NotNil(t, token) - require.Equal(t, epoch1, token.ExpiredAt()) - require.Equal(t, key1, token.SessionKey()) - }) + require.NoError(t, newStorage.db.View(func(tx *bbolt.Tx) error { + rootBucket := tx.Bucket(sessionsBucket) + require.NotNil(t, rootBucket) - t.Run("verify token 2", func(t *testing.T) { - token := newStorage.GetToken(ownerID1, tokenID2) - require.NotNil(t, token) - require.Equal(t, epoch2, token.ExpiredAt()) - require.Equal(t, key2, token.SessionKey()) - }) + owner1Bucket := rootBucket.Bucket(ownerID1[:]) + require.NotNil(t, owner1Bucket) + require.NotNil(t, owner1Bucket.Get(tokenID1)) + require.NotNil(t, owner1Bucket.Get(tokenID2)) - t.Run("verify token 3", func(t *testing.T) { - token := newStorage.GetToken(ownerID2, tokenID3) - require.NotNil(t, token) - require.Equal(t, epoch3, token.ExpiredAt()) - require.Equal(t, key3, token.SessionKey()) - }) + owner2Bucket := rootBucket.Bucket(ownerID2[:]) + require.NotNil(t, owner2Bucket) + require.NotNil(t, owner2Bucket.Get(tokenID3)) - t.Run("existing token preserved", func(t *testing.T) { - token := newStorage.GetToken(exOwnerID, exTokenID) - require.NotNil(t, token) - require.Equal(t, exEpoch, token.ExpiredAt()) - require.Equal(t, exKey, token.SessionKey()) - }) + require.Nil(t, owner1Bucket.Get([]byte("non-existent"))) - t.Run("non-existent token", func(t *testing.T) { - token := newStorage.GetToken(usertest.ID(), []byte("non-existent")) - require.Nil(t, token) - }) + return nil + })) } func TestMigrateEmptyOldTokenStorage(t *testing.T) { @@ -102,8 +84,17 @@ func TestMigrateEmptyOldTokenStorage(t *testing.T) { err := newStorage.MigrateOldTokenStorage(oldDBPath) require.NoError(t, err) - token := newStorage.GetToken(usertest.ID(), []byte("any-token")) - require.Nil(t, token) + require.NoError(t, newStorage.db.View(func(tx *bbolt.Tx) error { + rootBucket := tx.Bucket(sessionsBucket) + if rootBucket == nil { + return nil + } + c := rootBucket.Cursor() + k, v := c.First() + require.Nil(t, k) + require.Nil(t, v) + return nil + })) } func TestMigrateOldTokenStorageMultipleOwners(t *testing.T) { @@ -135,14 +126,21 @@ func TestMigrateOldTokenStorageMultipleOwners(t *testing.T) { err := newStorage.MigrateOldTokenStorage(oldDBPath) require.NoError(t, err) - for ownerID, tokens := range allTokens { - for tokenIDStr, tokenData := range tokens { - token := newStorage.GetToken(ownerID, []byte(tokenIDStr)) - require.NotNil(t, token) - require.Equal(t, tokenData.epoch, token.ExpiredAt()) - require.Equal(t, tokenData.key.D, token.SessionKey().D) + require.NoError(t, newStorage.db.View(func(tx *bbolt.Tx) error { + rootBucket := tx.Bucket(sessionsBucket) + require.NotNil(t, rootBucket) + + for _, ownerID := range owners { + ownerBucket := rootBucket.Bucket(ownerID[:]) + require.NotNil(t, ownerBucket) + + for tokenIDStr := range allTokens[ownerID] { + tokenID := []byte(tokenIDStr) + require.NotNil(t, ownerBucket.Get(tokenID)) + } } - } + return nil + })) } func TestMigrateOldTokenStorageNonExistentFile(t *testing.T) { @@ -180,10 +178,140 @@ func TestMigrateOldTokenStorageWithEncryption(t *testing.T) { err = newStorage.MigrateOldTokenStorage(oldDBPath) require.NoError(t, err) - token := newStorage.GetToken(ownerID, tokenID) - require.NotNil(t, token) - require.Equal(t, epoch, token.ExpiredAt()) - require.Equal(t, sessionKey.D, token.SessionKey().D) + require.NoError(t, newStorage.db.View(func(tx *bbolt.Tx) error { + rootBucket := tx.Bucket(sessionsBucket) + require.NotNil(t, rootBucket) + + ownerBucket := rootBucket.Bucket(ownerID[:]) + require.NotNil(t, ownerBucket) + require.NotNil(t, ownerBucket.Get(tokenID)) + + return nil + })) +} + +func TestMigrateSessionTokensToAccounts(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "sessions.db") + + db, err := bbolt.Open(dbPath, 0o600, nil) + require.NoError(t, err) + + ownerID1 := usertest.ID() + ownerID2 := usertest.ID() + ownerID3 := usertest.ID() + ownerID4 := usertest.ID() + + key1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + key2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + key3, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + pubKeyID1 := user.NewFromECDSAPublicKey(key1.PublicKey) + pubKeyID2 := user.NewFromECDSAPublicKey(key2.PublicKey) + pubKeyID3 := user.NewFromECDSAPublicKey(key3.PublicKey) + + uuid1 := []byte("0123456789abcdef") + uuid2 := []byte("fedcba9876543210") + + epoch1 := uint64(100) + epoch2 := uint64(200) + epoch3 := uint64(300) + + err = db.Update(func(tx *bbolt.Tx) error { + rootBucket, err := tx.CreateBucketIfNotExists(sessionsBucket) + require.NoError(t, err) + + tempStorage := &PersistentStorage{db: db} + + // Owner 1 with one UUID token + ownerBucket1, err := rootBucket.CreateBucket(ownerID1[:]) + require.NoError(t, err) + + packedToken1, err := tempStorage.packToken(epoch1, key1) + require.NoError(t, err) + + err = ownerBucket1.Put(uuid1, packedToken1) + require.NoError(t, err) + + // Owner 2 with one token that has both UUID and pubkey ID entries + ownerBucket2, err := rootBucket.CreateBucket(ownerID2[:]) + require.NoError(t, err) + + packedToken2, err := tempStorage.packToken(epoch2, key2) + require.NoError(t, err) + err = ownerBucket2.Put(uuid2, packedToken2) + require.NoError(t, err) + err = ownerBucket2.Put(pubKeyID2[:], packedToken2) + require.NoError(t, err) + + // Owner 3 with one token with pubkey ID + ownerBucket3, err := rootBucket.CreateBucket(ownerID3[:]) + require.NoError(t, err) + + packedToken3, err := tempStorage.packToken(epoch3, key3) + require.NoError(t, err) + err = ownerBucket3.Put(pubKeyID3[:], packedToken3) + require.NoError(t, err) + + // Owner 4 with no tokens + _, err = rootBucket.CreateBucket(ownerID4[:]) + require.NoError(t, err) + + return nil + }) + require.NoError(t, err) + _ = db.Close() + + logger, logBuf := testutil.NewBufferedLogger(t, zap.DebugLevel) + storage, err := NewPersistentStorage(dbPath, true, WithLogger(logger)) + require.NoError(t, err) + defer func() { _ = storage.Close() }() + + require.NoError(t, storage.MigrateSessionTokensToAccounts()) + + logBuf.AssertContains(testutil.LogEntry{ + Level: zap.InfoLevel, + Message: "session token storage migration completed", + Fields: map[string]any{ + "migrated": json.Number("3"), + "deleted": json.Number("4"), + }, + }) + + token1 := storage.GetToken(pubKeyID1) + require.NotNil(t, token1) + require.Equal(t, epoch1, token1.ExpiredAt()) + require.Equal(t, key1, token1.SessionKey()) + + token2 := storage.GetToken(pubKeyID2) + require.NotNil(t, token2) + require.Equal(t, epoch2, token2.ExpiredAt()) + require.Equal(t, key2, token2.SessionKey()) + + token3 := storage.GetToken(pubKeyID3) + require.NotNil(t, token3) + require.Equal(t, epoch3, token3.ExpiredAt()) + require.Equal(t, key3, token3.SessionKey()) + + require.NoError(t, storage.db.View(func(tx *bbolt.Tx) error { + rootBucket := tx.Bucket(sessionsBucket) + if rootBucket == nil { + return nil + } + + require.Nil(t, rootBucket.Bucket(ownerID1[:])) + require.Nil(t, rootBucket.Bucket(ownerID2[:])) + require.Nil(t, rootBucket.Bucket(ownerID3[:])) + require.Nil(t, rootBucket.Bucket(ownerID4[:])) + + return nil + })) + + require.NoError(t, storage.MigrateSessionTokensToAccounts()) + logBuf.AssertContainsMsg(zap.DebugLevel, "session token storage migration is not needed, already migrated") } type tokenData struct { diff --git a/pkg/util/state/token.go b/pkg/util/state/token.go index aafc475eba..8b7bb9015f 100644 --- a/pkg/util/state/token.go +++ b/pkg/util/state/token.go @@ -3,7 +3,6 @@ package state import ( "crypto/aes" "crypto/cipher" - "encoding/hex" "fmt" "github.com/nspcc-dev/bbolt" @@ -49,22 +48,17 @@ func (p *PersistentStorage) initTokenStore(cfg cfg) error { return nil } -// GetToken returns private token corresponding to the given identifiers. +// GetToken returns private token corresponding to the given account. // // Returns nil is there is no element in storage. -func (p PersistentStorage) GetToken(ownerID user.ID, tokenID []byte) (t *session.PrivateToken) { +func (p PersistentStorage) GetToken(account user.ID) (t *session.PrivateToken) { err := p.db.View(func(tx *bbolt.Tx) error { rootBucket := tx.Bucket(sessionsBucket) if rootBucket == nil { return nil } - ownerBucket := rootBucket.Bucket(ownerID[:]) - if ownerBucket == nil { - return nil - } - - rawToken := ownerBucket.Get(tokenID) + rawToken := rootBucket.Get(account[:]) if rawToken == nil { return nil } @@ -81,8 +75,7 @@ func (p PersistentStorage) GetToken(ownerID user.ID, tokenID []byte) (t *session if err != nil { p.l.Error("could not get session from persistent storage", zap.Error(err), - zap.Stringer("ownerID", ownerID), - zap.String("tokenID", hex.EncodeToString(tokenID)), + zap.Stringer("account", account), ) } @@ -94,25 +87,21 @@ func (p PersistentStorage) RemoveOldTokens(epoch uint64) { err := p.db.Update(func(tx *bbolt.Tx) error { rootBucket := tx.Bucket(sessionsBucket) - // iterating over ownerIDs - return iterateNestedBuckets(rootBucket, func(b *bbolt.Bucket) error { - c := b.Cursor() - var err error - - // iterating over fixed ownerID's tokens - for k, v := c.First(); k != nil; k, v = c.Next() { - if epochFromToken(v) <= epoch { - err = c.Delete() - if err != nil { - p.l.Error("could not delete %s token", - zap.String("token_id", hex.EncodeToString(k)), - ) - } + // iterating over accounts + c := rootBucket.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + if epochFromToken(v) <= epoch { + err := c.Delete() + var id user.ID + copy(id[:], k) + if err != nil { + p.l.Error("could not delete token", + zap.Stringer("account", id), + ) } } - - return nil - }) + } + return nil }) if err != nil { p.l.Error("could not clean up expired tokens", @@ -124,7 +113,7 @@ func (p PersistentStorage) RemoveOldTokens(epoch uint64) { // FindTokenBySubjects searches for a private token whose public key // matches any of the given user ID Targets. // Returns nil if no matching non-expired token is found. -func (p PersistentStorage) FindTokenBySubjects(ownerID user.ID, subjects []sessionv2.Target) *session.PrivateToken { +func (p PersistentStorage) FindTokenBySubjects(subjects []sessionv2.Target) *session.PrivateToken { var token *session.PrivateToken err := p.db.View(func(tx *bbolt.Tx) error { rootBucket := tx.Bucket(sessionsBucket) @@ -132,14 +121,9 @@ func (p PersistentStorage) FindTokenBySubjects(ownerID user.ID, subjects []sessi return nil } - ownerBucket := rootBucket.Bucket(ownerID[:]) - if ownerBucket == nil { - return nil - } - for _, subject := range subjects { if subjectUser := subject.UserID(); !subjectUser.IsZero() { - rawToken := ownerBucket.Get(subjectUser[:]) + rawToken := rootBucket.Get(subjectUser[:]) if rawToken == nil { continue } @@ -160,7 +144,6 @@ func (p PersistentStorage) FindTokenBySubjects(ownerID user.ID, subjects []sessi if err != nil { p.l.Error("could not search for any subject in persistent storage", zap.Error(err), - zap.Stringer("ownerID", ownerID), zap.Stringers("subjects", subjects), ) } diff --git a/pkg/util/state/util.go b/pkg/util/state/util.go index 5841123206..ac43cef3aa 100644 --- a/pkg/util/state/util.go +++ b/pkg/util/state/util.go @@ -6,7 +6,6 @@ import ( "encoding/binary" "fmt" - "github.com/nspcc-dev/bbolt" "github.com/nspcc-dev/neofs-node/pkg/util/state/session" ) @@ -57,20 +56,3 @@ func (p PersistentStorage) unpackToken(raw []byte) (*session.PrivateToken, error func epochFromToken(rawToken []byte) uint64 { return binary.LittleEndian.Uint64(rawToken) } - -func iterateNestedBuckets(b *bbolt.Bucket, fn func(b *bbolt.Bucket) error) error { - c := b.Cursor() - - for k, v := c.First(); k != nil; k, v = c.Next() { - // nil value is a hallmark - // of the nested buckets - if v == nil { - err := fn(b.Bucket(k)) - if err != nil { - return err - } - } - } - - return nil -}