fix(routes): preserve existing ownership on admin edit

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.
This commit is contained in:
Louis King
2026-07-24 21:54:23 +01:00
parent 333b094a77
commit 08a637e276
3 changed files with 20 additions and 15 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ Routes carry the same role-based visibility levels as channels — `community`,
Both operators and admins can create 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.
**Ownership-based editing:** Each route records the user who created it (`created_by`). Operators can edit and delete only the routes they created. Admins can edit and delete any route, and take ownership when they edit a route they did not create (the `created_by` field updates to the admin's user ID). Routes created before ownership tracking was introduced have a `NULL` `created_by` and are admin-only. The creator's display name is shown on each route card when available. Attempting to modify a route above the caller's visibility tier returns `404`; modifying a visible route the caller does not own returns `403`.
**Ownership-based editing:** Each route records the user who created it (`created_by`). Operators can edit and delete only the routes they created. Admins can edit and delete any route; they claim ownership of legacy (unowned) routes — those with a `NULL` `created_by` — when they edit them, but do not displace an existing creator. Routes created before ownership tracking was introduced have a `NULL` `created_by` and are admin-only. The creator's display name is shown on each route card when available. Attempting to modify a route above the caller's visibility tier returns `404`; modifying a visible route the caller does not own returns `403`.
## Defining routes
+5 -4
View File
@@ -647,7 +647,8 @@ def update_route(
"""Update a route (operator or admin).
Operators may only modify routes they created; admins can modify any
route and take ownership on edit.
route. Admins claim ownership of legacy (unowned) routes on edit but
do not displace an existing creator.
"""
user_id, _ = caller
route = session.execute(
@@ -657,10 +658,10 @@ def update_route(
raise HTTPException(status_code=404, detail="Route not found")
_assert_route_modifiable(request, route)
# Admin edits transfer ownership to the admin
if resolve_user_role(request) == "admin" and route.created_by != user_id:
# Admin claims ownership of legacy (unowned) routes on edit
if resolve_user_role(request) == "admin" and route.created_by is None:
route.created_by = user_id
logger.info("Admin %s took ownership of route %s", user_id, route.id)
logger.info("Admin %s claimed ownership of legacy route %s", user_id, route.id)
if body.from_label is not None or body.to_label is not None:
new_from = body.from_label if body.from_label is not None else route.from_label
+14 -10
View File
@@ -1324,7 +1324,8 @@ class TestRouteOperatorPermissions:
Ownership model (PR #337):
- Operators can edit/delete only routes they created.
- Admins can edit/delete any route and take ownership on edit.
- Admins can edit/delete any route; they claim ownership of legacy
(unowned) routes on edit but do not displace an existing creator.
- Routes with NULL ``created_by`` (legacy) are admin-only.
"""
@@ -1601,12 +1602,14 @@ class TestRouteOperatorPermissions:
assert resp.status_code == 200
assert resp.json()["created_by"] == "test-operator"
# ── Admin ownership transfer ──────────────────────────────────────
# ── Admin ownership semantics ─────────────────────────────────────
def test_admin_edit_transfers_ownership(self, client_no_auth, api_db_session):
"""Admin editing a route takes ownership (created_by changes)."""
def test_admin_edit_preserves_existing_ownership(
self, client_no_auth, api_db_session
):
"""Admin editing an operator-created route does NOT take ownership."""
route = Route(
from_label="Transfer",
from_label="Preserve",
to_label="End",
visibility="operator",
created_by="test-operator",
@@ -1616,19 +1619,20 @@ class TestRouteOperatorPermissions:
resp = client_no_auth.put(
f"/api/v1/routes/{route.id}",
json={"description": "admin took over"},
json={"description": "admin tweaked it"},
headers=ADMIN_HEADERS,
)
assert resp.status_code == 200
assert resp.json()["created_by"] == "test-admin"
# Ownership stays with the original creator
assert resp.json()["created_by"] == "test-operator"
# Original operator can no longer edit
# Original operator can still edit
resp2 = client_no_auth.put(
f"/api/v1/routes/{route.id}",
json={"description": "try again"},
json={"description": "operator still owns it"},
headers=OPERATOR_HEADERS,
)
assert resp2.status_code == 403
assert resp2.status_code == 200
def test_admin_edit_adopts_null_created_by(self, client_no_auth, api_db_session):
"""Admin editing a legacy route (NULL created_by) takes ownership."""