guard permission fix

This commit is contained in:
2026-03-12 13:01:06 +01:00
parent 39a113c5fc
commit 17a72af5f2
6 changed files with 226 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class AuthzUiContractTest extends TestCase
@@ -149,6 +150,48 @@ class AuthzUiContractTest extends TestCase
}
}
#[DataProvider('adminPageAuthWiringProvider')]
public function testAdminPageAuthKeysAreWiredFromActions(
string $templateFile,
string $actionFile,
?string $uiCapabilityMapConstant
): void {
$templateContent = $this->readProjectFile($templateFile);
$requiredKeys = $this->extractPageAuthKeys($templateContent);
$this->assertNotEmpty($requiredKeys, $templateFile . ' must read at least one $pageAuth key.');
$actionContent = $this->readProjectFile($actionFile);
$this->assertStringContainsString("\$viewAuth['page']", $actionContent, $actionFile);
$wiredKeys = $this->extractViewAuthPageKeys($actionContent);
if ($uiCapabilityMapConstant !== null) {
$this->assertStringContainsString('->pageCapabilities(', $actionContent, $actionFile);
$this->assertStringContainsString($uiCapabilityMapConstant, $actionContent, $actionFile);
$wiredKeys = array_values(array_unique([
...$wiredKeys,
...$this->extractUiCapabilityMapKeys($uiCapabilityMapConstant),
]));
}
$missingKeys = array_values(array_diff($requiredKeys, $wiredKeys));
$this->assertSame(
[],
$missingKeys,
sprintf(
'Missing $pageAuth wiring in %s (from %s): %s',
$templateFile,
$actionFile,
implode(', ', $missingKeys)
)
);
}
public function testTenantEditActionExposesDeleteCapabilityForTemplate(): void
{
$content = $this->readProjectFile('pages/admin/tenants/edit($id).php');
$this->assertStringContainsString("'can_delete_tenant' =>", $content);
}
public function testMapDrivenHardCutActionsUseUiCapabilityMaps(): void
{
@@ -192,5 +235,123 @@ class AuthzUiContractTest extends TestCase
$this->assertStringContainsString('data-tab-panel="audit"', $content);
}
/**
* @return array<string, array{0: string, 1: string, 2: string|null}>
*/
public static function adminPageAuthWiringProvider(): array
{
return [
'users index' => [
'pages/admin/users/index(default).phtml',
'pages/admin/users/index().php',
'UiCapabilityMap::PAGE_USERS_INDEX',
],
'users create' => [
'pages/admin/users/create(default).phtml',
'pages/admin/users/create().php',
'UiCapabilityMap::PAGE_USERS_CREATE',
],
'tenants edit' => [
'pages/admin/tenants/edit(default).phtml',
'pages/admin/tenants/edit($id).php',
null,
],
'departments edit' => [
'pages/admin/departments/edit(default).phtml',
'pages/admin/departments/edit($id).php',
null,
],
'stats index' => [
'pages/admin/stats/index(default).phtml',
'pages/admin/stats/index().php',
'UiCapabilityMap::PAGE_STATS_INDEX',
],
'api audit index' => [
'pages/admin/api-audit/index(default).phtml',
'pages/admin/api-audit/index().php',
'UiCapabilityMap::PAGE_AUDIT_PURGE',
],
'import audit index' => [
'pages/admin/import-audit/index(default).phtml',
'pages/admin/import-audit/index().php',
'UiCapabilityMap::PAGE_AUDIT_PURGE',
],
'scheduled jobs index' => [
'pages/admin/scheduled-jobs/index(default).phtml',
'pages/admin/scheduled-jobs/index().php',
'UiCapabilityMap::PAGE_AUDIT_PURGE',
],
'system audit index' => [
'pages/admin/system-audit/index(default).phtml',
'pages/admin/system-audit/index().php',
'UiCapabilityMap::PAGE_SYSTEM_AUDIT',
],
'user lifecycle index' => [
'pages/admin/user-lifecycle-audit/index(default).phtml',
'pages/admin/user-lifecycle-audit/index().php',
'UiCapabilityMap::PAGE_AUDIT_PURGE',
],
'user lifecycle view' => [
'pages/admin/user-lifecycle-audit/view(default).phtml',
'pages/admin/user-lifecycle-audit/view($id).php',
'UiCapabilityMap::PAGE_USER_LIFECYCLE_VIEW',
],
];
}
/**
* @return list<string>
*/
private function extractPageAuthKeys(string $content): array
{
preg_match_all('/\\$pageAuth\\[[\'"]([a-z_]+)[\'"]\\]/', $content, $matches);
$keys = array_values(array_unique($matches[1] ?? []));
sort($keys);
return $keys;
}
/**
* @return list<string>
*/
private function extractViewAuthPageKeys(string $content): array
{
$keys = [];
preg_match_all('/\\$viewAuth\\[\'page\'\\]\\s*=\\s*\\[(.*?)\\];/s', $content, $blocks);
foreach ($blocks[1] ?? [] as $block) {
preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', $block, $matches);
foreach ($matches[1] ?? [] as $key) {
$keys[] = $key;
}
}
$keys = array_values(array_unique($keys));
sort($keys);
return $keys;
}
/**
* @return list<string>
*/
private function extractUiCapabilityMapKeys(string $mapConstant): array
{
$parts = explode('::', $mapConstant);
$constantName = trim((string) end($parts));
$this->assertNotSame('', $constantName, 'Invalid UiCapabilityMap constant: ' . $mapConstant);
$mapContent = $this->readProjectFile('lib/Service/Access/UiCapabilityMap.php');
$constantPattern = '/public const\\s+' . preg_quote($constantName, '/') . '\\s*=\\s*\\[(.*?)\\];/s';
$matched = preg_match($constantPattern, $mapContent, $constantMatch);
$this->assertSame(
1,
$matched,
'Could not find UiCapabilityMap constant block: ' . $mapConstant
);
preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', (string) ($constantMatch[1] ?? ''), $keyMatches);
$keys = array_values(array_unique($keyMatches[1] ?? []));
sort($keys);
return $keys;
}
}

View File

@@ -164,6 +164,60 @@ class DepartmentAuthorizationPolicyTest extends TestCase
$this->assertDeniedDecision($decision, 403, 'permission_denied');
}
public function testEditContextExposesDeleteCapabilityWhenDeletePermissionIsGranted(): void
{
$permissionGateway = $this->permissionGatewayAllowing([
5 => [PermissionService::DEPARTMENTS_VIEW, PermissionService::DEPARTMENTS_DELETE],
]);
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
$scopeGateway->expects($this->once())
->method('canAccess')
->with('departments', 17, 5)
->willReturn(true);
$scopeGateway->expects($this->once())
->method('getUserTenantIds')
->with(5)
->willReturn([2]);
$policy = new DepartmentAuthorizationPolicy($permissionGateway, $scopeGateway);
$decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT, [
'actor_user_id' => 5,
'target_department_id' => 17,
]);
$capabilities = $decision->attribute('capabilities', []);
$this->assertTrue($decision->isAllowed());
$this->assertTrue((bool) ($capabilities['can_delete_department'] ?? false));
}
public function testEditContextExposesDeleteCapabilityAsFalseWithoutDeletePermission(): void
{
$permissionGateway = $this->permissionGatewayAllowing([
5 => [PermissionService::DEPARTMENTS_VIEW],
]);
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
$scopeGateway->expects($this->once())
->method('canAccess')
->with('departments', 17, 5)
->willReturn(true);
$scopeGateway->expects($this->once())
->method('getUserTenantIds')
->with(5)
->willReturn([2]);
$policy = new DepartmentAuthorizationPolicy($permissionGateway, $scopeGateway);
$decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT, [
'actor_user_id' => 5,
'target_department_id' => 17,
]);
$capabilities = $decision->attribute('capabilities', []);
$this->assertTrue($decision->isAllowed());
$this->assertFalse((bool) ($capabilities['can_delete_department'] ?? true));
}
public function testEditSubmitRequiresDepartmentsUpdatePermission(): void
{
$permissionGateway = $this->permissionGatewayAllowing([