Skip to content

feat(routes): ownership-based edit/delete permissions - #336

Merged
jinglemansweep merged 3 commits into
mainfrom
feat/routes-ownership
Jul 24, 2026
Merged

feat(routes): ownership-based edit/delete permissions#336
jinglemansweep merged 3 commits into
mainfrom
feat/routes-ownership

Conversation

@jinglemansweep

Copy link
Copy Markdown
Contributor

Summary

Replaces the visibility-tier-based write permission model with ownership-based permissions:

  • Operators can edit/delete only the routes they created
  • Admins can edit/delete any route and take ownership on edit
  • Routes with NULL created_by (legacy, pre-tracking) are admin-only
  • The creator's friendly name is displayed on the route card with a profile link

Permission Matrix

Caller Create Edit/Delete own Edit/Delete others NULL created_by Above-tier
Admin any visibility yes yes yes yes
Operator up to operator tier yes 403 403 404

Changes

Database

  • Migration da69304d8106: adds nullable created_by String(255) to routes (batch mode, SQLite-safe)

Backend (routes.py)

  • _assert_route_modifiable: dual-layer check — visibility (404, hides existence) + ownership (403, transparent rejection)
  • create_route: stamps created_by=user_id, ensures UserProfile exists via get_or_create_profile
  • update_route: admin edits transfer ownership (route.created_by = admin_id, logged); RouteUpdate schema has no created_by field so clients can't inject it
  • Owner resolution: _resolve_owner (single) + _resolve_owners_batch (list, avoids N+1)

Schema

  • New RouteOwner model (user_id, name, callsign, profile_id) — mirrors AdoptedByUser pattern
  • created_by + owner added to both RouteRead and RouteDetail

Frontend (Routes.tsx)

  • RouteCard renamed canManage to canEdit (per-route ownership gate)
  • canEditRoute(r) = isAdmin || r.created_by === config.user?.sub
  • Edit/Delete buttons hidden when the operator doesn't own the route
  • Owner name badge with profile link, shown when created_by is set

Tests

  • Backend (75 total): 20+ new ownership tests — create stamping, operator own/other/legacy/admin-created routes, admin ownership transfer, ownership injection prevention, owner name resolution in list/detail
  • Frontend (325 total): 5 new per-route ownership tests — hidden on unowned, shown on owned, shown on all for admin, owner badge displayed/hidden
  • E2E: Operator spec asserts no edit/delete on seeded (NULL) route; admin spec asserts edit/delete visible on all routes

Docs

  • routes.md + auth.md updated for ownership model

Test Results

  • 1486 backend tests pass
  • 325 frontend tests pass
  • TS clean, e2e typecheck + collection clean
  • pre-commit all passed

Replace visibility-tier-based write permissions with ownership-based:
operators can only edit/delete routes they created; admins can modify
any route and take ownership on edit. Each route stores the creator's
OIDC user_id (created_by column). Legacy routes with NULL created_by
are admin-only.

The creator's friendly name is resolved from UserProfile and displayed
on the route card with a profile link. Edit/delete buttons are hidden
per-route based on ownership rather than a flat role check.

- Migration: add nullable routes.created_by (batch mode, SQLite-safe)
- _assert_route_modifiable: dual-check visibility (404) + ownership (403)
- create_route: stamps created_by, ensures profile exists
- update_route: admin edits transfer ownership (logged)
- RouteOwner schema mirrors AdoptedByUser pattern
- Batch owner resolution in list endpoint (avoids N+1)
- Frontend: per-route canEdit gate, owner badge with profile link
- Tests: 20+ backend ownership tests, 5 frontend gating tests, e2e assertions
- Docs: routes.md + auth.md updated for ownership model
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Admins editing an operator-created route no longer steal ownership.
Ownership transfer now happens only for legacy (NULL created_by) routes.
This ensures operators retain edit access to their routes after an admin
makes a small tweak.
@jinglemansweep
jinglemansweep merged commit ca88e9f into main Jul 24, 2026
5 checks passed
@jinglemansweep
jinglemansweep deleted the feat/routes-ownership branch July 24, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant