Let only the owner delete a project, not an administrator (#272)
game-maker / gate (push) Successful in 2m36s
game-maker / suite (push) Successful in 13m2s

#179 decided that nobody deletes a project on someone else's behalf.
The API still let an administrator do it, because the admin store
treated every project as its own. Option A from the issue is now in
place: opening, editing, snapshots and release stay open to the
administrator, deleting stays with the owner.

- Store::delete checks the owner under the save lock and throws
  NotOwnerException for anyone else. It follows the pattern of
  StaleRevisionException.
- The route answers that with 403 "Only its owner may delete a
  project". A file without an owner is covered the same way.
  Non-admins still get 404 for a project that is not theirs.
- The editor has no way to delete a foreign project, the project
  picker lists only one's own. Nothing changes there.
- An account that owns projects can no longer be deleted by an
  administrator at all. Its owner has to delete them first.
- e2e 49 deleted the throwaway projects as administrator, both in
  its sweep and in 49i. It now signs in as the owner for that.
  49i checks both answers.

Tests:
- tests/php/accounts.test.php: 56 checks passed (was 52, one check
  turned around, five new)
- e2e 49: 22 checks passed (was 21), deleteByAdmin 403,
  deleteByOwner 204
- Gate: 197 files, 2855 tests passed
- Counter-checks: without the owner check 4 PHP checks and 49i fail
- The e2e total of 1603 is computed (1602 + 1), not measured

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-30 21:13:20 +02:00
1 parent f14c4ecb77
commit cbda6d64a7
5 files changed
+69 -17

No files matched your search

+10 -3
View File
@@ -16,7 +16,8 @@ declare(strict_types=1);
*
* Everything below needs a signed-in user (401 otherwise), and projects belong to one: a project
* of somebody else answers like one that is not there – unless the one asking is an administrator
* (#179), who reads, saves and deletes it like their own; a save keeps its owner.
* (#179), who reads and saves it like their own; a save keeps its owner. Deleting a project is
* its owner's alone (#272).
*
* Administrators only (403 for everybody else, #180):
*
@@ -31,7 +32,8 @@ declare(strict_types=1);
* POST ?r=projects {name} → {id,name,revision,updatedAt,project}
* GET ?r=projects/{id} → {id,name,revision,updatedAt,project}
* PUT ?r=projects/{id} {revision, project} → {revision,updatedAt} | 409 {error,revision}
* DELETE ?r=projects/{id} → 204 (its snapshots and its release go with it)
* DELETE ?r=projects/{id} → 204 | 403 | 404 (its snapshots and its release go with it;
* 403 for an administrator who is not the owner)
*
* GET ?r=projects/{id}/snapshots → [{id,name,revision,createdAt}], newest first
* POST ?r=projects/{id}/snapshots {name?} → 201 {id,name,revision,createdAt}
@@ -230,7 +232,12 @@ function routeProjects(array $parts, string $method, string $owner, bool $seesAl
}
if ($method === 'DELETE') {
if (!$store->delete($id)) {
try {
$deleted = $store->delete($id);
} catch (NotOwnerException $e) {
Response::error($e->getMessage(), 403);
}
if (!$deleted) {
Response::error('Project not found', 404);
}
$snapshots->deleteAll($id);