feat(store): ListAllUsers for scheduler user enumeration
The in-process scheduler (ADR-018) needs to enumerate every user to run a discovery pass each. user_identities is the un-RLS'd map; add ListAllUsers as a plain pool query (no withUser) — the same enumerate-then-act pattern UserBySubject and the login_events gate query established. Scoping it to a single user would defeat the point; user_identities carries no RLS by design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,41 @@ func (s *Store) UserBySubject(ctx context.Context, subject string) (userID strin
|
|||||||
return userID, true, nil
|
return userID, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserIdentity is one (userID, dexSubject) pair from the un-RLS'd
|
||||||
|
// user_identities map — the unit the scheduler enumerates to run a discovery
|
||||||
|
// pass per user (ADR-018).
|
||||||
|
type UserIdentity struct {
|
||||||
|
UserID string
|
||||||
|
DexSubject string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllUsers returns every (userID, dexSubject) pair from user_identities. It
|
||||||
|
// runs as a plain pool query WITHOUT withUser — intentional and legitimate:
|
||||||
|
// user_identities is un-RLS'd auth plumbing (like UserBySubject), and the
|
||||||
|
// scheduler enumerating all users to run their discovery passes is an admin
|
||||||
|
// operation that cannot be scoped to any single user. Order is unspecified.
|
||||||
|
func (s *Store) ListAllUsers(ctx context.Context) ([]UserIdentity, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT user_id, dex_subject FROM user_identities`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("store: list all users: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var users []UserIdentity
|
||||||
|
for rows.Next() {
|
||||||
|
var u UserIdentity
|
||||||
|
if err := rows.Scan(&u.UserID, &u.DexSubject); err != nil {
|
||||||
|
return nil, fmt.Errorf("store: scan user identity: %w", err)
|
||||||
|
}
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("store: iterate user identities: %w", err)
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterUser creates the tapir user for a Dex subject and the identity mapping
|
// RegisterUser creates the tapir user for a Dex subject and the identity mapping
|
||||||
// that points to it, returning the new user_id. It errors with
|
// that points to it, returning the new user_id. It errors with
|
||||||
// ErrSubjectRegistered if the subject already maps.
|
// ErrSubjectRegistered if the subject already maps.
|
||||||
|
|||||||
@@ -85,6 +85,39 @@ func TestRegisterUserRejectsDuplicateSubject(t *testing.T) {
|
|||||||
require.Equal(t, first, got)
|
require.Equal(t, first, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListAllUsersReturnsEveryIdentity(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
s := newStore(t)
|
||||||
|
resetDB(t, rawPool(t))
|
||||||
|
|
||||||
|
empty, err := s.ListAllUsers(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, empty, "no registrations yet → empty slice")
|
||||||
|
|
||||||
|
const subjectC = "dex|carol-789"
|
||||||
|
idA, err := s.RegisterUser(ctx, subjectA, "Alice")
|
||||||
|
require.NoError(t, err)
|
||||||
|
idB, err := s.RegisterUser(ctx, subjectB, "Bob")
|
||||||
|
require.NoError(t, err)
|
||||||
|
idC, err := s.RegisterUser(ctx, subjectC, "Carol")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
users, err := s.ListAllUsers(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, users, 3)
|
||||||
|
|
||||||
|
// Order is unspecified; compare as a set of (userID, subject) pairs.
|
||||||
|
got := make(map[string]string, len(users))
|
||||||
|
for _, u := range users {
|
||||||
|
got[u.DexSubject] = u.UserID
|
||||||
|
}
|
||||||
|
require.Equal(t, map[string]string{
|
||||||
|
subjectA: idA,
|
||||||
|
subjectB: idB,
|
||||||
|
subjectC: idC,
|
||||||
|
}, got)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRegisterUserDistinctSubjectsGetDistinctUsers(t *testing.T) {
|
func TestRegisterUserDistinctSubjectsGetDistinctUsers(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
s := newStore(t)
|
s := newStore(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user