Skip to content

Commit 45bbf86

Browse files
committed
fix(security): rollbackImport — require importer or admin
`POST /api/registers/import/rollback` was `@NoAdminRequired` and the only safety net was that the downstream `deleteObject` runs RBAC. With broad delete rights any user could rollback a different user's import (or a different tenant's, since the audit lookup never filters by org) just by guessing/learning the job UUID. Look up the first audit row for the supplied `importJobId`, verify the caller is either the original importer (`auditRow->getUser()` matches the current UID) or in the admin group; reject otherwise. 404 when no audit rows match the job UUID at all. Refs: #1419 review (blocker 4) — discussion_r3187494430
1 parent 35aae9f commit 45bbf86

1 file changed

Lines changed: 37 additions & 0 deletions

File tree

lib/Controller/RegistersController.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,43 @@ public function rollbackImport(): JSONResponse
12641264
);
12651265
}
12661266

1267+
$user = $this->userSession->getUser();
1268+
if ($user === null) {
1269+
return new JSONResponse(
1270+
data: ['error' => 'Authentication required'],
1271+
statusCode: 401
1272+
);
1273+
}
1274+
1275+
// SECURITY: rollback wipes every object created by an import job.
1276+
// The only safety net was that `deleteObject` runs RBAC, which is
1277+
// much weaker than it sounds — any user with broad delete rights
1278+
// on the affected schemas could otherwise wipe a *different*
1279+
// user's import (and across tenants, since the audit lookup
1280+
// doesn't filter by organisation). Require the caller to be
1281+
// either the original importer or a member of the admin group.
1282+
$isAdmin = $this->groupManager->isAdmin($user->getUID());
1283+
$auditSample = $this->auditTrailMapper->findByImportJobId(
1284+
importJobId: $importJobId,
1285+
action: 'create'
1286+
);
1287+
if (count($auditSample) === 0) {
1288+
return new JSONResponse(
1289+
data: ['error' => 'Import job not found', 'importJobId' => $importJobId],
1290+
statusCode: 404
1291+
);
1292+
}
1293+
1294+
$importerUid = method_exists($auditSample[0], 'getUser') === true
1295+
? $auditSample[0]->getUser()
1296+
: null;
1297+
if ($isAdmin === false && $importerUid !== $user->getUID()) {
1298+
return new JSONResponse(
1299+
data: ['error' => 'Forbidden: only the user who initiated the import or an admin may roll it back'],
1300+
statusCode: 403
1301+
);
1302+
}
1303+
12671304
try {
12681305
$report = $this->importService->softDeleteByImportJobId(importJobId: $importJobId);
12691306
return new JSONResponse(data: $report, statusCode: 200);

0 commit comments

Comments
 (0)