From 850a9d4cc42dc2398d3d8cb4a5ff0bc8cb92035c Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 22 Feb 2026 20:28:37 -0500 Subject: [PATCH] Fix bug with provisioned token removal --- user/manager.go | 2 +- user/manager_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ user/store.go | 12 +++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/user/manager.go b/user/manager.go index 1e1b27f8..534ff86d 100644 --- a/user/manager.go +++ b/user/manager.go @@ -763,7 +763,7 @@ func (a *Manager) maybeProvisionTokens(provisionUsernames []string) error { } for _, existingToken := range existingTokens { if !slices.Contains(provisionTokens, existingToken.Value) { - if err := a.store.RemoveToken("", existingToken.Value); err != nil { + if err := a.store.RemoveProvisionedToken(existingToken.Value); err != nil { return fmt.Errorf("failed to remove provisioned token %s: %v", existingToken.Value, err) } } diff --git a/user/manager_test.go b/user/manager_test.go index d903332f..b1858ad4 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -1313,6 +1313,53 @@ func TestManager_WithProvisionedUsers(t *testing.T) { }) } +func TestManager_WithProvisionedUsers_RemoveToken(t *testing.T) { + forEachBackend(t, func(t *testing.T, newStore newStoreFunc) { + conf := &Config{ + DefaultAccess: PermissionReadWrite, + ProvisionEnabled: true, + Users: []*User{ + {Name: "phil", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, + }, + Tokens: map[string][]*Token{ + "phil": { + {Value: "tk_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Label: "Token A"}, + {Value: "tk_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Label: "Token B"}, + }, + }, + } + a := newTestManagerFromStoreConfig(t, newStore, conf) + + users, err := a.Users() + require.Nil(t, err) + philUserID := "" + for _, u := range users { + if u.Name == "phil" { + philUserID = u.ID + } + } + require.NotEmpty(t, philUserID) + + tokens, err := a.Tokens(philUserID) + require.Nil(t, err) + require.Equal(t, 2, len(tokens)) + + // Re-open the DB: user stays, but Token B is removed from config + require.Nil(t, a.Close()) + conf.Tokens = map[string][]*Token{ + "phil": { + {Value: "tk_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Label: "Token A"}, + }, + } + a = newTestManagerFromStoreConfig(t, newStore, conf) + + tokens, err = a.Tokens(philUserID) + require.Nil(t, err) + require.Equal(t, 1, len(tokens)) + require.Equal(t, "tk_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", tokens[0].Value) + }) +} + func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing.T) { forEachBackend(t, func(t *testing.T, newStore newStoreFunc) { conf := &Config{ diff --git a/user/store.go b/user/store.go index 8b958c3c..53598b14 100644 --- a/user/store.go +++ b/user/store.go @@ -44,6 +44,7 @@ type Store interface { ChangeTokenExpiry(userID, token string, expires time.Time) error UpdateTokenLastAccess(token string, lastAccess time.Time, lastOrigin netip.Addr) error RemoveToken(userID, token string) error + RemoveProvisionedToken(token string) error RemoveExpiredTokens() error TokenCount(userID string) (int, error) RemoveExcessTokens(userID string, maxCount int) error @@ -538,6 +539,17 @@ func (s *commonStore) RemoveToken(userID, token string) error { return nil } +// RemoveProvisionedToken deletes a provisioned token by value, regardless of user +func (s *commonStore) RemoveProvisionedToken(token string) error { + if token == "" { + return errNoTokenProvided + } + if _, err := s.db.Exec(s.queries.deleteProvisionedToken, token); err != nil { + return err + } + return nil +} + // RemoveExpiredTokens deletes all expired tokens from the database func (s *commonStore) RemoveExpiredTokens() error { if _, err := s.db.Exec(s.queries.deleteExpiredTokens, time.Now().Unix()); err != nil {