Skip to content

Commit d61a108

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 d61a108

File tree

2 files changed

+217
-22
lines changed

2 files changed

+217
-22
lines changed

session/sql_migration.go

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,35 +40,16 @@ func MigrateSessionStoreToSQL(ctx context.Context, kvStore *bbolt.DB,
4040
return err
4141
}
4242

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-
}
43+
initialGroupSessions, linkedSessions := filterSessions(kvSessions)
6544

45+
// Migrate the non-linked sessions first.
6646
err = migrateSessionsToSQLAndValidate(ctx, tx, initialGroupSessions)
6747
if err != nil {
6848
return fmt.Errorf("migration of non-linked session failed: %w",
6949
err)
7050
}
7151

52+
// Then migrate the linked sessions.
7253
err = migrateSessionsToSQLAndValidate(ctx, tx, linkedSessions)
7354
if err != nil {
7455
return fmt.Errorf("migration of linked session failed: %w", err)
@@ -81,6 +62,70 @@ func MigrateSessionStoreToSQL(ctx context.Context, kvStore *bbolt.DB,
8162
return nil
8263
}
8364

65+
// filterSessions categorizes the sessions into two groups: initial group
66+
// sessions and linked sessions. The initial group sessions are the first
67+
// sessions in a session group, while the linked sessions are those that have a
68+
// linked parent session. These are separated to ensure that we can insert the
69+
// initial group sessions first, which allows us to fetch the SQL group ID when
70+
// inserting the rest of the linked sessions afterward.
71+
//
72+
// Additionally, it checks for duplicate session IDs and drops all but
73+
// one session with the same ID, keeping the one with the latest CreatedAt
74+
// timestamp. Note that users with duplicate session IDs should be extremely
75+
// rare, as it could only occur if colliding session IDs were created prior to
76+
// the introduction of the session linking functionality.
77+
func filterSessions(kvSessions []*Session) ([]*Session, []*Session) {
78+
// First map sessions by their ID.
79+
sessionsByID := make(map[ID][]*Session)
80+
for _, s := range kvSessions {
81+
sessionsByID[s.ID] = append(sessionsByID[s.ID], s)
82+
}
83+
84+
var (
85+
initialGroupSessions []*Session
86+
linkedSessions []*Session
87+
)
88+
89+
// Process the mapped sessions. If there are duplicate sessions with the
90+
// same ID, we will only iterate the session with the latest CreatedAt
91+
// timestamp, and drop the other sessions. This is to ensure that we can
92+
// keep a UNIQUE constraint for the session ID (alias) in the SQL db.
93+
for id, sessions := range sessionsByID {
94+
sessionToKeep := sessions[0]
95+
if len(sessions) > 1 {
96+
log.Warnf("Found %d sessions with duplicate ID %x, "+
97+
"keeping only the latest one", len(sessions),
98+
id)
99+
100+
// Find the session with the latest timestamp in a single pass.
101+
latestSession := sessions[0]
102+
for _, s := range sessions[1:] {
103+
if s.CreatedAt.After(latestSession.CreatedAt) {
104+
latestSession = s
105+
}
106+
}
107+
sessionToKeep = latestSession
108+
109+
// Log the sessions that will be dropped.
110+
for _, s := range sessions[1:] {
111+
log.Warnf("Dropping duplicate session with ID "+
112+
"%x created at %v", id, s.CreatedAt)
113+
}
114+
}
115+
116+
// Categorize the session that we are keeping.
117+
if sessionToKeep.GroupID == sessionToKeep.ID {
118+
initialGroupSessions = append(
119+
initialGroupSessions, sessionToKeep,
120+
)
121+
} else {
122+
linkedSessions = append(linkedSessions, sessionToKeep)
123+
}
124+
}
125+
126+
return initialGroupSessions, linkedSessions
127+
}
128+
84129
// getBBoltSessions is a helper function that fetches all sessions from the
85130
// Bbolt store, by iterating directly over the buckets, without needing to
86131
// use any public functions of the BoltStore struct.

session/sql_migration_test.go

Lines changed: 150 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+
sess4.CreatedAt.Add(time.Minute),
428+
)
429+
require.NoError(t, err)
430+
431+
err = updateSessionIDAndCreatedAt(
432+
store, sess6.ID, sess4.MacaroonRootKey,
433+
sess4.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,44 @@ 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+
// Update the session.
947+
sess.ID = newId
948+
sess.GroupID = newId
949+
sess.MacaroonRootKey = newIdRootKey
950+
sess.CreatedAt = newCreatedAt
951+
952+
// Write it back under the same key (local pubkey).
953+
return putSession(sessionBkt, sess)
954+
})
955+
}

0 commit comments

Comments
 (0)