mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-11-07 08:24:52 +00:00
fix(session): keep cooldown trackers and trim stale chunks by key
Cleanup evicted a refresh tracker by last attempt time alone. A tracker could leave its cooldown early. Cleanup now also requires the cooldown to be over. The stale chunk trim deleted an index range derived from the chunk count. A sparse key set kept stale high keys and failed the read. The trim now deletes every key at or above token_total.
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cleanup must not evict a tracker whose cooldown is still running.
|
||||||
|
func TestRefreshCleanupKeepsTrackerInCooldown(t *testing.T) {
|
||||||
|
cfg := DefaultRefreshCoordinatorConfig()
|
||||||
|
cfg.MaxRefreshAttempts = 2
|
||||||
|
cfg.RefreshAttemptWindow = 20 * time.Millisecond
|
||||||
|
cfg.RefreshCooldownPeriod = time.Hour
|
||||||
|
rc := NewRefreshCoordinator(cfg, GetSingletonNoOpLogger())
|
||||||
|
defer rc.Shutdown()
|
||||||
|
|
||||||
|
rc.recordRefreshAttempt("s")
|
||||||
|
rc.recordRefreshAttempt("s")
|
||||||
|
if !rc.isInCooldown("s") {
|
||||||
|
t.Fatal("precondition: session must enter cooldown")
|
||||||
|
}
|
||||||
|
time.Sleep(80 * time.Millisecond)
|
||||||
|
rc.cleanupStaleEntries()
|
||||||
|
if !rc.isInCooldown("s") {
|
||||||
|
t.Fatal("cleanup evicted a tracker still in cooldown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stale chunks above token_total must be dropped by key, even when the key
|
||||||
|
// set is sparse.
|
||||||
|
func TestChunkTrimDropsStaleSparseKeys(t *testing.T) {
|
||||||
|
cm := NewChunkManager(newNoOpLogger())
|
||||||
|
mk := func(s string, total interface{}) *sessions.Session {
|
||||||
|
v := map[interface{}]interface{}{"token_chunk": s}
|
||||||
|
if total != nil {
|
||||||
|
v["token_total"] = total
|
||||||
|
}
|
||||||
|
return &sessions.Session{Values: v}
|
||||||
|
}
|
||||||
|
chunks := map[int]*sessions.Session{
|
||||||
|
0: mk("Zq8xK2mP", 3), 1: mk("r7TnB4wY", nil), 2: mk("c9LdG5sH", nil), 7: mk("stale", nil),
|
||||||
|
}
|
||||||
|
res := cm.processChunkedToken(chunks, RefreshTokenConfig)
|
||||||
|
if res.Error != nil || res.Token != "Zq8xK2mPr7TnB4wYc9LdG5sH" {
|
||||||
|
t.Fatalf("token=%q err=%v, want Zq8xK2mPr7TnB4wYc9LdG5sH", res.Token, res.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -823,13 +823,16 @@ func (rc *RefreshCoordinator) cleanupRoutine() {
|
|||||||
// cleanupStaleEntries removes outdated tracking entries. Lock-free iteration
|
// cleanupStaleEntries removes outdated tracking entries. Lock-free iteration
|
||||||
// via sync.Map.Range; safe to race with concurrent reads/writes.
|
// via sync.Map.Range; safe to race with concurrent reads/writes.
|
||||||
func (rc *RefreshCoordinator) cleanupStaleEntries() {
|
func (rc *RefreshCoordinator) cleanupStaleEntries() {
|
||||||
cutoff := time.Now().Add(-2 * rc.config.RefreshAttemptWindow).UnixNano()
|
now := time.Now()
|
||||||
|
cutoff := now.Add(-2 * rc.config.RefreshAttemptWindow).UnixNano()
|
||||||
|
nowNano := now.UnixNano()
|
||||||
rc.sessionRefreshAttempts.Range(func(key, value interface{}) bool {
|
rc.sessionRefreshAttempts.Range(func(key, value interface{}) bool {
|
||||||
tracker := trackerFromMapValue(value)
|
tracker := trackerFromMapValue(value)
|
||||||
if tracker == nil {
|
if tracker == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if tracker.stateOf().lastAttemptNano < cutoff {
|
st := tracker.stateOf()
|
||||||
|
if st.lastAttemptNano < cutoff && st.cooldownEndNano <= nowNano {
|
||||||
// Compare-and-delete to avoid evicting a tracker that was just
|
// Compare-and-delete to avoid evicting a tracker that was just
|
||||||
// re-used by a concurrent caller. We compare by pointer identity.
|
// re-used by a concurrent caller. We compare by pointer identity.
|
||||||
rc.sessionRefreshAttempts.CompareAndDelete(key, value)
|
rc.sessionRefreshAttempts.CompareAndDelete(key, value)
|
||||||
|
|||||||
@@ -445,9 +445,10 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
|
|||||||
// not clear cookies above n). token_total is the authoritative
|
// not clear cookies above n). token_total is the authoritative
|
||||||
// count; drop the excess rather than failing — which forced a
|
// count; drop the excess rather than failing — which forced a
|
||||||
// re-auth and discarded the still-valid new token (R129).
|
// re-auth and discarded the still-valid new token (R129).
|
||||||
n := len(chunks)
|
for i := range chunks {
|
||||||
for i := total; i < n; i++ {
|
if i >= total {
|
||||||
delete(chunks, i)
|
delete(chunks, i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(chunks) < total {
|
if len(chunks) < total {
|
||||||
|
|||||||
Reference in New Issue
Block a user