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.
Define a staff role taxonomy (for discussion — starting point):
support — contact messages, child-account review, applications/hiring.
(extend as needed.)
Normalize on a single canonical casing (lowercase) and stop listing case variants everywhere.
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.
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).
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.
## 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.
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 policiesAdminOnly/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).
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Our role/permission model is ad-hoc and not specialized to how SpikerSoft staff actually run the platform:
AdminandStaff(with case-duplicated variants), e.g. backendIsAdminOrStaff()=User.IsInRole("Admin"|"admin"|"Staff"|"staff")and frontend routes everywhere usingdata: { roles: ["Admin","admin","Staff","staff"] }. There is nodev-opsrole, and no way to say "this staffer can do X but not Y."/admin/*tool is open to anyone withStaff. There's no separation between, say, curriculum/content work, support, and infrastructure/ops.keycloak-adminlibrary is generic.libraries/keycloak-adminis 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 onlyAuthGuard-gated — not role-gated at all.RoleGuardroute data, componenthasAnyRole(...)calls, and backendIsInRole(...)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.
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.Normalize on a single canonical casing (lowercase) and stop listing case variants everywhere.
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.Specialize
keycloak-admin:/keycloak-adminroute itself toadmin(currently unprotected beyond auth).Centralize role constants (shared FE constant + matching backend named authorization policies) so
RoleGuard,hasAnyRole, and backend[Authorize(Policy=...)]all reference one source of truth. MigrateIsAdminOrStaff()and the duplicated route literals onto it incrementally.Acceptance criteria
dev-ops).keycloak-adminpresents SpikerSoft staff roles meaningfully and its route isadmin-gated.dev-opsis available so #297 can gate todev-ops/admin.Notes
dev-opsrole)./admin/*routes onto the permission map over time without a big-bang change.Related: #297, #287.
Backend foundation slice merged in spikersoft-backend PR #94 (additive + behavior-preserving — no endpoint's gating changed).
SpikerSoftRolesconstants (canonical lowercaseadmin/staff+dev-ops) with a central case-tolerant matcher — the "single canonical casing / stop listingAdmin|admin|Staff|staffvariants" ask. Scoped to roles actually in use +dev-ops; the debatablecontent/curriculum/supporttaxonomy is deliberately NOT defined (that's your call).AdminOnly/AdminOrStaff/DevOpsOrAdminregistered (prod + test), additive/unused — ready for incremental adoption.IsAdminOrStaff()bodies + ArtStudioIsStaff()+OpsAuthorization.IsDevOpsOrAdminnow delegate to the constants. Proven behavior-identical (equivalence matrix + all pre-existing authz tests green); 54 new tests. LeftIsAdmin()-only gates and Blog's lowercase-only local untouched (changing them would alter access).Still open for #298 (needs you / separate PRs):
content/curriculum/support) — "for discussion".dev-opsrealm-role creation (once created, #297's ops API flips from admin-only todev-ops/admin via the one-line// #298seam already in place).RoleGuard/hasAnyRolemigration,keycloak-adminroute admin-gating./admin/*routes onto a central permission map + adopt the named policies at endpoints.Ticket stays open.
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.