Enforce role-based authorization on admin-only routes (fixes #10) #15

Merged
whitlocktech merged 1 commits from fix/role-authorization into main 2026-07-03 02:31:09 +00:00
Member

Summary

Fixes #10. isLoggedIn only verified that the JWT was valid — no route ever checked req.user.role. Any authenticated editor could therefore call every admin endpoint: create/promote/delete users, flip the site in/out of maintenance, change settings, delete wiki pages, etc. This is a privilege-escalation hole.

This PR adds a role gate and applies it to the admin-only routes.

What changed

  • server/src/utils/auth.js — new requireRole(...roles) middleware factory. It assumes isLoggedIn has already run (so req.user is set) and returns 403 Forbidden when req.user.role is not in the allowed list.
  • server/src/router/v1/admin/admin.routes.js — defines const adminOnly = requireRole('admin') and applies it to the sensitive routes:
    • PUT /site-mode
    • GET /settings, PUT /settings
    • all /users/* — gated once with adminRouter.use('/users', adminOnly) placed before the user route definitions, so list/create/update/delete are all covered by a single line.

Access model (per-route decision)

The two roles map to a content-vs-administration split:

Area admin editor
Posts (CRUD, publish, upload) yes yes
Wiki pages / categories / tags / revisions yes yes
Dashboard, Activity log yes yes
PUT /site-mode yes no
GET/PUT /settings yes no
/users/* (manage accounts) yes no

Rationale: an editor is a content role, so posts and wiki stay open to them. Anything that can change who has access or how the whole site behaves (users, site mode, settings) is restricted to admin.

Why this approach

  • Middleware factory over inline checks — one reusable, testable gate rather than scattered if (req.user.role !== 'admin') branches in each controller. It matches the suggested fix in the issue and composes with the existing isLoggedIn chain.
  • adminRouter.use('/users', adminOnly) — gating the subtree in one place is harder to get wrong than annotating four separate user routes, and any future /users/* route is protected by default.
  • Left in auth.js alongside isLoggedIn so all auth/authorization primitives live together and are exported from one module.

Testing

  • node -c syntax check on both changed files.
  • Manual reasoning against every admin route; content routes remain reachable by editors, the three admin-only areas now return 403 for a non-admin token.

Notes

Purely additive/backend. No DB or client changes. Independent of the companion PR for #12 (stale-JWT revalidation); the two reinforce each other — once #12 lands, req.user.role is always the fresh DB role, which is exactly what this gate reads.

## Summary Fixes #10. `isLoggedIn` only verified that the JWT was valid — no route ever checked `req.user.role`. Any authenticated **editor** could therefore call every admin endpoint: create/promote/delete users, flip the site in/out of maintenance, change settings, delete wiki pages, etc. This is a privilege-escalation hole. This PR adds a role gate and applies it to the admin-only routes. ## What changed - **`server/src/utils/auth.js`** — new `requireRole(...roles)` middleware factory. It assumes `isLoggedIn` has already run (so `req.user` is set) and returns `403 Forbidden` when `req.user.role` is not in the allowed list. - **`server/src/router/v1/admin/admin.routes.js`** — defines `const adminOnly = requireRole('admin')` and applies it to the sensitive routes: - `PUT /site-mode` - `GET /settings`, `PUT /settings` - all `/users/*` — gated once with `adminRouter.use('/users', adminOnly)` placed before the user route definitions, so list/create/update/delete are all covered by a single line. ## Access model (per-route decision) The two roles map to a content-vs-administration split: | Area | admin | editor | |------|:-----:|:------:| | Posts (CRUD, publish, upload) | yes | yes | | Wiki pages / categories / tags / revisions | yes | yes | | Dashboard, Activity log | yes | yes | | `PUT /site-mode` | yes | no | | `GET/PUT /settings` | yes | no | | `/users/*` (manage accounts) | yes | no | Rationale: an `editor` is a content role, so posts and wiki stay open to them. Anything that can change *who has access* or *how the whole site behaves* (users, site mode, settings) is restricted to `admin`. ## Why this approach - **Middleware factory over inline checks** — one reusable, testable gate rather than scattered `if (req.user.role !== 'admin')` branches in each controller. It matches the suggested fix in the issue and composes with the existing `isLoggedIn` chain. - **`adminRouter.use('/users', adminOnly)`** — gating the subtree in one place is harder to get wrong than annotating four separate user routes, and any future `/users/*` route is protected by default. - **Left in `auth.js`** alongside `isLoggedIn` so all auth/authorization primitives live together and are exported from one module. ## Testing - `node -c` syntax check on both changed files. - Manual reasoning against every admin route; content routes remain reachable by editors, the three admin-only areas now return 403 for a non-admin token. ## Notes Purely additive/backend. No DB or client changes. Independent of the companion PR for #12 (stale-JWT revalidation); the two reinforce each other — once #12 lands, `req.user.role` is always the fresh DB role, which is exactly what this gate reads.
wtclaude added 1 commit 2026-07-03 02:22:22 +00:00
isLoggedIn only verified a valid JWT, so an authenticated editor could
call any admin endpoint (create/promote/delete users, flip site mode,
change settings). Add a requireRole middleware factory and gate the
sensitive routes with admin-only:

- PUT  /site-mode
- GET/PUT /settings
- all /users/* (list/create/update/delete)

Content routes (posts, wiki, categories, tags, uploads, dashboard,
activity) remain available to editors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
whitlocktech approved these changes 2026-07-03 02:30:59 +00:00
whitlocktech left a comment
Owner

It works and is properly gated

It works and is properly gated
whitlocktech merged commit 853224b578 into main 2026-07-03 02:31:09 +00:00
Sign in to join this conversation.
No description provided.