feat(routes): allow operators to manage routes

Operators can now create, edit, and delete routes (previously admin-only).
A user may never scope a route above their own role tier: an operator
creating/editing an admin-visibility route is rejected (403 on the
visibility value, 404 on touching an existing higher-visibility route),
preventing them from creating routes they could then never see or modify.

- routes.py: RequireAdmin -> RequireOperatorOrAdmin on create/update/delete;
  add visibility-cap enforcement helpers reusing the existing
  resolve_user_role / VISIBILITY_LEVELS ladder
- web/app.py: proxy access map admits operator for routes POST/PUT/DELETE
- Routes.tsx: canManage gate (admin||operator) on Add/Edit/Delete; visibility
  <select> filters options by caller tier so operators never see 'admin'
- tests: operator-tier coverage (create/update/delete at/below/above level),
  proxy access-map assertion, vitest role-gating + filtered select
- e2e: mint operator session + routes-operator spec
- docs: routes.md + auth.md operator/visibility-cap notes
This commit is contained in:
Louis King
2026-07-24 20:59:25 +01:00
parent 5ead0fbad3
commit 83a936cd33
12 changed files with 437 additions and 61 deletions
+6 -2
View File
@@ -21,7 +21,7 @@ User roles are read from the OIDC token's `roles` claim (configurable via `OIDC_
| Role | Config Variable | Default | Description |
|------|----------------|---------|-------------|
| Admin | `OIDC_ROLE_ADMIN` | `admin` | Full write access to all API endpoints through the proxy |
| Operator | `OIDC_ROLE_OPERATOR` | `operator` | Reserved for future use — no endpoint assignments yet |
| Operator | `OIDC_ROLE_OPERATOR` | `operator` | Manage nodes, node tags, adoptions, and routes (create/edit/delete, scoped to the operator visibility tier) |
| Member | `OIDC_ROLE_MEMBER` | `member` | Read-only access (no endpoint assignments) |
The role names are configurable to match your IdP's role naming convention. For example, if your IdP uses `superuser` instead of `admin`, set `OIDC_ROLE_ADMIN=superuser`.
@@ -36,17 +36,21 @@ The proxy uses a hardcoded per-endpoint, per-method mapping in `src/meshcore_hub
|-------------|--------|--------|
| `v1/nodes` | GET | Open |
| `v1/nodes/` | GET | Open |
| `v1/nodes/` | POST, PUT, DELETE | `admin` |
| `v1/nodes/` | POST, PUT, DELETE | `admin`, `operator` |
| `v1/members` | GET | Open |
| `v1/members` | POST, PUT, DELETE | `admin` |
| `v1/messages` | GET | Open |
| `v1/advertisements` | GET | Open |
| `v1/adoptions` | POST, DELETE | `admin`, `operator` |
| `v1/routes` | POST | `admin`, `operator` |
| `v1/routes/` | PUT, DELETE | `admin`, `operator` |
| `v1/dashboard` | GET | Open |
| `v1/trace-paths` | GET | Open |
| `v1/telemetry` | GET | Open |
- **Open** = no authentication required (anonymous OK, works with or without OIDC)
- **`admin`** = requires OIDC enabled + user has the `admin` role
- **`admin`, `operator`** = requires OIDC enabled + user has the `admin` *or* `operator` role
- Method not listed for a matched prefix = denied
- No prefix match = denied
+4 -2
View File
@@ -44,11 +44,13 @@ The collector runs a background thread that re-evaluates every enabled route on
Routes carry the same role-based visibility levels as channels — `community`, `member`, `operator`, `admin`. A user only sees routes whose visibility is at or below their role's maximum level. Seeded routes default to `community` (visible to everyone); set a higher level to restrict a route to operators/admins only. Visibility is enforced on both the list and detail endpoints, so a hidden route's existence is not leaked.
Both operators and admins can create, edit, and delete routes. A user may never scope a route above their own role (e.g. an operator cannot create an `admin`-visibility route) — this is enforced on the write endpoints and prevents a user from creating a route they could then never see or modify. Operators can only edit/delete routes whose visibility is at or below the operator tier; attempting to modify a higher-visibility route returns `404`.
## Defining routes
Routes are keyed by their `from`/`to` endpoint labels and upserted by that pair. There are two ways to create them:
- **Seed YAML** — add a `routes.yaml` to your `SEED_HOME` and run the seed process. See [seeding.md → Routes](seeding.md#routes) for the format and rules (path nodes must already exist in the database; the `(from, to)` pair must be unique).
- **API** — `POST /api/v1/routes` (admin only) creates a route, with a `/preview` endpoint that dry-runs matching against an unsaved configuration so you can tune thresholds before committing. See `SCHEMAS.md` for the request/response shapes.
- **API** — `POST /api/v1/routes` (operator or admin) creates a route, with a `/preview` endpoint that dry-runs matching against an unsaved configuration so you can tune thresholds before committing. See `SCHEMAS.md` for the request/response shapes.
The `/routes` page renders the live status card, the per-day history strip, recent matching transmissions (with observer attribution), and — for admins — inline edit/delete controls.
The `/routes` page renders the live status card, the per-day history strip, recent matching transmissions (with observer attribution), and — for operators and admins — inline edit/delete controls.