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
+16
-4
@@ -49,11 +49,15 @@ export async function users(t) {
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
});
|
||||
const asAdmin = (method, route, body) => call(adminCookie, method, route, body);
|
||||
// Projects first: an account that still owns one is not deleted (#179).
|
||||
// Projects first: an account that still owns one is not deleted (#179), and only the owner may
|
||||
// delete them (#272). The step never changes the throwaway password, so it still signs in.
|
||||
const sweep = async () => {
|
||||
const accounts = await (await asAdmin('GET', 'users')).json();
|
||||
for (const account of accounts.filter((a) => a.username === TEMP || a.username === RENAMED)) {
|
||||
for (const project of account.projects) await asAdmin('DELETE', `projects/${project.id}`);
|
||||
if (account.projects.length > 0) {
|
||||
const ownerCookie = await logIn(baseUrl, account.username, TEMP_PASSWORD);
|
||||
for (const project of account.projects) await call(ownerCookie, 'DELETE', `projects/${project.id}`);
|
||||
}
|
||||
await asAdmin('DELETE', `users/${account.username}`);
|
||||
}
|
||||
};
|
||||
@@ -283,8 +287,16 @@ export async function users(t) {
|
||||
JSON.stringify({ name: stored.name, owner: stored.owner }),
|
||||
);
|
||||
|
||||
// 49i. With the project gone, the account goes too.
|
||||
await asAdmin('DELETE', `projects/${made.id}`);
|
||||
// 49i. Opening and editing are not deleting: that stays with the owner (#272). With the
|
||||
// project gone, the account goes too.
|
||||
const deleteByAdmin = (await asAdmin('DELETE', `projects/${made.id}`)).status;
|
||||
const ownerCookie = await logIn(baseUrl, RENAMED, TEMP_PASSWORD);
|
||||
const deleteByOwner = (await call(ownerCookie, 'DELETE', `projects/${made.id}`)).status;
|
||||
check(
|
||||
'an administrator may not delete somebody else’s project, its owner may',
|
||||
deleteByAdmin === 403 && deleteByOwner === 204,
|
||||
JSON.stringify({ deleteByAdmin, deleteByOwner }),
|
||||
);
|
||||
await openManagement();
|
||||
await pressButton(RENAMED, 'delete');
|
||||
await clickLast('Delete');
|
||||
|
||||
Reference in new issue
Block a user