diff --git a/CHANGELOG.md b/CHANGELOG.md index 2174caa93c..13923f5d23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,12 @@ 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 ### 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/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/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= 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 -}