package session import ( "context" "fmt" "testing" "time" "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/db/sqlcmig6" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/sqldb/v2" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" "golang.org/x/exp/rand" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery/checkers" "gopkg.in/macaroon.v2" ) // TestSessionsStoreMigration tests the migration of session store from a bolt // backend to a SQL database. Note that this test does not attempt to be a // complete migration test. func TestSessionsStoreMigration(t *testing.T) { t.Parallel() ctx := context.Background() clock := clock.NewTestClock(time.Now()) // When using build tags that creates a kvdb store for NewTestDB, we // skip this test as it is only applicable for postgres and sqlite tags. store := NewTestDB(t, clock) if _, ok := store.(*BoltStore); ok { t.Skipf("Skipping session store migration test for kvdb build") } makeSQLDB := func(t *testing.T, acctStore accounts.Store) (*SQLStore, *sqlcmig6.TxExecutor) { // Create a sql store with a linked account store. testDBStore := NewTestDBWithAccounts(t, clock, acctStore) store, ok := testDBStore.(*SQLStore) require.True(t, ok) baseDB := store.BaseDB queries := sqlcmig6.NewForType(baseDB, baseDB.BackendType) return store, sqlcmig6.NewTxExecutor(baseDB, queries) } // assertMigrationResults asserts that the sql store contains the // same sessions as the passed kv store sessions. This is intended to be // run after the migration. assertMigrationResults := func(t *testing.T, sqlStore *SQLStore, kvSessions []*Session) { for _, kvSession := range kvSessions { // Fetch the migrated session from the sql store. sqlSession, err := sqlStore.GetSession( ctx, kvSession.ID, ) require.NoError(t, err) // Since the SQL store can't represent a session with // a non-nil MacaroonRecipe, but with nil caveats and // perms, we need to override the macaroon recipe if the // kvSession has such a recipe stored. overrideMacaroonRecipe(kvSession, sqlSession) // We also need to override the account ID if the // kvSession has an account ID set, while the SQL // session doesn't. overrideRemovedAccount(kvSession, sqlSession) // Finally, we also need to override specific feature's // config if they are empty. overrideFeatureConfig(kvSession, sqlSession) assertEqualSessions(t, kvSession, sqlSession) } // Finally we ensure that the sql store doesn't contain more // sessions than the kv store. sqlSessions, err := sqlStore.ListAllSessions(ctx) require.NoError(t, err) require.Equal(t, len(kvSessions), len(sqlSessions)) } tests := []struct { name string populateDB func( t *testing.T, kvStore *BoltStore, accountStore accounts.Store, ) []*Session }{ { name: "empty", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { // Don't populate the DB. return []*Session{} }, }, { name: "one session no options", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "multiple sessions no options", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { _, err := store.NewSession( ctx, "session1", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) _, err = store.NewSession( ctx, "session2", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) _, err = store.NewSession( ctx, "session3", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with one privacy flag", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "", WithPrivacy(PrivacyFlags{ClearPubkeys}), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with multiple privacy flags", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "", WithPrivacy(PrivacyFlags{ ClearChanInitiator, ClearHTLCs, ClearClosingTxIds, }), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with a feature config", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { featureConfig := map[string][]byte{ "AutoFees": {1, 2, 3, 4}, "AutoSomething": {4, 3, 4, 5, 6, 6}, } _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "", WithFeatureConfig(featureConfig), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with a feature config with empty", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { // Set a feature config, which has an empty // entry for a specific feature. featureConfig := map[string][]byte{ "AutoFees": {}, } _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "", WithFeatureConfig(featureConfig), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with a feature config with nil", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { // Set a feature config, which has a nil entry // for a specific feature. featureConfig := map[string][]byte{ "AutoFees": nil, } _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "", WithFeatureConfig(featureConfig), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with dev server", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "", WithDevServer(), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with macaroon recipe", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { // this test uses caveats & perms from the // tlv_test.go _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "foo.bar.baz:1234", WithMacaroonRecipe(caveats, perms), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with macaroon recipe nil caveats", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { // this test uses perms from the tlv_test.go _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "foo.bar.baz:1234", WithMacaroonRecipe(nil, perms), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with empty caveat verification id", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { rCaveats := []macaroon.Caveat{ { Id: []byte("id"), VerificationId: []byte{}, }, } _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "foo.bar.baz:1234", WithMacaroonRecipe(rCaveats, perms), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with macaroon recipe nil perms", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { // this test uses caveats from the tlv_test.go _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "foo.bar.baz:1234", WithMacaroonRecipe(caveats, nil), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "macaroon recipe with nil perms and caveats", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { _, err := store.NewSession( ctx, "test", TypeMacaroonAdmin, time.Unix(1000, 0), "foo.bar.baz:1234", WithMacaroonRecipe(nil, nil), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with a linked account", populateDB: func(t *testing.T, store *BoltStore, acctStore accounts.Store) []*Session { // Create an account with balance acct, err := acctStore.NewAccount( ctx, 1234, time.Now().Add(time.Hour), "", ) require.NoError(t, err) require.False(t, acct.HasExpired()) // For now, we manually add the account caveat // for bbolt compatibility. accountCaveat := checkers.Condition( macaroons.CondLndCustom, fmt.Sprintf("%s %x", accounts.CondAccount, acct.ID[:], ), ) sessCaveats := []macaroon.Caveat{ { Id: []byte(accountCaveat), }, } _, err = store.NewSession( ctx, "test", TypeMacaroonAccount, time.Unix(1000, 0), "", WithAccount(acct.ID), WithMacaroonRecipe(sessCaveats, nil), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "one session with a deleted linked account", populateDB: func(t *testing.T, store *BoltStore, acctStore accounts.Store) []*Session { // Create an account with balance acct, err := acctStore.NewAccount( ctx, 1234, time.Now().Add(time.Hour), "", ) require.NoError(t, err) require.False(t, acct.HasExpired()) // For now, we manually add the account caveat // for bbolt compatibility. accountCaveat := checkers.Condition( macaroons.CondLndCustom, fmt.Sprintf("%s %x", accounts.CondAccount, acct.ID[:], ), ) sessCaveats := []macaroon.Caveat{ { Id: []byte(accountCaveat), }, } _, err = store.NewSession( ctx, "test", TypeMacaroonAccount, time.Unix(1000, 0), "", WithAccount(acct.ID), WithMacaroonRecipe(sessCaveats, nil), ) require.NoError(t, err) // Now delete the account, which represents // that the user ran "litcli accounts remove". err = acctStore.RemoveAccount(ctx, acct.ID) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "linked session", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { // First create the initial session for the // group. sess1, err := store.NewSession( ctx, "initSession", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) // As the store won't allow us to link a // session before all sessions in the group have // been revoked, we revoke the session before // creating a new session that links to the // initial session. err = store.ShiftState( ctx, sess1.ID, StateCreated, ) require.NoError(t, err) err = store.ShiftState( ctx, sess1.ID, StateRevoked, ) require.NoError(t, err) _, err = store.NewSession( ctx, "linkedSession", TypeMacaroonAdmin, time.Unix(1000, 0), "", WithLinkedGroupID(&sess1.ID), ) require.NoError(t, err) return getBoltStoreSessions(t, store) }, }, { name: "multiple sessions with the same ID", populateDB: func(t *testing.T, store *BoltStore, _ accounts.Store) []*Session { // We first add one session which has no other // session with same ID, to test that this is // correctly migrated, and included in the // migration result. sess1, err := store.NewSession( ctx, "session1", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) sess2, err := store.NewSession( ctx, "session2", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) // Then add two sessions with the same ID, to // test that only the latest session is included // in the migration result. sess3, err := store.NewSession( ctx, "session3", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) // During the addition of the session linking // functionality, logic was added in the // NewSession function to ensure we can't create // multiple sessions with the same ID. Therefore // we need to manually override the ID of // the second session to match the first // session, to simulate such a scenario that // could occur prior to the addition of that // logic. // We also need to update the CreatedAt time // as the execution of this function is too // fast for the CreatedAt time of sess2 and // sess3 to differ. err = updateSessionIDAndCreatedAt( store, sess3.ID, sess2.MacaroonRootKey, sess2.CreatedAt.Add(time.Minute), ) require.NoError(t, err) // Finally, we add three sessions with the same // ID, to test we can handle more than two // sessions with the same ID. sess4, err := store.NewSession( ctx, "session4", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) sess5, err := store.NewSession( ctx, "session5", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) sess6, err := store.NewSession( ctx, "session6", TypeMacaroonAdmin, time.Unix(1000, 0), "", ) require.NoError(t, err) err = updateSessionIDAndCreatedAt( store, sess5.ID, sess4.MacaroonRootKey, sess4.CreatedAt.Add(time.Minute), ) require.NoError(t, err) err = updateSessionIDAndCreatedAt( store, sess6.ID, sess4.MacaroonRootKey, sess4.CreatedAt.Add(time.Minute*2), ) require.NoError(t, err) // Now fetch the updated sessions from the kv // store, so that we are sure that the new IDs // have really been persisted in the DB. kvSessions := getBoltStoreSessions(t, store) require.Len(t, kvSessions, 6) getSessionByName := func(name string) *Session { for _, session := range kvSessions { if session.Label == name { return session } } t.Fatalf("session %s not found", name) return nil } // When multiple sessions with the same ID // exist, we expect only the session with the // latest creation time to be migrated. return []*Session{ getSessionByName(sess1.Label), getSessionByName(sess3.Label), getSessionByName(sess6.Label), } }, }, { name: "randomized sessions", populateDB: randomizedSessions, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() // First let's create an account store to link to in // the sessions store. Note that this is will be a sql // store due to the build tags enabled when running this // test, which means that we won't need to migrate the // account store in this test. accountStore := accounts.NewTestDB(t, clock) kvStore, err := NewDB( t.TempDir(), DBFilename, clock, accountStore, ) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, kvStore.Close()) }) // populate the kvStore with the test data, in // preparation for the test. kvSessions := test.populateDB(t, kvStore, accountStore) // Proceed to create the sql store and execute the // migration. sqlStore, txEx := makeSQLDB(t, accountStore) err = txEx.ExecTx( ctx, sqldb.WriteTxOpt(), func(tx *sqlcmig6.Queries) error { return MigrateSessionStoreToSQL( ctx, kvStore.DB, tx, ) }, sqldb.NoOpReset, ) require.NoError(t, err) // The migration function will check if the inserted // sessions equals the migrated ones, but as a sanity // check we'll also fetch migrated sessions from the sql // store and compare them to the original. assertMigrationResults(t, sqlStore, kvSessions) }) } } // randomizedSessions adds 100 randomized sessions to the kvStore, where 25% of // them will contain up to 10 linked sessions. The rest of the session will have // the rest of the session options randomized. func randomizedSessions(t *testing.T, kvStore *BoltStore, accountsStore accounts.Store) []*Session { ctx := context.Background() var ( // numberOfSessions is set to 100 to add enough sessions to get // enough variation between randomized sessions, but kept low // enough for the test not take too long to run, as the test // time increases drastically by the number of sessions we // migrate. numberOfSessions = 100 ) for i := range numberOfSessions { var ( opts []Option serverAddr string ) macType := macaroonType(i) expiry := time.Unix(rand.Int63n(10000), rand.Int63n(10000)) label := fmt.Sprintf("session%d", i+1) // Half of the sessions will get a set server address. if rand.Intn(2) == 0 { serverAddr = "foo.bar.baz:1234" } // Every 10th session will get no added options. if i%10 != 0 { // Add random privacy flags to 50% of the sessions. if rand.Intn(2) == 0 { opts = append( opts, WithPrivacy(randomPrivacyFlags()), ) } // Add random feature configs to 50% of the sessions. if rand.Intn(2) == 0 { opts = append( opts, WithFeatureConfig( randomFeatureConfig(), ), ) } // Set that the session uses a dev server for 50% of the // sessions. if rand.Intn(2) == 0 { opts = append(opts, WithDevServer()) } // Add a random macaroon recipe to 50% of the sessions. if rand.Intn(2) == 0 { // In 50% of those cases, we add a random // macaroon recipe with caveats and perms, // and for the other 50% we added a linked // account with the correct macaroon recipe (to // simulate realistic data). if rand.Intn(2) == 0 { opts = append( opts, randomMacaroonRecipe(), ) } else { acctOpts := randomAccountOptions( ctx, t, accountsStore, ) opts = append(opts, acctOpts...) } } } // We insert the session with the randomized params and options. activeSess, err := kvStore.NewSession( ctx, label, macType, expiry, serverAddr, opts..., ) require.NoError(t, err) // For 25% of the sessions, we link a random number of sessions // to the session. if rand.Intn(4) == 0 { // Link up to 10 sessions to the session, and set the // same opts as the initial group session. for j := range rand.Intn(10) { // We first need to revoke the previous session // before we can create a new session that links // to the session. err = kvStore.ShiftState( ctx, activeSess.ID, StateCreated, ) require.NoError(t, err) err = kvStore.ShiftState( ctx, activeSess.ID, StateRevoked, ) require.NoError(t, err) opts = []Option{ WithLinkedGroupID(&activeSess.GroupID), } if activeSess.DevServer { opts = append(opts, WithDevServer()) } if activeSess.FeatureConfig != nil { opts = append(opts, WithFeatureConfig( *activeSess.FeatureConfig, )) } if activeSess.PrivacyFlags != nil { opts = append(opts, WithPrivacy( activeSess.PrivacyFlags, )) } if activeSess.MacaroonRecipe != nil { macRec := activeSess.MacaroonRecipe opts = append(opts, WithMacaroonRecipe( macRec.Caveats, macRec.Permissions, )) } activeSess.AccountID.WhenSome( func(alias accounts.AccountID) { opts = append( opts, WithAccount(alias), ) }, ) label = fmt.Sprintf("linkedSession%d", j+1) activeSess, err = kvStore.NewSession( ctx, label, activeSess.Type, time.Unix(1000, 0), activeSess.ServerAddr, opts..., ) require.NoError(t, err) } } // When an account is set, we also remove the account in 50% of // the cases. This simulates that the user ran "litcli accounts // remove" after creating the session. activeSess.AccountID.WhenSome(func(id accounts.AccountID) { if rand.Intn(2) == 0 { err = accountsStore.RemoveAccount(ctx, id) } }) require.NoError(t, err) // Finally, we shift the active session to a random state. // As the state we set may be a state that's no longer set // through the current code base, or be an illegal state // transition, we use an alternative test state shifting method // that doesn't check that we transition the state in the legal // order. err = shiftStateUnsafe(kvStore, activeSess.ID, lastState(i)) require.NoError(t, err) } return getBoltStoreSessions(t, kvStore) } // macaroonType returns a macaroon type based on the given index by taking the // index modulo 6. This ensures an approximately equal distribution of macaroon // types. func macaroonType(i int) Type { switch i % 6 { case 0: return TypeMacaroonReadonly case 1: return TypeMacaroonAdmin case 2: return TypeMacaroonCustom case 3: return TypeUIPassword case 4: return TypeAutopilot default: return TypeMacaroonAccount } } // lastState returns a state based on the given index by taking the index modulo // 5. This ensures an approximately equal distribution of states. func lastState(i int) State { switch i % 5 { case 0: return StateCreated case 1: return StateInUse case 2: return StateRevoked case 3: return StateExpired default: return StateReserved } } // randomPrivacyFlags returns a random set of privacy flags. func randomPrivacyFlags() PrivacyFlags { allFlags := []PrivacyFlag{ ClearPubkeys, ClearChanIDs, ClearTimeStamps, ClearChanInitiator, ClearHTLCs, ClearClosingTxIds, ClearNetworkAddresses, } var privFlags []PrivacyFlag for _, flag := range allFlags { if rand.Intn(2) == 0 { privFlags = append(privFlags, flag) } } return privFlags } // randomFeatureConfig returns a random feature config with a random number of // features. The feature names are generated as "feature0", "feature1", etc. func randomFeatureConfig() FeaturesConfig { featureConfig := make(FeaturesConfig) for i := range rand.Intn(10) { var ( featureName = fmt.Sprintf("feature%d", i) configValue []byte ) // For the vast majority of the features, we set a value for the // feature's config. But in 1/5 of the cases, we set an empty // config. if rand.Intn(5) != 0 { configValue = []byte{byte(rand.Int31())} } featureConfig[featureName] = configValue } return featureConfig } // randomMacaroonRecipe returns a random macaroon recipe with a random number of // caveats and permissions. The returned macaroon recipe may have nil set for // either the caveats or permissions, but not both. func randomMacaroonRecipe() Option { var ( macCaveats []macaroon.Caveat macPerms []bakery.Op ) loopLen := rand.Intn(10) + 1 if rand.Intn(2) == 0 { for range loopLen { var macCaveat macaroon.Caveat // We always have a caveat.Id, but the rest are // randomized if they exist or not. macCaveat.Id = randomBytes(rand.Intn(10) + 1) if rand.Intn(2) == 0 { macCaveat.VerificationId = randomBytes(rand.Intn(32) + 1) } if rand.Intn(2) == 0 { macCaveat.Location = randomString(rand.Intn(10) + 1) } macCaveats = append(macCaveats, macCaveat) } } else { macCaveats = nil } // We can't do both nil caveats and nil perms, so if we have nil // caveats, we set perms to a value. if rand.Intn(2) == 0 || macCaveats == nil { for range loopLen { var macPerm bakery.Op macPerm.Action = randomString(rand.Intn(10) + 1) macPerm.Entity = randomString(rand.Intn(10) + 1) macPerms = append(macPerms, macPerm) } } else { macPerms = nil } return WithMacaroonRecipe(macCaveats, macPerms) } // randomAccountOptions creates a random account with a random balance and // expiry time, that's linked in the returned options. The returned options also // returns the macaroon recipe with the account caveat. func randomAccountOptions(ctx context.Context, t *testing.T, acctStore accounts.Store) []Option { balance := lnwire.MilliSatoshi(rand.Int63()) // randomize expiry from 10 to 10,000 minutes expiry := time.Now().Add( time.Minute * time.Duration(rand.Intn(10000-10)+10), ) // As the store has a unique constraint for inserting labels, we suffix // it with a sufficiently large random number avoid collisions. label := fmt.Sprintf("account:%d", rand.Int63()) // Create an account with balance acct, err := acctStore.NewAccount(ctx, balance, expiry, label) require.NoError(t, err) require.False(t, acct.HasExpired()) // For now, we manually add the account caveat // for bbolt compatibility. accountCaveat := checkers.Condition( macaroons.CondLndCustom, fmt.Sprintf("%s %x", accounts.CondAccount, acct.ID[:], ), ) sessCaveats := []macaroon.Caveat{} sessCaveats = append( sessCaveats, macaroon.Caveat{ Id: []byte(accountCaveat), }, ) opts := []Option{ WithAccount(acct.ID), WithMacaroonRecipe(sessCaveats, nil), } return opts } // randomBytes generates a random byte array of the passed length n. func randomBytes(n int) []byte { b := make([]byte, n) for i := range b { // Random int between 0-255, then cast to byte. b[i] = byte(rand.Intn(256)) } return b } // randomString generates a random string of the passed length n. func randomString(n int) string { letterBytes := "abcdefghijklmnopqrstuvwxyz" b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) } // getBoltStoreSessions is a helper function that fetches all sessions // from the kv store, while already asserting that there no error occurs // when retrieving the sessions. func getBoltStoreSessions(t *testing.T, db *BoltStore) []*Session { kvSessions, err := getBBoltSessions(db.DB) require.NoError(t, err) return kvSessions } // shiftStateUnsafe updates the state of the session with the given ID to the // "dest" state, without checking if the state transition is legal. // // NOTE: this function should only be used for testing purposes. func shiftStateUnsafe(db *BoltStore, id ID, dest State) error { return db.Update(func(tx *bbolt.Tx) error { sessionBucket, err := getBucket(tx, sessionBucketKey) if err != nil { return err } session, err := getSessionByID(sessionBucket, id) if err != nil { return err } // If the session is already in the desired state, we return // with no error to maintain idempotency. if session.State == dest { return nil } session.State = dest // If the session is terminal, we set the revoked at time to the // current time. if dest.Terminal() { session.RevokedAt = db.clock.Now().UTC() } return putSession(sessionBucket, session) }) } // updateSessionIDAndCreatedAt can be used to update the ID, the GroupID, // the MacaroonRootKey and the CreatedAt time a session in the BoltStore. // // NOTE: this function should only be used for testing purposes. Also note that // we pass the macaroon root key to set the new session ID, as the // DeserializeSession function derives the session ID from the // session.MacaroonRootKey. func updateSessionIDAndCreatedAt(db *BoltStore, oldID ID, newIdRootKey uint64, newCreatedAt time.Time) error { newId := IDFromMacRootKeyID(newIdRootKey) if oldID == newId { return fmt.Errorf("can't update session ID to the same ID: %s", oldID) } return db.Update(func(tx *bbolt.Tx) error { // Get the main session bucket. sessionBkt, err := getBucket(tx, sessionBucketKey) if err != nil { return err } // Look up the session using the old ID. sess, err := getSessionByID(sessionBkt, oldID) if err != nil { return err } // Update the session. sess.ID = newId sess.GroupID = newId sess.MacaroonRootKey = newIdRootKey sess.CreatedAt = newCreatedAt // Write it back under the same key (local pubkey). return putSession(sessionBkt, sess) }) }