guard permission fix
This commit is contained in:
@@ -100,6 +100,7 @@ class DepartmentAuthorizationPolicy implements AuthorizationPolicyInterface
|
|||||||
'capabilities' => [
|
'capabilities' => [
|
||||||
'can_view_page' => true,
|
'can_view_page' => true,
|
||||||
'can_update_department' => $this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_UPDATE),
|
'can_update_department' => $this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_UPDATE),
|
||||||
|
'can_delete_department' => $this->hasPermission($actorUserId, PermissionService::DEPARTMENTS_DELETE),
|
||||||
'allowed_tenant_ids' => $this->scopeGateway->getUserTenantIds($actorUserId),
|
'allowed_tenant_ids' => $this->scopeGateway->getUserTenantIds($actorUserId),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ if (!is_array($capabilities)) {
|
|||||||
}
|
}
|
||||||
$canViewPage = (bool) ($capabilities['can_view_page'] ?? false);
|
$canViewPage = (bool) ($capabilities['can_view_page'] ?? false);
|
||||||
$canUpdateDepartment = (bool) ($capabilities['can_update_department'] ?? false);
|
$canUpdateDepartment = (bool) ($capabilities['can_update_department'] ?? false);
|
||||||
|
$canDeleteDepartment = (bool) ($capabilities['can_delete_department'] ?? false);
|
||||||
$viewAuth['page'] = [
|
$viewAuth['page'] = [
|
||||||
'can_update_department' => $canUpdateDepartment,
|
'can_update_department' => $canUpdateDepartment,
|
||||||
|
'can_delete_department' => $canDeleteDepartment,
|
||||||
];
|
];
|
||||||
$allowedTenantIdsRaw = $capabilities['allowed_tenant_ids'] ?? [];
|
$allowedTenantIdsRaw = $capabilities['allowed_tenant_ids'] ?? [];
|
||||||
$allowedTenantIds = is_array($allowedTenantIdsRaw)
|
$allowedTenantIds = is_array($allowedTenantIdsRaw)
|
||||||
@@ -117,8 +119,10 @@ if ($request->hasBody('description')) {
|
|||||||
$submitCapabilities = $submitDecision->attribute('capabilities', []);
|
$submitCapabilities = $submitDecision->attribute('capabilities', []);
|
||||||
if (is_array($submitCapabilities)) {
|
if (is_array($submitCapabilities)) {
|
||||||
$canUpdateDepartment = (bool) ($submitCapabilities['can_update_department'] ?? $canUpdateDepartment);
|
$canUpdateDepartment = (bool) ($submitCapabilities['can_update_department'] ?? $canUpdateDepartment);
|
||||||
|
$canDeleteDepartment = (bool) ($submitCapabilities['can_delete_department'] ?? $canDeleteDepartment);
|
||||||
$viewAuth['page'] = [
|
$viewAuth['page'] = [
|
||||||
'can_update_department' => $canUpdateDepartment,
|
'can_update_department' => $canUpdateDepartment,
|
||||||
|
'can_delete_department' => $canDeleteDepartment,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (!$canUpdateDepartment) {
|
if (!$canUpdateDepartment) {
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><small><?php e(t($appThemeUserDesc)); ?></small></legend>
|
<legend><small><?php e(t($appThemeUserDesc)); ?></small></legend>
|
||||||
<label class="app-field">
|
<label class="app-field">
|
||||||
<input type="checkbox" name="app_theme_user" value="1" <?php e($appThemeUser ? 'checked' : ''); ?> <?php e($disabledAttr); ?>>
|
<input type="checkbox" role="switch" name="app_theme_user" value="1" <?php e($appThemeUser ? 'checked' : ''); ?> <?php e($disabledAttr); ?>>
|
||||||
<span><?php e(t('Allow user theme')); ?> <span class="badge" data-variant="info"><?php e(t('Cached')); ?></span></span>
|
<span><?php e(t('Allow user theme')); ?> <span class="badge" data-variant="info"><?php e(t('Cached')); ?></span></span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -508,6 +508,7 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
|
|||||||
<label class="app-field">
|
<label class="app-field">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
name="frontend_telemetry_enabled"
|
name="frontend_telemetry_enabled"
|
||||||
value="1"
|
value="1"
|
||||||
data-telemetry-enabled-toggle
|
data-telemetry-enabled-toggle
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ $canViewPage = (bool) ($capabilities['can_view_page'] ?? false);
|
|||||||
$canUpdateTenant = (bool) ($capabilities['can_update_tenant'] ?? false);
|
$canUpdateTenant = (bool) ($capabilities['can_update_tenant'] ?? false);
|
||||||
$canManageCustomFields = (bool) ($capabilities['can_manage_custom_fields'] ?? false);
|
$canManageCustomFields = (bool) ($capabilities['can_manage_custom_fields'] ?? false);
|
||||||
$canManageSso = (bool) ($capabilities['can_manage_sso'] ?? false);
|
$canManageSso = (bool) ($capabilities['can_manage_sso'] ?? false);
|
||||||
|
$canDeleteTenant = (bool) ($capabilities['can_delete_tenant'] ?? false);
|
||||||
$viewAuth['page'] = [
|
$viewAuth['page'] = [
|
||||||
'can_update_tenant' => $canUpdateTenant,
|
'can_update_tenant' => $canUpdateTenant,
|
||||||
|
'can_delete_tenant' => $canDeleteTenant,
|
||||||
'can_manage_custom_fields' => $canManageCustomFields,
|
'can_manage_custom_fields' => $canManageCustomFields,
|
||||||
'can_manage_sso' => $canManageSso,
|
'can_manage_sso' => $canManageSso,
|
||||||
];
|
];
|
||||||
@@ -120,10 +122,12 @@ if ($request->hasBody('description')) {
|
|||||||
$submitCapabilities = $submitDecision->attribute('capabilities', []);
|
$submitCapabilities = $submitDecision->attribute('capabilities', []);
|
||||||
if (is_array($submitCapabilities)) {
|
if (is_array($submitCapabilities)) {
|
||||||
$canUpdateTenant = (bool) ($submitCapabilities['can_update_tenant'] ?? $canUpdateTenant);
|
$canUpdateTenant = (bool) ($submitCapabilities['can_update_tenant'] ?? $canUpdateTenant);
|
||||||
|
$canDeleteTenant = (bool) ($submitCapabilities['can_delete_tenant'] ?? $canDeleteTenant);
|
||||||
$canManageCustomFields = (bool) ($submitCapabilities['can_manage_custom_fields'] ?? $canManageCustomFields);
|
$canManageCustomFields = (bool) ($submitCapabilities['can_manage_custom_fields'] ?? $canManageCustomFields);
|
||||||
$canManageSso = (bool) ($submitCapabilities['can_manage_sso'] ?? $canManageSso);
|
$canManageSso = (bool) ($submitCapabilities['can_manage_sso'] ?? $canManageSso);
|
||||||
$viewAuth['page'] = [
|
$viewAuth['page'] = [
|
||||||
'can_update_tenant' => $canUpdateTenant,
|
'can_update_tenant' => $canUpdateTenant,
|
||||||
|
'can_delete_tenant' => $canDeleteTenant,
|
||||||
'can_manage_custom_fields' => $canManageCustomFields,
|
'can_manage_custom_fields' => $canManageCustomFields,
|
||||||
'can_manage_sso' => $canManageSso,
|
'can_manage_sso' => $canManageSso,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace MintyPHP\Tests\Architecture;
|
namespace MintyPHP\Tests\Architecture;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class AuthzUiContractTest extends 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
|
public function testMapDrivenHardCutActionsUseUiCapabilityMaps(): void
|
||||||
{
|
{
|
||||||
@@ -192,5 +235,123 @@ class AuthzUiContractTest extends TestCase
|
|||||||
$this->assertStringContainsString('data-tab-panel="audit"', $content);
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,60 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
|||||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
$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
|
public function testEditSubmitRequiresDepartmentsUpdatePermission(): void
|
||||||
{
|
{
|
||||||
$permissionGateway = $this->permissionGatewayAllowing([
|
$permissionGateway = $this->permissionGatewayAllowing([
|
||||||
|
|||||||
Reference in New Issue
Block a user