Specialize keycloak-admin library toward SpikerSoft staff roles & permission delineation #298

Open
opened 2026-06-30 16:42:32 +00:00 by spikerj · 2 comments
Owner

Problem

Our role/permission model is ad-hoc and not specialized to how SpikerSoft staff actually run the platform:

  • No formal role taxonomy. In practice the only roles used are Admin and Staff (with case-duplicated variants), e.g. backend IsAdminOrStaff() = User.IsInRole("Admin"|"admin"|"Staff"|"staff") and frontend routes everywhere using data: { roles: ["Admin","admin","Staff","staff"] }. There is no dev-ops role, and no way to say "this staffer can do X but not Y."
  • Coarse gating. Nearly every /admin/* tool is open to anyone with Staff. There's no separation between, say, curriculum/content work, support, and infrastructure/ops.
  • The keycloak-admin library is generic. libraries/keycloak-admin is a thin wrapper over the Keycloak Admin API (user-list, user-detail, user-roles, roles-list, realm-settings, sessions, credentials, etc.). It manages Keycloak primitives but encodes none of SpikerSoft's intended staff structure or permission semantics. Its route (/keycloak-admin) is currently only AuthGuard-gated — not role-gated at all.
  • Scattered role logic. Role strings are duplicated as literals across RoleGuard route data, component hasAnyRole(...) calls, and backend IsInRole(...) checks, with case-sensitivity worked around by listing both casings.

Proposal

Start specializing toward SpikerSoft's staff roles and delineate the permissions each role needs to operate the platform.

  1. Define a staff role taxonomy (for discussion — starting point):

    • admin — full platform control.
    • dev-ops — infrastructure/ops surfaces (host incidents & security console per #297, deploys/provisioning, health).
    • content / curriculum — lessons, books, facts, geography, skills, blog moderation.
    • support — contact messages, child-account review, applications/hiring.
    • (extend as needed.)
      Normalize on a single canonical casing (lowercase) and stop listing case variants everywhere.
  2. Permission map. Define which roles may access which staff surfaces (the existing /admin/* tools + the new ops console), so access is driven by a central mapping rather than per-route literals.

  3. Specialize keycloak-admin:

    • Treat the SpikerSoft staff roles as first-class (curated create/assign flows, descriptions of what each role grants) rather than raw Keycloak realm-role CRUD.
    • Provide a staff-oriented view: which staff have which role, and what each role can do.
    • Gate the /keycloak-admin route itself to admin (currently unprotected beyond auth).
  4. Centralize role constants (shared FE constant + matching backend named authorization policies) so RoleGuard, hasAnyRole, and backend [Authorize(Policy=...)] all reference one source of truth. Migrate IsAdminOrStaff() and the duplicated route literals onto it incrementally.

Acceptance criteria

  • Agreed staff role taxonomy documented, created in Keycloak (incl. dev-ops).
  • Single canonical role casing; shared role constants on FE (+ named policies on BE).
  • keycloak-admin presents SpikerSoft staff roles meaningfully and its route is admin-gated.
  • A permission map governs which roles reach which staff surfaces.
  • dev-ops is available so #297 can gate to dev-ops/admin.

Notes

  • This is the dependency for #297 (the ops console needs a real dev-ops role).
  • Migration can be incremental — introduce the taxonomy + constants first, then move existing /admin/* routes onto the permission map over time without a big-bang change.

Related: #297, #287.

## Problem Our role/permission model is ad-hoc and not specialized to how SpikerSoft staff actually run the platform: - **No formal role taxonomy.** In practice the only roles used are `Admin` and `Staff` (with case-duplicated variants), e.g. backend `IsAdminOrStaff()` = `User.IsInRole("Admin"|"admin"|"Staff"|"staff")` and frontend routes everywhere using `data: { roles: ["Admin","admin","Staff","staff"] }`. There is **no `dev-ops` role**, and no way to say "this staffer can do X but not Y." - **Coarse gating.** Nearly every `/admin/*` tool is open to anyone with `Staff`. There's no separation between, say, curriculum/content work, support, and infrastructure/ops. - **The `keycloak-admin` library is generic.** `libraries/keycloak-admin` is a thin wrapper over the Keycloak Admin API (user-list, user-detail, user-roles, roles-list, realm-settings, sessions, credentials, etc.). It manages Keycloak primitives but encodes **none** of SpikerSoft's intended staff structure or permission semantics. Its route (`/keycloak-admin`) is currently only `AuthGuard`-gated — **not role-gated at all**. - **Scattered role logic.** Role strings are duplicated as literals across `RoleGuard` route data, component `hasAnyRole(...)` calls, and backend `IsInRole(...)` checks, with case-sensitivity worked around by listing both casings. ## Proposal Start specializing toward SpikerSoft's staff roles and delineate the permissions each role needs to operate the platform. 1. **Define a staff role taxonomy** (for discussion — starting point): - `admin` — full platform control. - `dev-ops` — infrastructure/ops surfaces (host incidents & security console per #297, deploys/provisioning, health). - `content` / `curriculum` — lessons, books, facts, geography, skills, blog moderation. - `support` — contact messages, child-account review, applications/hiring. - (extend as needed.) Normalize on a **single canonical casing** (lowercase) and stop listing case variants everywhere. 2. **Permission map.** Define which roles may access which staff surfaces (the existing `/admin/*` tools + the new ops console), so access is driven by a central mapping rather than per-route literals. 3. **Specialize `keycloak-admin`:** - Treat the SpikerSoft staff roles as first-class (curated create/assign flows, descriptions of what each role grants) rather than raw Keycloak realm-role CRUD. - Provide a staff-oriented view: which staff have which role, and what each role can do. - **Gate the `/keycloak-admin` route itself to `admin`** (currently unprotected beyond auth). 4. **Centralize role constants** (shared FE constant + matching backend named authorization policies) so `RoleGuard`, `hasAnyRole`, and backend `[Authorize(Policy=...)]` all reference one source of truth. Migrate `IsAdminOrStaff()` and the duplicated route literals onto it incrementally. ## Acceptance criteria - [ ] Agreed staff role taxonomy documented, created in Keycloak (incl. `dev-ops`). - [ ] Single canonical role casing; shared role constants on FE (+ named policies on BE). - [ ] `keycloak-admin` presents SpikerSoft staff roles meaningfully and its route is `admin`-gated. - [ ] A permission map governs which roles reach which staff surfaces. - [ ] `dev-ops` is available so #297 can gate to `dev-ops`/`admin`. ## Notes - This is the **dependency** for #297 (the ops console needs a real `dev-ops` role). - Migration can be incremental — introduce the taxonomy + constants first, then move existing `/admin/*` routes onto the permission map over time without a big-bang change. Related: #297, #287.
spikerj added the enhancement label 2026-06-30 16:42:32 +00:00
Author
Owner

Backend foundation slice merged in spikersoft-backend PR #94 (additive + behavior-preserving — no endpoint's gating changed).

  • SpikerSoftRoles constants (canonical lowercase admin/staff + dev-ops) with a central case-tolerant matcher — the "single canonical casing / stop listing Admin|admin|Staff|staff variants" ask. Scoped to roles actually in use + dev-ops; the debatable content/curriculum/support taxonomy is deliberately NOT defined (that's your call).
  • Named policies AdminOnly/AdminOrStaff/DevOpsOrAdmin registered (prod + test), additive/unused — ready for incremental adoption.
  • The 10 duplicated inline IsAdminOrStaff() bodies + ArtStudio IsStaff() + OpsAuthorization.IsDevOpsOrAdmin now delegate to the constants. Proven behavior-identical (equivalence matrix + all pre-existing authz tests green); 54 new tests. Left IsAdmin()-only gates and Blog's lowercase-only local untouched (changing them would alter access).

Still open for #298 (needs you / separate PRs):

  • Role-taxonomy decision (content/curriculum/support) — "for discussion".
  • Keycloak dev-ops realm-role creation (once created, #297's ops API flips from admin-only to dev-ops/admin via the one-line // #298 seam already in place).
  • Angular: shared role constants, RoleGuard/hasAnyRole migration, keycloak-admin route admin-gating.
  • Migrate /admin/* routes onto a central permission map + adopt the named policies at endpoints.

Ticket stays open.

**Backend foundation slice merged** in spikersoft-backend PR #94 (additive + behavior-preserving — no endpoint's gating changed). - **`SpikerSoftRoles`** constants (canonical lowercase `admin`/`staff` + `dev-ops`) with a central case-tolerant matcher — the "single canonical casing / stop listing `Admin|admin|Staff|staff` variants" ask. Scoped to roles actually in use + `dev-ops`; the debatable `content`/`curriculum`/`support` taxonomy is deliberately NOT defined (that's your call). - **Named policies** `AdminOnly`/`AdminOrStaff`/`DevOpsOrAdmin` registered (prod + test), additive/unused — ready for incremental adoption. - The 10 duplicated inline `IsAdminOrStaff()` bodies + ArtStudio `IsStaff()` + `OpsAuthorization.IsDevOpsOrAdmin` now delegate to the constants. Proven behavior-identical (equivalence matrix + all pre-existing authz tests green); 54 new tests. Left `IsAdmin()`-only gates and Blog's lowercase-only local untouched (changing them would alter access). **Still open for #298 (needs you / separate PRs):** - **Role-taxonomy decision** (`content`/`curriculum`/`support`) — "for discussion". - **Keycloak `dev-ops` realm-role creation** (once created, #297's ops API flips from admin-only to `dev-ops`/admin via the one-line `// #298` seam already in place). - **Angular**: shared role constants, `RoleGuard`/`hasAnyRole` migration, `keycloak-admin` route admin-gating. - Migrate `/admin/*` routes onto a central permission map + adopt the named policies at endpoints. Ticket stays open.
Author
Owner

Board-sweep status (2026-07-22): backend foundation merged (PR #94 — SpikerSoftRoles constants + named policies). REMAINING: role-taxonomy decision (owner call), dev-ops rollout, frontend gating.

Board-sweep status (2026-07-22): backend foundation merged (PR #94 — SpikerSoftRoles constants + named policies). REMAINING: role-taxonomy decision (owner call), dev-ops rollout, frontend gating.
Sign in to join this conversation.