Harden subuser permission payloads (#328)

## Summary
- Normalize subuser grant permission payload keys before enum validation
so mixed-case customer-facing writes are accepted and deduped
consistently.
- Add a focused subuser route static check for permission payload
normalization.

## Verification
- `php
services/nginx/app/tests/subusers/SubusersRoutePermissionLinkTest.php &&
php
services/nginx/app/tests/subusers/SubusersRoutePermissionsPayloadTest.php`
- `php -l services/nginx/app/routes/subusersRoute.php && php -l
services/nginx/app/tests/subusers/SubusersRoutePermissionsPayloadTest.php`

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
This commit is contained in:
Jeppe B
2026-07-28 17:06:29 +02:00
committed by GitHub
co-authored by Jeppe Bundgaard
parent 30d860fdef
commit 3c13892366
3 changed files with 36 additions and 1 deletions
+2 -1
View File
@@ -119,10 +119,11 @@ class subusersRoute
$permissions = []; $permissions = [];
foreach ($raw as $permission) { foreach ($raw as $permission) {
$permission = is_string($permission) ? strtoupper(trim($permission)) : $permission;
if (!is_string($permission) || subusers_permission_node_key::tryFrom($permission) === null) { if (!is_string($permission) || subusers_permission_node_key::tryFrom($permission) === null) {
$response->error('Unknown permission key: ' . (string)$permission, 400); $response->error('Unknown permission key: ' . (string)$permission, 400);
} }
$permissions[] = strtoupper(trim($permission)); $permissions[] = $permission;
} }
return array_values(array_unique($permissions)); return array_values(array_unique($permissions));
@@ -30,5 +30,6 @@ return [
['path' => 'tests/slackModule/SlackModuleTest.php', 'classification' => 'manual-external', 'type' => 'script', 'reason' => 'Requires a configured Slack webhook and department webhook cache state.'], ['path' => 'tests/slackModule/SlackModuleTest.php', 'classification' => 'manual-external', 'type' => 'script', 'reason' => 'Requires a configured Slack webhook and department webhook cache state.'],
['path' => 'tests/subusers/SelfservePermissionInitTest.php', 'classification' => 'unit', 'type' => 'script'], ['path' => 'tests/subusers/SelfservePermissionInitTest.php', 'classification' => 'unit', 'type' => 'script'],
['path' => 'tests/subusers/SubusersRoutePermissionLinkTest.php', 'classification' => 'unit', 'type' => 'script'], ['path' => 'tests/subusers/SubusersRoutePermissionLinkTest.php', 'classification' => 'unit', 'type' => 'script'],
['path' => 'tests/subusers/SubusersRoutePermissionsPayloadTest.php', 'classification' => 'unit', 'type' => 'script'],
['path' => 'tests/subusers/SubuserUserGrantInitTest.php', 'classification' => 'unit', 'type' => 'script'], ['path' => 'tests/subusers/SubuserUserGrantInitTest.php', 'classification' => 'unit', 'type' => 'script'],
]; ];
@@ -0,0 +1,33 @@
<?php
if (!defined('WD')) {
define('WD', dirname(__DIR__, 2));
}
function ok($message): void { echo "\n\033[32m✔ $message\033[0m\n"; }
function fail($message): void { echo "\n\033[31m✖ $message\033[0m\n"; exit(1); }
$routeFile = WD . '/routes/subusersRoute.php';
if (!file_exists($routeFile)) {
fail('subusersRoute.php not found');
}
$code = file_get_contents($routeFile);
if ($code === false) {
fail('Unable to read subusersRoute.php');
}
$normalized = preg_replace('/\s+/', ' ', $code);
$normalizesBeforeEnumLookup = 'permission = is_string($permission) ? strtoupper(trim($permission)) : $permission; if (!is_string($permission) || subusers_permission_node_key::tryFrom($permission) === null)';
if (strpos($normalized, $normalizesBeforeEnumLookup) === false) {
fail('Expected permissions payload parser to normalize permission keys before enum validation');
}
$dedupesNormalizedPermissions = 'return array_values(array_unique($permissions));';
if (strpos($normalized, $dedupesNormalizedPermissions) === false) {
fail('Expected permissions payload parser to deduplicate normalized permission keys');
}
ok('Subusers route normalizes mixed-case permission payloads before validation and dedupe');
echo "\nSubusersRoutePermissionsPayloadTest completed.\n";