Skip to content

Commit 0ea7e9f

Browse files
session: handle multiple sessions with the same ID
The logic to avoid duplicate session IDs was first added with the session linking linking functionality. That means that there may be old legacy sessions that were added prior to that which may have duplicate IDs. Before this commit, such sessions would cause the kvdb to SQL migration to error, as there is a uniqueness constraint on the legacy session alias in the SQL sessions table. We want to keep that constraint, so in such rare cases, we update the kvdb to SQL migration logic to drop all but the session with the latest CreatedAt time if for sessions that share the same ID. That follows the logic in c8b78bd as closely as possible, as that migration ensured that all sessions but the latest one was revoked if multiple sessions shared the same ID.
1 parent 2d3564b commit 0ea7e9f

File tree

2 files changed

+240
-22
lines changed

2 files changed

+240
-22
lines changed

session/sql_migration.go

Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"reflect"
10+
"sort"
1011
"time"
1112

1213
"github.com/davecgh/go-spew/spew"
@@ -40,35 +41,16 @@ func MigrateSessionStoreToSQL(ctx context.Context, kvStore *bbolt.DB,
4041
return err
4142
}
4243

43-
// If sessions are linked to a group, we must insert the initial session
44-
// of each group before the other sessions in that group. This ensures
45-
// we can retrieve the SQL group ID when inserting the remaining
46-
// sessions. Therefore, we first insert all initial group sessions,
47-
// allowing us to fetch the group IDs and insert the rest of the
48-
// sessions afterward.
49-
// We therefore filter out the initial sessions first, and then migrate
50-
// them prior to the rest of the sessions.
51-
var (
52-
initialGroupSessions []*Session
53-
linkedSessions []*Session
54-
)
55-
56-
for _, kvSession := range kvSessions {
57-
if kvSession.GroupID == kvSession.ID {
58-
initialGroupSessions = append(
59-
initialGroupSessions, kvSession,
60-
)
61-
} else {
62-
linkedSessions = append(linkedSessions, kvSession)
63-
}
64-
}
44+
initialGroupSessions, linkedSessions := filterSessions(kvSessions)
6545

46+
// Migrate the non-linked sessions first.
6647
err = migrateSessionsToSQLAndValidate(ctx, tx, initialGroupSessions)
6748
if err != nil {
6849
return fmt.Errorf("migration of non-linked session failed: %w",
6950
err)
7051
}
7152

53+
// Then migrate the linked sessions.
7254
err = migrateSessionsToSQLAndValidate(ctx, tx, linkedSessions)
7355
if err != nil {
7456
return fmt.Errorf("migration of linked session failed: %w", err)
@@ -81,6 +63,87 @@ func MigrateSessionStoreToSQL(ctx context.Context, kvStore *bbolt.DB,
8163
return nil
8264
}
8365

66+
// filterSessions filters the sessions into two groups: initial group sessions
67+
// and linked sessions. The initial group sessions are the first sessions in a
68+
// session group, while the linked sessions are those that have a linked parent
69+
// session. These are separated to ensure that we can insert the initial
70+
// group sessions first, which allows us to fetch the SQL group ID when
71+
// inserting the rest of the linked sessions afterward.
72+
//
73+
// Additionally, it checks for duplicate session IDs and drops all but
74+
// one session with the same ID, keeping the one with the latest CreatedAt
75+
// timestamp.
76+
func filterSessions(kvSessions []*Session) ([]*Session, []*Session) {
77+
var (
78+
initialGroupSessions []*Session
79+
linkedSessions []*Session
80+
sessionIDs = make(map[ID][]*Session)
81+
hasDuplicateIDs bool
82+
)
83+
84+
for _, kvSession := range kvSessions {
85+
// Check for duplicate session IDs.
86+
if dupSess, exists := sessionIDs[kvSession.ID]; exists {
87+
log.Warnf("Duplicate session ID (%x) found, will drop "+
88+
"all but one session of the sessions with "+
89+
"that ID during the migration", kvSession.ID)
90+
91+
hasDuplicateIDs = true
92+
sessionIDs[kvSession.ID] = append(dupSess, kvSession)
93+
} else {
94+
sessionIDs[kvSession.ID] = []*Session{kvSession}
95+
}
96+
}
97+
98+
// If there are duplicate sessions with the same ID, we will only
99+
// migrate the session with the latest CreatedAt timestamp, and drop the
100+
// other sessions. This is to ensure that we can keep a UNIQUE
101+
// constraint for the session ID (alias) in the SQL db.
102+
if hasDuplicateIDs {
103+
filteredSessions := make([]*Session, 0, len(sessionIDs))
104+
105+
for sessID, sessions := range sessionIDs {
106+
if len(sessions) > 1 {
107+
sort.Slice(sessions, func(i, j int) bool {
108+
return sessions[i].CreatedAt.After(
109+
sessions[j].CreatedAt,
110+
)
111+
})
112+
113+
filteredSessions = append(
114+
filteredSessions, sessions[0],
115+
)
116+
117+
for _, sess := range sessions[1:] {
118+
log.Warnf("Dropping duplicate session "+
119+
"with ID %x created at %v",
120+
sessID, sess.CreatedAt)
121+
}
122+
} else {
123+
// If there are no duplicates, we can just
124+
// append the session to the filtered list.
125+
filteredSessions = append(
126+
filteredSessions, sessions[0],
127+
)
128+
}
129+
}
130+
131+
kvSessions = filteredSessions
132+
}
133+
134+
for _, kvSession := range kvSessions {
135+
if kvSession.GroupID == kvSession.ID {
136+
initialGroupSessions = append(
137+
initialGroupSessions, kvSession,
138+
)
139+
} else {
140+
linkedSessions = append(linkedSessions, kvSession)
141+
}
142+
}
143+
144+
return initialGroupSessions, linkedSessions
145+
}
146+
84147
// getBBoltSessions is a helper function that fetches all sessions from the
85148
// Bbolt store, by iterating directly over the buckets, without needing to
86149
// use any public functions of the BoltStore struct.

session/sql_migration_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,115 @@ func TestSessionsStoreMigration(t *testing.T) {
352352
return getBoltStoreSessions(t, store)
353353
},
354354
},
355+
{
356+
name: "multiple sessions with the same ID",
357+
populateDB: func(t *testing.T, store *BoltStore,
358+
_ accounts.Store) []*Session {
359+
360+
// We first add one session which has no other
361+
// session with same ID, to test that this is
362+
// correctly migrated, and included in the
363+
// migration result.
364+
sess1, err := store.NewSession(
365+
ctx, "session1", TypeMacaroonAdmin,
366+
time.Unix(1000, 0), "",
367+
)
368+
require.NoError(t, err)
369+
370+
sess2, err := store.NewSession(
371+
ctx, "session2", TypeMacaroonAdmin,
372+
time.Unix(1000, 0), "",
373+
)
374+
require.NoError(t, err)
375+
376+
// Then add two sessions with the same ID, to
377+
// test that only the latest session is included
378+
// in the migration result.
379+
sess3, err := store.NewSession(
380+
ctx, "session3", TypeMacaroonAdmin,
381+
time.Unix(1000, 0), "",
382+
)
383+
require.NoError(t, err)
384+
385+
// During the addition of the session linking
386+
// functionality, logic was added in the
387+
// NewSession function to ensure we can't create
388+
// multiple sessions with the same ID. Therefore
389+
// we need to manually override the ID of
390+
// the second session to match the first
391+
// session, to simulate such a scenario that
392+
// could occur prior to the addition of that
393+
// logic.
394+
// We also need to update the CreatedAt time
395+
// as the execution of this function is too
396+
// fast for the CreatedAt time of sess2 and
397+
// sess3 to differ.
398+
err = updateSessionIDAndCreatedAt(
399+
store, sess3.ID, sess2.MacaroonRootKey,
400+
sess2.CreatedAt.Add(time.Minute),
401+
)
402+
require.NoError(t, err)
403+
404+
// Finally, we add three sessions with the same
405+
// ID, to test we can handle more than two
406+
// sessions with the same ID.
407+
sess4, err := store.NewSession(
408+
ctx, "session4", TypeMacaroonAdmin,
409+
time.Unix(1000, 0), "",
410+
)
411+
require.NoError(t, err)
412+
413+
sess5, err := store.NewSession(
414+
ctx, "session5", TypeMacaroonAdmin,
415+
time.Unix(1000, 0), "",
416+
)
417+
require.NoError(t, err)
418+
419+
sess6, err := store.NewSession(
420+
ctx, "session6", TypeMacaroonAdmin,
421+
time.Unix(1000, 0), "",
422+
)
423+
require.NoError(t, err)
424+
425+
err = updateSessionIDAndCreatedAt(
426+
store, sess5.ID, sess4.MacaroonRootKey,
427+
sess2.CreatedAt.Add(time.Minute),
428+
)
429+
require.NoError(t, err)
430+
431+
err = updateSessionIDAndCreatedAt(
432+
store, sess6.ID, sess4.MacaroonRootKey,
433+
sess2.CreatedAt.Add(time.Minute*2),
434+
)
435+
require.NoError(t, err)
436+
437+
// Now fetch the updated sessions from the kv
438+
// store, so that we are sure that the new IDs
439+
// have really been persisted in the DB.
440+
kvSessions := getBoltStoreSessions(t, store)
441+
require.Len(t, kvSessions, 6)
442+
443+
getSessionByName := func(name string) *Session {
444+
for _, session := range kvSessions {
445+
if session.Label == name {
446+
return session
447+
}
448+
}
449+
450+
t.Fatalf("session %s not found", name)
451+
return nil
452+
}
453+
454+
// When multiple sessions with the same ID
455+
// exist, we expect only the session with the
456+
// latest creation time to be migrated.
457+
return []*Session{
458+
getSessionByName(sess1.Label),
459+
getSessionByName(sess3.Label),
460+
getSessionByName(sess6.Label),
461+
}
462+
},
463+
},
355464
{
356465
name: "randomized sessions",
357466
populateDB: randomizedSessions,
@@ -803,3 +912,49 @@ func shiftStateUnsafe(db *BoltStore, id ID, dest State) error {
803912
return putSession(sessionBucket, session)
804913
})
805914
}
915+
916+
// updateSessionIDAndCreatedAt can be used to update the ID, the GroupID,
917+
// the MacaroonRootKey and the CreatedAt time a session in the BoltStore.
918+
//
919+
// NOTE: this function should only be used for testing purposes. Also note that
920+
// we pass the macaroon root key to set the new session ID, as the
921+
// DeserializeSession function derives the session ID from the
922+
// session.MacaroonRootKey.
923+
func updateSessionIDAndCreatedAt(db *BoltStore, oldID ID, newIdRootKey uint64,
924+
newCreatedAt time.Time) error {
925+
926+
newId := IDFromMacRootKeyID(newIdRootKey)
927+
928+
if oldID == newId {
929+
return fmt.Errorf("can't update session ID to the same ID: %s",
930+
oldID)
931+
}
932+
933+
return db.Update(func(tx *bbolt.Tx) error {
934+
// Get the main session bucket.
935+
sessionBkt, err := getBucket(tx, sessionBucketKey)
936+
if err != nil {
937+
return err
938+
}
939+
940+
// Look up the session using the old ID.
941+
sess, err := getSessionByID(sessionBkt, oldID)
942+
if err != nil {
943+
return err
944+
}
945+
946+
// Delete the old serialized session (keyed by local pubkey).
947+
if err := sessionBkt.Delete(getSessionKey(sess)); err != nil {
948+
return err
949+
}
950+
951+
// Update the session ID.
952+
sess.ID = newId
953+
sess.GroupID = newId
954+
sess.MacaroonRootKey = newIdRootKey
955+
sess.CreatedAt = newCreatedAt
956+
957+
// Write it back under the same key (local pubkey).
958+
return putSession(sessionBkt, sess)
959+
})
960+
}

0 commit comments

Comments
 (0)