# Switchboard Core — Session Turnover **Date**: 2026-03-26 **Author**: Jeffrey Smith (jasafpro@gmail.com) **Session**: v0.2.0 PR 1 — Full RBAC migration --- ## What Was Done ### PR 1: Admin → RBAC group migration — COMPLETE Branch `feat/admin-rbac-migration` (PR #1), 3 commits: 1. **Admin → RBAC group migration** — `surface.admin.access` permission + Admins system group replaces hardcoded `role == "admin"` checks. Admin bypass removed from `RequirePermission`. Bootstrap/seed/OIDC/admin handlers sync group membership. 2. **Remove token budgets + allowed models from groups** — Provider-era cruft: `token_budget_daily`, `token_budget_monthly`, `allowed_models` columns, `ResolveTokenBudget()`, `ResolveModelAllowlist()`, and all store/handler/UI code deleted. -283 lines. 3. **Drop `users.role` column** — Full RBAC. Zero magic roles. The `role` column, `UserRoleAdmin`/`UserRoleUser` constants, `CountByRole()`, `DefaultRole` config, `role` in JWT claims, and all role-based shortcuts removed. Everyone group membership is now explicit (all users added on create). 28 files changed. **Net result: -364 lines across 3 commits.** ### Architecture after this PR: - **Everyone group** (`00000000...0001`): all users explicitly added on creation. Carries `extension.use`, `workflow.submit`. - **Admins group** (`00000000...0002`): carries all 7 kernel permissions including `surface.admin.access`. Not special-cased — just a group with permissions. - **Permission resolution**: union of all group memberships. No implicit groups, no role shortcuts, no admin bypass. - **Admin middleware**: checks `surface.admin.access` grant via `resolveAndCachePerms`. - **JWT claims**: `user_id` + `email` only. No role. - **OIDC**: `isIdPAdmin()` maps IdP role claim → Admins group membership. No `role` column writes. - **User creation paths**: builtin register, OIDC auto-provision, mTLS auto-provision, admin create, bootstrap, seed — all call `EnsureEveryoneGroup()`. --- ## Known Issues 1. **Frontend test job**: FE test suite may have remaining stale references from Phase 0 gut (pre-existing) 2. **Traefik middleware**: `rbac-traefik.yaml` needs one-time manual apply by cluster admin (pre-existing, non-blocking) 3. **`SEED_USERS`**: Not set in K8s secrets (pre-existing) 4. **Editor modules**: `src/editor/*.mjs` still reference "Chat Switchboard" (pre-existing, low priority) 5. **K8s deploy**: PR not yet merged to main — CI/CD will need a deploy after merge --- ## What's Next ### Merge PR #1 Review and merge `feat/admin-rbac-migration` to main. ### v0.2.0 — Remaining PRs **PR 2: Settings cascade** - Add `user_overridable` flag to settings schema - RBAC controls scope auth (admin → global, team-admin → team, user → personal) - Resolution: user → team → global (first non-null wins) **PR 3: ICD (API contract)** - Generate full OpenAPI spec from registered routes - Document kernel-only endpoints **PR 4: Trigger system** - Time triggers (cron), webhook (inbound HTTP), event (bus subscription) - Extensions register match expressions at install **PR 5: SDK stabilization** - `sb.slots()`, `sb.actions`, `sb.api.ext()`, `sb.storage` - Theme tokens exposed to extensions --- ## Architecture Decisions (this session) | Decision | Detail | |----------|--------| | Full RBAC — no magic roles | `users.role` column dropped entirely. All authorization through group membership + permission grants. | | Explicit Everyone membership | No implicit "all users get Everyone perms". Every user added to Everyone group on create. `member_count` reflects reality. | | Admins group not special-cased | Code never checks group identity — only checks for `surface.admin.access` permission. Any group can grant it. | | No new migrations pre-MVP | Edit existing SQL files in place. Schema isn't in production. | | JWT simplified | Claims carry `user_id` + `email` only. Permissions resolved server-side from groups on every request. | | OIDC role → group sync | `isIdPAdmin()` replaces `resolveRole()`. IdP admin claim maps to Admins group membership, not a DB column. | | Token budgets/allowed models removed | Provider-era cruft. Belongs in a future provider extension, not the kernel. | --- ## File Locations | What | Where | |------|-------| | Roadmap | `ROADMAP.md` | | Changelog | `CHANGELOG.md` | | CI workflow | `.gitea/workflows/ci.yaml` | | K8s manifests | `k8s/` | | Docker compose (local dev) | `docker-compose.yml` (SQLite, port 3000) | | Nginx template | `nginx.conf.template` | | Migrations | `server/database/migrations/{postgres,sqlite}/` | | Permission constants | `server/auth/permissions.go` | | Admin middleware | `server/middleware/admin.go` | | Group store | `server/store/{postgres,sqlite}/groups.go` | | Bootstrap/seed | `server/handlers/auth.go` | | Admin handlers | `server/handlers/admin.go` | | Groups UI | `src/js/sw/surfaces/admin/groups.js` | | Users UI | `src/js/sw/surfaces/admin/users.js` | | SDK API | `src/js/sw/sdk/api-domains.js` | | Memory (Claude) | `.claude/projects/-config-Projects-core/memory/` | --- ## Gitea Secrets/Vars Needed **Secrets**: `POSTGRES_USER`, `POSTGRES_USER_PASSWORD`, `POSTGRES_ADMIN_USER`, `POSTGRES_ADMIN_PASSWORD`, `ENCRYPTION_KEY` **Vars**: `DOMAIN`, `NAMESPACE`, `S3_BUCKET`, `S3_ENDPOINT`, `STORAGE_BACKEND`, `STORAGE_CLASS`, `STORAGE_SIZE` **Not yet configured**: `S3_ACCESS_KEY`, `S3_SECRET_KEY` (using PVC for now), `SEED_USERS` (needed for initial admin bootstrap)