Let only the owner delete a project, not an administrator (#272)
#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:
1 parent
f14c4ecb77
commit
cbda6d64a7
5 files changed
+69
-17
No files matched your search
@@ -27,12 +27,15 @@ function check(string $name, bool $ok, mixed $got = null): void
|
||||
echo "FAIL {$name}", $got === null ? '' : ' — got ' . json_encode($got), "\n";
|
||||
}
|
||||
|
||||
function throws(callable $work): bool
|
||||
function throws(callable $work, string $class = InvalidArgumentException::class): bool
|
||||
{
|
||||
try {
|
||||
$work();
|
||||
} catch (InvalidArgumentException) {
|
||||
return true;
|
||||
} catch (Throwable $e) {
|
||||
if ($e instanceof $class) {
|
||||
return true;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -110,8 +113,16 @@ check('the new name owns it', $moved !== null && $moved['owner'] === 'annabel');
|
||||
check('the old name does not', $mine->load($id) === null);
|
||||
check('transfer keeps the revision', $moved['revision'] === 2, $moved['revision'] ?? null);
|
||||
|
||||
$other = (new Store($projects, 'carl'))->create('Carl\'s game');
|
||||
check('an administrator deletes somebody else\'s project', $admin->delete($other['id']) && $admin->listOf('carl') === []);
|
||||
$carl = new Store($projects, 'carl');
|
||||
$other = $carl->create('Carl\'s game');
|
||||
check('an administrator may not delete somebody else\'s project', throws(fn() => $admin->delete($other['id']), NotOwnerException::class));
|
||||
check('the refused delete leaves the project', $carl->load($other['id']) !== null);
|
||||
check('its owner deletes it', $carl->delete($other['id']) && $admin->listOf('carl') === []);
|
||||
$ownerless = "{$projects}/nobody01.json";
|
||||
file_put_contents($ownerless, json_encode(['id' => 'nobody01', 'revision' => 1, 'project' => []]));
|
||||
check('an administrator may not delete a project without an owner', throws(fn() => $admin->delete('nobody01'), NotOwnerException::class) && is_file($ownerless));
|
||||
$own = $admin->create('Boss\'s game');
|
||||
check('an administrator deletes their own project', $admin->delete($own['id']) && $admin->list() === []);
|
||||
|
||||
// The web server serves `data/` as static files, so a readable project would skip the sign-in.
|
||||
check('a project file is not readable for the web server', (fileperms("{$projects}/{$id}.json") & 0o007) === 0);
|
||||
|
||||
Reference in new issue
Block a user