Add one-time limited backoffice login grants (#329)
## Summary Adds the missing backend contract used by Pleno Control Plane Conversations/Suggestions to create an employee login action safely. - issues 60–900 second one-time limited-backoffice login grants - persists only SHA-256 bearer digests; bearer recovery is deterministic under the server encryption key for identical idempotent retries - enforces manager permissions, department scope, active managed-employee constraints, one-time atomic exchange, revocation, expiry, and account-deletion cleanup - adds employee-create idempotency so an approved automation retry cannot duplicate an employee - documents the create, revoke, and unauthenticated exchange endpoints in OpenAPI ## Security and concurrency - bearer values are returned only in a URL fragment and are never written to logs or database plaintext - employee and grant rows use a consistent employee-then-grant lock order - deactivation revokes outstanding grants and existing sessions in the same transaction - consumed, revoked, expired, or payload-mismatched idempotent replays fail closed ## Verification - `scripts/php-ci-test.sh api`: 273 passed, 11,086 assertions (one inherited warning) - focused security contract: 1 passed, 21 assertions - PHP syntax checks passed for the service and routes - `git diff --check` passed ## Dependency Required by copenhagentruckwash/pleno-control-plane#1. Merge before the matching frontend and Control Plane PRs.
This commit is contained in:
@@ -450,7 +450,25 @@ class account_deletion_service
|
||||
{
|
||||
global $db;
|
||||
if ($type === 'customer') {
|
||||
if (self::tableExists('limited_backoffice_employees')) {
|
||||
$employeeLock = $db->query(
|
||||
"SELECT user_id FROM limited_backoffice_employees
|
||||
WHERE user_id = $id LIMIT 1 FOR UPDATE"
|
||||
);
|
||||
if ($employeeLock === false) {
|
||||
throw new \RuntimeException('Unable to lock deleted employee.');
|
||||
}
|
||||
}
|
||||
$this->updateExistingColumns('users', $id, ['deleted_at' => $now]);
|
||||
if (self::tableExists('limited_backoffice_login_grants')) {
|
||||
$this->execute(
|
||||
"UPDATE limited_backoffice_login_grants
|
||||
SET revoked_at = COALESCE(revoked_at, " . self::sql($now) . ")
|
||||
WHERE (target_user_id = $id OR actor_user_id = $id)
|
||||
AND consumed_at IS NULL",
|
||||
'Unable to revoke deleted principal login grants.'
|
||||
);
|
||||
}
|
||||
$this->deleteTokensForPrincipal('customer', $id);
|
||||
} else {
|
||||
$this->updateExistingColumns('subusers', $id, ['suspended_at' => $now, 'deleted_at' => $now]);
|
||||
|
||||
Reference in New Issue
Block a user