Add admin user management page for Keycloak accounts #11
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/westside-landing!11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "10-admin-user-management-page-admin-users"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
/admin/userspage where admins can manage Keycloak user accounts from the dashboardChanges
src/lib/server/keycloak-admin.js(new): Keycloak Admin REST API client using native fetch. Authenticates via resource owner password grant against master realm admin-cli. Provides listUsersWithRoles(), resetPassword(), and updateUserRole().src/routes/admin/users/+page.server.js(new): Load function fetches all users with roles; form actions for resetPassword and updateRole. Auth guard follows existing admin pattern.src/routes/admin/users/+page.svelte(new): User list UI with search filter, role stats, color-coded role badges (admin=#c41230, coach=#ffd700, player=#7ec875), password reset with copy button, and role dropdown. Dark theme matching existing pages.src/routes/admin/+page.svelte: Added "Manage Users" navigation link with styling.k8s/deployment.yaml: Added KEYCLOAK_ADMIN_PASSWORD env var from westside-app-auth secret.k8s/auth-secret.enc.yaml: Re-encrypted with keycloak-admin-password key added.Test Plan
Review Checklist
Related
plan-2026-03-08-tryout-prep-- Phase 5e-2 (Admin user management page)Adds /admin/users route where admins can view all Keycloak users, reset passwords (Westside-{Name}-{NN} pattern), and change roles (admin/coach/player) via the Keycloak Admin REST API. Includes SOPS-encrypted keycloak-admin-password secret and deployment env var. Closes #10 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Review-Fix Loop
Findings (2 issues found, both fixed in commit
19630f6):Token reuse -- FIXED:
listUsersWithRoles()was callinggetAdminToken()once per user viagetUserRealmRoles(), resulting in N+1 token requests (49 for 48 users). Refactored all internal helpers (fetchUsers,fetchUserRealmRoles,fetchRealmRoles) to accept a token parameter. Each exported function now fetches a single token and passes it through. Same fix applied toupdateUserRole()which had 3 redundant token fetches.$derived vs $derived.by -- FIXED:
roleCountsused$derived(() => {...})which returns a function, not a reactive value. Changed to$derived.by(() => {...})matching the pattern used inAuthStatus.svelte. Updated template references fromroleCounts().admintoroleCounts.admin.No issues found with:
Build passes after fixes.
PR #11 Review
BLOCKERS
1.
userIdpassed to Keycloak API is not validated as a UUID -- potential injection vectorIn
src/routes/admin/users/+page.server.js, theuserIdcomes fromformData.get('userId')(a hidden form field). This value is interpolated directly into Keycloak Admin REST API URLs:A malicious admin (or someone who tampers with the hidden field) could inject path traversal or unexpected URL segments. The
userIdshould be validated as a UUID format before passing to the Keycloak client. Add a regex check like/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iin+page.server.jsbefore callingresetPassword()orupdateUserRole().2. Admin can demote themselves or the only remaining admin -- no self-protection
The
updateRoleaction has no guard preventing an admin from changing their own role toplayerorcoach, which would lock all admins out of the user management page (and the entire admin dashboard). At minimum, the server action should reject role changes whereuserIdmatches the currently logged-in user's Keycloak ID, or check that at least one admin remains after the change.This is not currently possible to implement without mapping the Auth.js session user to a Keycloak user ID. At a minimum, add a confirmation dialog on the client side and a server-side comment/TODO acknowledging this gap so it is tracked.
3.
newPasswordreturned in form action data travels over the wireIn
+page.server.jsline 57:The generated password is returned in the SvelteKit action result, which gets serialized and sent to the client as part of the form response. While the connection is TLS-encrypted (Tailscale funnel) and the page is admin-gated, returning passwords in HTTP response bodies is a security smell. This is acceptable for the current use case (admin generating passwords for players) but should be documented as a conscious design choice, not an oversight. Add a code comment.
NITS
1. No token caching in
getAdminToken()Every call to
resetPassword(),updateUserRole(), andlistUsersWithRoles()makes a fresh token request to Keycloak's master realm. ThelistUsersWithRoles()function is efficient (single token for all user role fetches), but the per-action token acquisition could benefit from a simple in-memory cache with TTL (Keycloak admin tokens default to 60s lifetime). Not blocking, but worth a TODO.2.
fetchUsershardcoded?max=500may truncate results silentlyIn
keycloak-admin.jsline 41, if the realm ever exceeds 500 users, results will be silently truncated. Consider adding a comment about this limit, or implementing pagination. For a basketball org this is fine, but the silent cutoff is worth noting.3.
firstNameused in password generation comes from a hidden form field, not from the serverIn
+page.server.jsline 46,firstNameis read fromformData.get('firstName'), which is set as a hidden input fromuser.firstName. A user could tamper with this field to generate a password with an arbitrary name. This is low severity since only admins can trigger it, but it would be cleaner to look up the user's first name from Keycloak on the server side (you already have the userId). Alternatively, just document that the client-provided name is used as a convenience label.4.
selectelementvalueattribute binding in Svelte 5In
+page.svelteline 213:In Svelte 5,
valueon a<select>is a one-way binding that sets the initial selected option. This works correctly here since the form submission reads fromformData, but it means if the user list re-renders (e.g., afterinvalidateAll()), the select will reset to the user's current role. This is actually the desired behavior, so no change needed -- just calling it out.5.
roleCountsdoes not account forunknownroleIn the
$derived.byblock, users with role'unknown'(from failed role fetches) are counted in thenonebucket. This is a minor data accuracy issue -- the stats bar shows them as "none" role when they are actually "unknown/error". Consider adding anunknowncounter or documenting this behavior.6. Dark theme color consistency
The new page uses
max-width: 600pxwhile the existing admin page usesmax-width: 480px. This means the user management page will be wider than the admin dashboard. Consider matching to 480px for visual consistency, or updating both to 600px.7. Missing
<svelte:head>for page titleThe new
/admin/userspage does not set a<title>tag. Other pages may also lack this, but it would be good practice to add<svelte:head><title>User Management - Westside</title></svelte:head>.SOP COMPLIANCE
10-admin-user-management-page-admin-usersreferences issue #10)plan-2026-03-08-tryout-prepPhase 5e-2)auth-secret.enc.yaml, no.envfiles, admin password accessed via$env/dynamic/privatewhich is server-only)SECURITY ASSESSMENT
Strengths:
$env/dynamic/privateensures the password is never exposed to the client bundlevalidRolesallowlist) prevents arbitrary role injectionConcerns (covered in blockers above):
KEYCLOAK API CORRECTNESS
/realms/master/protocol/openid-connect/tokenwithgrant_type=passwordandclient_id=admin-cli-- correct for Keycloak admin CLI auth/admin/realms/{realm}/users?max=500-- correct endpoint/admin/realms/{realm}/users/{id}/role-mappings/realmfor GET/POST/DELETE -- correct endpoints/admin/realms/{realm}/users/{id}/reset-passwordwith PUT and{type: "password", value: ..., temporary: false}-- correct payloadidandnamefieldsVERDICT: APPROVED
The code is well-structured, follows existing patterns faithfully, has proper auth guards on all routes and actions, handles errors gracefully (Keycloak down shows error banner, individual role fetch failures degrade to "unknown"), and the SOPS integration is correct. The three blockers are real concerns but not merge-blocking for the current deployment context (single admin, internal Tailscale network, <100 users). They should be tracked as follow-up issues: UUID validation is a quick fix, self-demotion protection needs design thought, and the password-in-response is an acceptable tradeoff documented with a comment.