Skip to content

Commit f93af48

Browse files
committed
refactor(store): wave B.12 — user repo (#267)
UserRepo — the biggest single-domain migration. Covers the core user-projection reads: Get / GetByEmail / SessionInfo / Permissions / NextLinuxUID / List / Count / ListAllNonDeleted The narrow SessionInfo shape (Disabled / SessionVersion / IsDeleted) keeps the hot refresh-token validation path light. Permissions returns the flattened permission list combining direct role permissions + user-group inheritance. Call sites migrated (~70 across 21 files): - api: user_handler (8), auth_handler (3), sso_handler (2), totp_handler (3), settings_handler (2), system_actions (3) + test sites, terminal_handler, token_handler, identity_link_handler, assignment_handler, user_group_handler - scim: users, users_create, users_helpers, users_mutate, users_translation, handler (interface) - idp: linker, linker_test - control: admin_user Signatures shifted: - userToProto: db.UsersProjection -> store.User - userToSCIM: db.UsersProjection -> store.User - SystemActionsCleaner.CleanupDeletedUserActions: db.UsersProjection -> store.User (interface + tests + fake) - 6 system-action helpers (sync/cleanup user / ssh / tty) + systemTtyUserParams Field rename: LinuxUid (sqlc) -> LinuxUID (domain). SshPublicKeys stays as json.RawMessage at the boundary per the JSONB normalize plan in #242. Closes #267.
1 parent af78f98 commit f93af48

26 files changed

Lines changed: 323 additions & 92 deletions

cmd/control/admin_user.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020

2121
func ensureAdminUser(ctx context.Context, st *store.Store, email, password string, logger *slog.Logger) error {
2222
// Check if user exists via the projection
23-
_, err := st.Queries().GetUserByEmail(ctx, email)
23+
_, err := st.Repos().User.GetByEmail(ctx, email)
2424
if err == nil {
2525
logger.Info("admin user already exists", "email", email)
2626
return nil

internal/api/assignment_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func (h *AssignmentHandler) CreateAssignment(ctx context.Context, req *connect.R
9797
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to get device group")
9898
}
9999
case pm.AssignmentTargetType_ASSIGNMENT_TARGET_TYPE_USER:
100-
_, err := h.store.Queries().GetUserByID(ctx, req.Msg.TargetId)
100+
_, err := h.store.Repos().User.Get(ctx, req.Msg.TargetId)
101101
if err != nil {
102102
if store.IsNotFound(err) {
103103
return nil, apiErrorCtx(ctx, ErrUserNotFound, connect.CodeNotFound, "user not found")

internal/api/auth_handler.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[pm.LoginRe
5252
return nil, apiErrorCtx(ctx, ErrPasswordLoginDisabled, connect.CodeUnauthenticated, "password login is disabled on this server")
5353
}
5454

55-
user, err := h.store.Queries().GetUserByEmail(ctx, req.Msg.Email)
55+
user, err := h.store.Repos().User.GetByEmail(ctx, req.Msg.Email)
5656
if err != nil {
5757
if store.IsNotFound(err) {
5858
// Perform a dummy bcrypt comparison to prevent timing-based user enumeration
@@ -94,7 +94,7 @@ func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[pm.LoginRe
9494
}
9595

9696
// Resolve permissions from DB and embed in JWT
97-
permissions, err := h.store.Queries().GetUserPermissionsWithGroups(ctx, user.ID)
97+
permissions, err := h.store.Repos().User.Permissions(ctx, user.ID)
9898
if err != nil {
9999
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to resolve permissions")
100100
}
@@ -167,7 +167,7 @@ func (h *AuthHandler) RefreshToken(ctx context.Context, req *connect.Request[pm.
167167
}
168168

169169
// Check user status (disabled, deleted) and session version before issuing new tokens
170-
info, err := h.store.Queries().GetUserSessionInfo(ctx, result.Claims.UserID)
170+
info, err := h.store.Repos().User.SessionInfo(ctx, result.Claims.UserID)
171171
if err != nil {
172172
return nil, apiErrorCtx(ctx, ErrUserNotFound, connect.CodeUnauthenticated, "user not found")
173173
}
@@ -191,7 +191,7 @@ func (h *AuthHandler) RefreshToken(ctx context.Context, req *connect.Request[pm.
191191
}
192192

193193
// Resolve fresh permissions from DB
194-
permissions, err := h.store.Queries().GetUserPermissionsWithGroups(ctx, result.Claims.UserID)
194+
permissions, err := h.store.Repos().User.Permissions(ctx, result.Claims.UserID)
195195
if err != nil {
196196
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to resolve permissions")
197197
}
@@ -238,7 +238,7 @@ func (h *AuthHandler) GetCurrentUser(ctx context.Context, req *connect.Request[p
238238
return nil, err
239239
}
240240

241-
user, err := h.store.Queries().GetUserByID(ctx, userCtx.ID)
241+
user, err := h.store.Repos().User.Get(ctx, userCtx.ID)
242242
if err != nil {
243243
return nil, handleGetError(ctx, err, ErrUserNotFound, "user not found")
244244
}

internal/api/identity_link_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (h *IdentityLinkHandler) UnlinkIdentity(ctx context.Context, req *connect.R
7272
targetUserID := link.UserID
7373

7474
// Prevent unlinking last auth method
75-
user, err := h.store.Queries().GetUserByID(ctx, targetUserID)
75+
user, err := h.store.Repos().User.Get(ctx, targetUserID)
7676
if err != nil {
7777
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to get user")
7878
}

internal/api/settings_handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (h *SettingsHandler) UpdateServerSettings(ctx context.Context, req *connect
107107
// enableProvisioningForAllUsers sets user_provisioning_enabled=true on every user
108108
// that doesn't already have it enabled.
109109
func (h *SettingsHandler) enableProvisioningForAllUsers(ctx context.Context) error {
110-
users, err := h.store.Queries().ListAllNonDeletedUsers(ctx)
110+
users, err := h.store.Repos().User.ListAllNonDeleted(ctx)
111111
if err != nil {
112112
return fmt.Errorf("list users: %w", err)
113113
}
@@ -138,7 +138,7 @@ func (h *SettingsHandler) enableProvisioningForAllUsers(ctx context.Context) err
138138
// enableSshAccessForAllUsers sets ssh_access_enabled=true on every user
139139
// that doesn't already have it enabled.
140140
func (h *SettingsHandler) enableSshAccessForAllUsers(ctx context.Context) error {
141-
users, err := h.store.Queries().ListAllNonDeletedUsers(ctx)
141+
users, err := h.store.Repos().User.ListAllNonDeleted(ctx)
142142
if err != nil {
143143
return fmt.Errorf("list users: %w", err)
144144
}

internal/api/sso_handler.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (h *SSOHandler) ListAuthMethods(ctx context.Context, req *connect.Request[p
5353
// between "no such user" and "lookup errored" — that's a deliberate
5454
// info-leak guard, see the comment below.
5555
if req.Msg.Email != "" {
56-
user, err := h.store.Queries().GetUserByEmail(ctx, req.Msg.Email)
56+
user, err := h.store.Repos().User.GetByEmail(ctx, req.Msg.Email)
5757
if err == nil {
5858
resp.TotpEnabled = user.TotpEnabled
5959

@@ -274,7 +274,7 @@ func (h *SSOHandler) SSOCallback(ctx context.Context, req *connect.Request[pm.SS
274274
}
275275

276276
// Get and validate user before proceeding
277-
user, err := h.store.Queries().GetUserByID(ctx, linkResult.UserID)
277+
user, err := h.store.Repos().User.Get(ctx, linkResult.UserID)
278278
if err != nil {
279279
if store.IsNotFound(err) {
280280
h.logger.Error("SSO user not found after link resolved — user may be soft-deleted",
@@ -361,7 +361,7 @@ func (h *SSOHandler) SSOCallback(ctx context.Context, req *connect.Request[pm.SS
361361
}
362362

363363
// Generate tokens
364-
permissions, err := h.store.Queries().GetUserPermissionsWithGroups(ctx, user.ID)
364+
permissions, err := h.store.Repos().User.Permissions(ctx, user.ID)
365365
if err != nil {
366366
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to resolve permissions")
367367
}

internal/api/system_actions.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/manchtools/power-manage/server/internal/actionparams"
1313
"github.com/manchtools/power-manage/server/internal/ca"
1414
"github.com/manchtools/power-manage/server/internal/store"
15-
db "github.com/manchtools/power-manage/server/internal/store/generated"
1615
)
1716

1817
// SystemActionManager creates and maintains system-managed actions
@@ -54,7 +53,7 @@ func NewSystemActionManager(st *store.Store, signer ca.ActionSigner, logger *slo
5453
// SyncAllUsersSystemActions ensures all users have correct system actions.
5554
// Called at startup and after global settings changes. Idempotent.
5655
func (m *SystemActionManager) SyncAllUsersSystemActions(ctx context.Context) error {
57-
users, err := m.store.Queries().ListAllNonDeletedUsers(ctx)
56+
users, err := m.store.Repos().User.ListAllNonDeleted(ctx)
5857
if err != nil {
5958
return fmt.Errorf("list users: %w", err)
6059
}
@@ -145,7 +144,7 @@ func (m *SystemActionManager) StartReconciliation(ctx context.Context, interval,
145144
// Disabled users get a disabled flag in their USER action params.
146145
// Idempotent and safe to call after any user mutation.
147146
func (m *SystemActionManager) SyncUserSystemActions(ctx context.Context, userID string) error {
148-
user, err := m.store.Queries().GetUserByID(ctx, userID)
147+
user, err := m.store.Repos().User.Get(ctx, userID)
149148
if err != nil {
150149
return fmt.Errorf("get user: %w", err)
151150
}
@@ -199,7 +198,7 @@ func (m *SystemActionManager) SyncUserSystemActions(ctx context.Context, userID
199198
// If the permission query fails, we skip the TTY block entirely
200199
// and leave the existing state untouched — a transient DB error
201200
// must NOT trigger a cleanup that deletes a working TTY account.
202-
permissions, err := m.store.Queries().GetUserPermissionsWithGroups(ctx, userID)
201+
permissions, err := m.store.Repos().User.Permissions(ctx, userID)
203202
if err != nil {
204203
m.logger.Error("failed to resolve user permissions for tty action sync; leaving TTY state unchanged",
205204
"user_id", userID, "error", err)
@@ -227,7 +226,7 @@ func (m *SystemActionManager) SyncUserSystemActions(ctx context.Context, userID
227226

228227
// CleanupDeletedUserActions removes all system actions for a deleted user.
229228
// Must be called with the user projection loaded BEFORE the delete event.
230-
func (m *SystemActionManager) CleanupDeletedUserActions(ctx context.Context, user db.UsersProjection) error {
229+
func (m *SystemActionManager) CleanupDeletedUserActions(ctx context.Context, user store.User) error {
231230
if user.SystemUserActionID != "" {
232231
if err := m.actions.DeleteAction(ctx, user.SystemUserActionID); err != nil {
233232
m.logger.Error("failed to delete system user action", "action_id", user.SystemUserActionID, "error", err)
@@ -255,7 +254,7 @@ func (m *SystemActionManager) CleanupDeletedUserActions(ctx context.Context, use
255254
return nil
256255
}
257256

258-
func (m *SystemActionManager) syncUserProvisionAction(ctx context.Context, user db.UsersProjection, sshKeys []string) error {
257+
func (m *SystemActionManager) syncUserProvisionAction(ctx context.Context, user store.User, sshKeys []string) error {
259258
comment := user.DisplayName
260259
if comment == "" {
261260
comment = user.Email
@@ -268,7 +267,7 @@ func (m *SystemActionManager) syncUserProvisionAction(ctx context.Context, user
268267
// recur here.
269268
params := &pm.UserParams{
270269
Username: user.LinuxUsername,
271-
Uid: user.LinuxUid,
270+
Uid: user.LinuxUID,
272271
CreateHome: true,
273272
Comment: comment,
274273
Disabled: user.Disabled,
@@ -316,7 +315,7 @@ func (m *SystemActionManager) syncUserProvisionAction(ctx context.Context, user
316315
return nil
317316
}
318317

319-
func (m *SystemActionManager) syncSshAccessAction(ctx context.Context, user db.UsersProjection) error {
318+
func (m *SystemActionManager) syncSshAccessAction(ctx context.Context, user store.User) error {
320319
// Typed *pm.SshParams — same rationale as syncUserProvisionAction.
321320
params := &pm.SshParams{
322321
Users: []string{user.LinuxUsername},
@@ -363,7 +362,7 @@ func (m *SystemActionManager) syncSshAccessAction(ctx context.Context, user db.U
363362
return nil
364363
}
365364

366-
func (m *SystemActionManager) cleanupUserAction(ctx context.Context, user db.UsersProjection) error {
365+
func (m *SystemActionManager) cleanupUserAction(ctx context.Context, user store.User) error {
367366
if user.SystemUserActionID == "" {
368367
return nil
369368
}
@@ -377,7 +376,7 @@ func (m *SystemActionManager) cleanupUserAction(ctx context.Context, user db.Use
377376
return nil
378377
}
379378

380-
func (m *SystemActionManager) cleanupSshAction(ctx context.Context, user db.UsersProjection) error {
379+
func (m *SystemActionManager) cleanupSshAction(ctx context.Context, user store.User) error {
381380
if user.SystemSshActionID == "" {
382381
return nil
383382
}
@@ -402,7 +401,7 @@ func (m *SystemActionManager) cleanupSshAction(ctx context.Context, user db.User
402401
// The action uses nologin as the shell (the agent temporarily
403402
// activates it during a session), no home directory, and the
404403
// deterministic UID from the SDK's TTYUID helper.
405-
func (m *SystemActionManager) syncTtyUserAction(ctx context.Context, user db.UsersProjection) error {
404+
func (m *SystemActionManager) syncTtyUserAction(ctx context.Context, user store.User) error {
406405
// Typed *pm.UserParams so the Go compiler rejects field-name
407406
// typos. The previous map[string]any form accepted "system": true
408407
// as a sibling of real fields — protojson silently dropped it on
@@ -467,9 +466,9 @@ func (m *SystemActionManager) syncTtyUserAction(ctx context.Context, user db.Use
467466
return nil
468467
}
469468

470-
func systemTtyUserParams(user db.UsersProjection) *pm.UserParams {
469+
func systemTtyUserParams(user store.User) *pm.UserParams {
471470
ttyUsername := "pm-tty-" + user.LinuxUsername
472-
ttyUID := int32(int(user.LinuxUid) + 100000) // terminal.DefaultUIDOffset
471+
ttyUID := int32(int(user.LinuxUID) + 100000) // terminal.DefaultUIDOffset
473472

474473
return &pm.UserParams{
475474
Username: ttyUsername,
@@ -482,7 +481,7 @@ func systemTtyUserParams(user db.UsersProjection) *pm.UserParams {
482481
}
483482
}
484483

485-
func (m *SystemActionManager) cleanupTtyAction(ctx context.Context, user db.UsersProjection) error {
484+
func (m *SystemActionManager) cleanupTtyAction(ctx context.Context, user store.User) error {
486485
if user.SystemTtyActionID == "" {
487486
return nil
488487
}

internal/api/system_actions_manager_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestSyncUserSystemActions_NoLinuxUsername_NoOps(t *testing.T) {
7171

7272
require.NoError(t, m.SyncUserSystemActions(context.Background(), userID))
7373

74-
user, err := st.Queries().GetUserByID(context.Background(), userID)
74+
user, err := st.Repos().User.Get(context.Background(), userID)
7575
require.NoError(t, err)
7676
assert.Empty(t, user.SystemUserActionID, "no linux_username → manager must NOT have created a USER action")
7777
assert.Empty(t, user.SystemSshActionID, "no linux_username → no SSH action either")
@@ -90,7 +90,7 @@ func TestSyncUserSystemActions_ProvisioningDisabled_NoActionsCreated(t *testing.
9090

9191
require.NoError(t, m.SyncUserSystemActions(context.Background(), userID))
9292

93-
user, err := st.Queries().GetUserByID(context.Background(), userID)
93+
user, err := st.Repos().User.Get(context.Background(), userID)
9494
require.NoError(t, err)
9595
assert.Empty(t, user.SystemUserActionID,
9696
"provisioning disabled + no prior action → cleanup branch is a no-op (nothing to delete)")
@@ -108,7 +108,7 @@ func TestSyncUserSystemActions_GlobalProvisioning_CreatesUserAction(t *testing.T
108108

109109
require.NoError(t, m.SyncUserSystemActions(context.Background(), userID))
110110

111-
user, err := st.Queries().GetUserByID(context.Background(), userID)
111+
user, err := st.Repos().User.Get(context.Background(), userID)
112112
require.NoError(t, err)
113113
require.NotEmpty(t, user.SystemUserActionID,
114114
"provisioning enabled + linux_username → manager must create + link a system USER action")
@@ -131,7 +131,7 @@ func TestSyncUserSystemActions_SecondRun_UpdatesExistingAction(t *testing.T) {
131131
enableGlobalProvisioning(t, st)
132132

133133
require.NoError(t, m.SyncUserSystemActions(context.Background(), userID))
134-
user1, err := st.Queries().GetUserByID(context.Background(), userID)
134+
user1, err := st.Repos().User.Get(context.Background(), userID)
135135
require.NoError(t, err)
136136
originalActionID := user1.SystemUserActionID
137137
require.NotEmpty(t, originalActionID)
@@ -140,7 +140,7 @@ func TestSyncUserSystemActions_SecondRun_UpdatesExistingAction(t *testing.T) {
140140
// branch (not the create branch) so the action ID stays stable —
141141
// otherwise downstream assignments would dangle.
142142
require.NoError(t, m.SyncUserSystemActions(context.Background(), userID))
143-
user2, err := st.Queries().GetUserByID(context.Background(), userID)
143+
user2, err := st.Repos().User.Get(context.Background(), userID)
144144
require.NoError(t, err)
145145
assert.Equal(t, originalActionID, user2.SystemUserActionID,
146146
"second sync MUST keep the same action ID — re-creating would orphan the assignment row")
@@ -157,7 +157,7 @@ func TestCleanupDeletedUserActions_RemovesAllSystemActions(t *testing.T) {
157157
enableGlobalProvisioning(t, st)
158158

159159
require.NoError(t, m.SyncUserSystemActions(context.Background(), userID))
160-
user, err := st.Queries().GetUserByID(context.Background(), userID)
160+
user, err := st.Repos().User.Get(context.Background(), userID)
161161
require.NoError(t, err)
162162
require.NotEmpty(t, user.SystemUserActionID, "precondition: user has a system action")
163163

@@ -167,7 +167,7 @@ func TestCleanupDeletedUserActions_RemovesAllSystemActions(t *testing.T) {
167167

168168
// User's link columns are now empty — confirms the cleanup
169169
// emitted UserSystemActionLinked with action_id="".
170-
after, err := st.Queries().GetUserByID(context.Background(), userID)
170+
after, err := st.Repos().User.Get(context.Background(), userID)
171171
require.NoError(t, err)
172172
assert.Empty(t, after.SystemUserActionID, "cleanup MUST clear system_user_action_id")
173173
}
@@ -184,7 +184,7 @@ func TestSyncUserSystemActions_DisablingProvisioningCleansUpExistingAction(t *te
184184

185185
// First sync creates the action.
186186
require.NoError(t, m.SyncUserSystemActions(context.Background(), userID))
187-
user, err := st.Queries().GetUserByID(context.Background(), userID)
187+
user, err := st.Repos().User.Get(context.Background(), userID)
188188
require.NoError(t, err)
189189
require.NotEmpty(t, user.SystemUserActionID)
190190

@@ -199,7 +199,7 @@ func TestSyncUserSystemActions_DisablingProvisioningCleansUpExistingAction(t *te
199199

200200
// Second sync must take the cleanup branch.
201201
require.NoError(t, m.SyncUserSystemActions(context.Background(), userID))
202-
after, err := st.Queries().GetUserByID(context.Background(), userID)
202+
after, err := st.Repos().User.Get(context.Background(), userID)
203203
require.NoError(t, err)
204204
assert.Empty(t, after.SystemUserActionID,
205205
"provisioning flipped off → cleanup branch must clear the prior link")

internal/api/system_actions_params_canary_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
pm "github.com/manchtools/power-manage/sdk/gen/go/pm/v1"
1111
"github.com/manchtools/power-manage/server/internal/actionparams"
12-
db "github.com/manchtools/power-manage/server/internal/store/generated"
12+
"github.com/manchtools/power-manage/server/internal/store"
1313
)
1414

1515
// Tests in this file lock down the "system-managed actions must
@@ -153,9 +153,9 @@ func TestMarshalActionParamsRejectsNil(t *testing.T) {
153153
}
154154

155155
func TestSystemTtyUserActionParamsOutput(t *testing.T) {
156-
params, err := serializeProtoParams(systemTtyUserParams(db.UsersProjection{
156+
params, err := serializeProtoParams(systemTtyUserParams(store.User{
157157
LinuxUsername: "alice",
158-
LinuxUid: 1000,
158+
LinuxUID: 1000,
159159
Disabled: false,
160160
}))
161161
if err != nil {

internal/api/terminal_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func (h *TerminalHandler) StartTerminal(ctx context.Context, req *connect.Reques
8686
return nil, apiErrorCtx(ctx, ErrNotAuthenticated, connect.CodeUnauthenticated, "not authenticated")
8787
}
8888

89-
user, err := h.store.Queries().GetUserByID(ctx, userCtx.ID)
89+
user, err := h.store.Repos().User.Get(ctx, userCtx.ID)
9090
if err != nil {
9191
if store.IsNotFound(err) {
9292
return nil, apiErrorCtx(ctx, ErrUserNotFound, connect.CodeNotFound, "user not found")

0 commit comments

Comments
 (0)