Move user administration to its own page; add Project Super User
User accounts lived in the Admin Console, which is admins-only. Project admins
need to create the accounts on their own jobs without an app admin on the phone,
so accounts move to a new User Directory page and a new role carries the right.
server/auth.py, server/app.py
New permissions role `project_super_user`, between admin and project_admin:
everything a project admin may do, plus user administration SCOPED to the
projects they hold the role on. Four limits make it safe to hand out, all
enforced server-side:
* Scope comes from projects, not the job title. It resolves per membership
(managed_project_ids), so an ordinary account can hold it on one job via
ProjectMember.role, and a super user demoted on one job administers
nobody there. No projects, no authority.
* Account-level changes (password, disable, rename, permissions, delete)
require EXCLUSIVE scope: refused when the target is also on a project the
caller does not administer, because those changes are global. The
directory renders such rows read-only with the reason.
* No admin or super-user targets, and neither role can be granted by a
super user -- that is the line that stops it becoming app-wide control.
* PUT .../projects rebuilds only the caller's own slice; memberships on
projects they do not administer are left untouched. A payload that simply
omits them must not cut someone off a job the caller cannot see.
Creating requires naming at least one of your own projects: an account with
none would be one the creator instantly cannot manage.
/api/auth/users is now scoped rather than admin-only, and carries a per-row
`manageable` verdict plus the reason. Non-managers get a contact card only --
a project user has no business reading colleagues' login history. New
/api/auth/user-scope tells the page what it may offer. Administrative
password resets are now audited; they were the one account change that left
no trace. Settings, feature flags and the auto-add rule stay admin-only.
While here: one definition of "is a user manager", derived from the managed
set. An account-role-only version disagreed with the scoped one and locked
per-project super users out of routes they were entitled to.
html/users.html, html/users.js
The directory: three renderings from one page -- admin (everything), super
user (controls per row, read-only where scope is shared), everyone else (a
read-only directory of the people on their own projects).
html/console.css, html/console-util.js
Extracted from admin.html/admin.js so both console pages share them. A
divergent jsq() is an XSS and a divergent role list offers permissions the
server refuses, so neither may exist twice.
html/wp-sidenav.{js,css}
Global nav drawer, role-gated, carrying ?project= across links. Mounted on
the field view (which had no way to anywhere) plus both console pages.
No migration: users.role is already String(20) and the new value fits.
Verified: 93 scope/gate tests, 29 live HTTP tests through the real dependency
stack, 33 static JS checks. Not verified in a browser -- no JS engine on this
machine -- so users.html and field.html want one manual load.
server/smoketest.py still fails with 401s. Pre-existing: it has no login code,
so auth_gate refuses it. Confirmed unchanged by stashing this work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,14 @@ the token cannot be read, but the injected code does not need it: it runs in the
|
||||
victim's page and can call any API the victim can, including
|
||||
`POST /api/auth/users/{id}/role`.
|
||||
|
||||
**The `project_super_user` role (added 2026-08-05) widens the set of victims whose
|
||||
session is worth stealing, without raising the ceiling.** Previously only an app
|
||||
admin's session could create accounts or change permissions; now a super user's can
|
||||
too, within the projects they administer. The ceiling is unchanged — it was already
|
||||
`admin` — but the odds of landing on a session that can mint an account go up, and a
|
||||
super user is likelier than an admin to be reading a WP creator on a live job. It is
|
||||
one more reason the accidental-breakage case is not the only one that matters.
|
||||
|
||||
Two controls that look like they would contain this do not:
|
||||
|
||||
- **CSP does not mitigate it.** `nginx-wp-suite.conf:58` serves
|
||||
@@ -83,6 +91,12 @@ malicious one.
|
||||
suite is exposed outside the corporate network, accounts are issued to
|
||||
subcontractors or clients, or self-registration is added.
|
||||
|
||||
Note that the second of those got easier to reach without anyone deciding to: a
|
||||
Project Super User can now issue accounts on their own job without an app admin
|
||||
involved, so "accounts are issued to subcontractors" can become true by ordinary
|
||||
delegated use rather than by a policy change. Worth checking the directory
|
||||
occasionally against who is actually on staff.
|
||||
|
||||
### What closing it takes
|
||||
|
||||
Small — roughly half an hour. The helper already exists; it was added to the SOP
|
||||
@@ -102,8 +116,9 @@ function escHandlerArg(v){ return escAttr(String(v==null?'':v).replace(/\\/g,'\\
|
||||
4. Confirm with a discipline named `Owner's Equipment`: the pill must respond to
|
||||
clicks and the name must display intact.
|
||||
|
||||
The equivalent fix on the admin side is `jsq()` in `html/admin.js` — same ordering,
|
||||
same reasoning, worth reading before starting.
|
||||
The equivalent fix on the admin side is `jsq()` in `html/console-util.js` (it moved
|
||||
out of `html/admin.js` on 2026-08-05 when the User Directory started needing it) —
|
||||
same ordering, same reasoning, worth reading before starting.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user