From 1bd4607b66caf193f02c7c3da4edc897fa7ae027 Mon Sep 17 00:00:00 2001 From: fs Date: Sun, 26 Apr 2026 15:00:05 +0200 Subject: [PATCH] refactor(creates-batch): migrate roles/permissions/tenants-create (step 10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cluster-7 batch-replay of the departments-create pilot (step 9). All three remaining create actions follow the same shape with minor domain-specific variations. Each migration touches one action and one policy: * roles-create + RoleAuthorizationPolicy::authorizeAdminRolesCreate — policy previously returned bare allow() with no capabilities; now emits ['can_view_page' => true]. Action passes viewAuthFlags: []. * permissions-create + PermissionAuthorizationPolicy::authorizeAdminPermissionsCreate — same pattern as roles-create. * tenants-create + TenantAuthorizationPolicy::authorizeAdminTenantsCreate — policy already emitted can_manage_sso + can_manage_custom_fields; can_view_page is added as the first capability. Action passes viewAuthFlags: ['can_manage_sso', 'can_manage_custom_fields'] and materializes both booleans from the aggregator capabilities. All three policy updates are tautological — every actor that survives the deny() branches in each policy can by definition see the page. View, Create, and EditContext now share a consistent capability shape across all four core master-data domains (departments, roles, permissions, tenants). Three drift decisions reproduced: * notFoundFlashScopeKey is N/A (no model lookup in create flows). * t() consistency: Flash::success('Role created' / 'Permission created' / 'Tenant created') now flow through t(). * Defensive scope consumption: $canManageAllTenants reads $tenantScope['scope'] === 'all' as a resilient hook even where the policy emits no manage-all flag (roles/permissions are global, tenants-create has no filter logic). Inline comments document the intentional non-consumption of $tenantScope['ids']. Two contract-test pattern updates (AuthzAdminMasterDataContractTest + AuthzAdminTenantsContractTest) shift the assertion targets from AuthorizationService::class to actionCreateContext( — semantically equivalent because the aggregator wraps the same authorize call internally. ActionContextCsrfPairingContractTest now covers nine callers (five edits + four creates) and stays green. Helper file core/Support/helpers/action_context.php is 0-diff for the seventh consecutive migration. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Access/PermissionAuthorizationPolicy.php | 6 +++- .../Access/RoleAuthorizationPolicy.php | 6 +++- .../Access/TenantAuthorizationPolicy.php | 1 + pages/admin/permissions/create().php | 24 +++++++++------ pages/admin/roles/create().php | 24 +++++++++------ pages/admin/tenants/create().php | 29 +++++++++++-------- .../AuthzAdminMasterDataContractTest.php | 4 +-- .../AuthzAdminTenantsContractTest.php | 2 +- 8 files changed, 61 insertions(+), 35 deletions(-) diff --git a/core/Service/Access/PermissionAuthorizationPolicy.php b/core/Service/Access/PermissionAuthorizationPolicy.php index e1fb3b5..f7b0d75 100644 --- a/core/Service/Access/PermissionAuthorizationPolicy.php +++ b/core/Service/Access/PermissionAuthorizationPolicy.php @@ -62,7 +62,11 @@ class PermissionAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::deny(403, 'forbidden'); } - return AuthorizationDecision::allow(); + return AuthorizationDecision::allow([ + 'capabilities' => [ + 'can_view_page' => true, + ], + ]); } private function authorizeAdminPermissionsEditContext(array $context): AuthorizationDecision diff --git a/core/Service/Access/RoleAuthorizationPolicy.php b/core/Service/Access/RoleAuthorizationPolicy.php index 57cda0c..cf4bc1b 100644 --- a/core/Service/Access/RoleAuthorizationPolicy.php +++ b/core/Service/Access/RoleAuthorizationPolicy.php @@ -62,7 +62,11 @@ class RoleAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::deny(403, 'forbidden'); } - return AuthorizationDecision::allow(); + return AuthorizationDecision::allow([ + 'capabilities' => [ + 'can_view_page' => true, + ], + ]); } private function authorizeAdminRolesEditContext(array $context): AuthorizationDecision diff --git a/core/Service/Access/TenantAuthorizationPolicy.php b/core/Service/Access/TenantAuthorizationPolicy.php index b8802f1..5758e3f 100644 --- a/core/Service/Access/TenantAuthorizationPolicy.php +++ b/core/Service/Access/TenantAuthorizationPolicy.php @@ -111,6 +111,7 @@ class TenantAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::allow([ 'capabilities' => [ + 'can_view_page' => true, 'can_manage_custom_fields' => $this->hasPermission($actorUserId, PermissionService::CUSTOM_FIELDS_MANAGE), 'can_manage_sso' => $this->hasPermission($actorUserId, PermissionService::TENANTS_SSO_MANAGE), ], diff --git a/pages/admin/permissions/create().php b/pages/admin/permissions/create().php index 9cb10ce..0e5e2ae 100644 --- a/pages/admin/permissions/create().php +++ b/pages/admin/permissions/create().php @@ -15,13 +15,19 @@ $closeTarget = requestResolveReturnTarget('admin/permissions'); $createTarget = requestPathWithReturnTarget('admin/permissions/create', $returnTarget); $currentUserId = (int) ($session['user']['id'] ?? 0); -$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class); -$decision = $authorizationService->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_CREATE, [ - 'actor_user_id' => $currentUserId, + +$context = actionCreateContext([ + 'abilityKey' => PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_CREATE, + 'context' => ['actor_user_id' => $currentUserId], + 'viewAuthFlags' => [], + 'forbiddenStrategy' => 'deny', ]); -if (!$decision->isAllowed()) { - Guard::deny(); -} +$capabilities = $context['capabilities']; +$tenantScope = $context['tenantScope']; +$viewAuth = $context['viewAuth']; +// Permissions are global; $tenantScope['ids'] is intentionally not consumed here. +// $canManageAllTenants stays as a resilient hook for future policy extensions. +$canManageAllTenants = $tenantScope['scope'] === 'all'; $errorBag = formErrors(); $errors = []; @@ -44,16 +50,16 @@ if ($request->isMethod('POST')) { if (($result['ok'] ?? false) && !$errorBag->hasAny()) { $action = (string) $request->body('action', 'create'); if ($action === 'create_close') { - Flash::success('Permission created', $closeTarget, 'permission_created'); + Flash::success(t('Permission created'), $closeTarget, 'permission_created'); Router::redirect($closeTarget); } else { $id = (int) ($result['id'] ?? 0); if ($id > 0) { $target = requestPathWithReturnTarget("admin/permissions/edit/{$id}", $returnTarget); - Flash::success('Permission created', $target, 'permission_created'); + Flash::success(t('Permission created'), $target, 'permission_created'); Router::redirect($target); } else { - Flash::success('Permission created', $closeTarget, 'permission_created'); + Flash::success(t('Permission created'), $closeTarget, 'permission_created'); Router::redirect($closeTarget); } } diff --git a/pages/admin/roles/create().php b/pages/admin/roles/create().php index dc08dc4..69107d6 100644 --- a/pages/admin/roles/create().php +++ b/pages/admin/roles/create().php @@ -15,13 +15,19 @@ $closeTarget = requestResolveReturnTarget('admin/roles'); $createTarget = requestPathWithReturnTarget('admin/roles/create', $returnTarget); $currentUserId = (int) ($session['user']['id'] ?? 0); -$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class); -$decision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_CREATE, [ - 'actor_user_id' => $currentUserId, + +$context = actionCreateContext([ + 'abilityKey' => RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_CREATE, + 'context' => ['actor_user_id' => $currentUserId], + 'viewAuthFlags' => [], + 'forbiddenStrategy' => 'deny', ]); -if (!$decision->isAllowed()) { - Guard::deny(); -} +$capabilities = $context['capabilities']; +$tenantScope = $context['tenantScope']; +$viewAuth = $context['viewAuth']; +// Roles are global; $tenantScope['ids'] is intentionally not consumed here. +// $canManageAllTenants stays as a resilient hook for future policy extensions. +$canManageAllTenants = $tenantScope['scope'] === 'all'; $errorBag = formErrors(); $errors = []; @@ -69,7 +75,7 @@ if ($request->isMethod('POST')) { if ($warnings) { Flash::info(implode(' ', $warnings), $closeTarget, 'role_warning'); } - Flash::success('Role created', $closeTarget, 'role_created'); + Flash::success(t('Role created'), $closeTarget, 'role_created'); Router::redirect($closeTarget); } else { $uuid = (string) ($result['uuid'] ?? ''); @@ -78,13 +84,13 @@ if ($request->isMethod('POST')) { if ($warnings) { Flash::info(implode(' ', $warnings), $target, 'role_warning'); } - Flash::success('Role created', $target, 'role_created'); + Flash::success(t('Role created'), $target, 'role_created'); Router::redirect($target); } else { if ($warnings) { Flash::info(implode(' ', $warnings), $closeTarget, 'role_warning'); } - Flash::success('Role created', $closeTarget, 'role_created'); + Flash::success(t('Role created'), $closeTarget, 'role_created'); Router::redirect($closeTarget); } } diff --git a/pages/admin/tenants/create().php b/pages/admin/tenants/create().php index 634241f..b0ef375 100644 --- a/pages/admin/tenants/create().php +++ b/pages/admin/tenants/create().php @@ -15,16 +15,21 @@ $closeTarget = requestResolveReturnTarget('admin/tenants'); $createTarget = requestPathWithReturnTarget('admin/tenants/create', $returnTarget); $currentUserId = (int) ($session['user']['id'] ?? 0); -$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class); -$decision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE, [ - 'actor_user_id' => $currentUserId, + +$context = actionCreateContext([ + 'abilityKey' => TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE, + 'context' => ['actor_user_id' => $currentUserId], + 'viewAuthFlags' => ['can_manage_sso', 'can_manage_custom_fields'], + 'forbiddenStrategy' => 'deny', ]); -if (!$decision->isAllowed()) { - Guard::deny(); -} -$capabilities = $decision->attribute('capabilities', []); -$canManageSso = is_array($capabilities) ? (bool) ($capabilities['can_manage_sso'] ?? false) : false; -$canManageCustomFields = is_array($capabilities) ? (bool) ($capabilities['can_manage_custom_fields'] ?? false) : false; +$capabilities = $context['capabilities']; +$tenantScope = $context['tenantScope']; +$viewAuth = $context['viewAuth']; +// Tenants-Create has no list-filter logic; $tenantScope['ids'] is intentionally not consumed here. +// $canManageAllTenants stays as a resilient hook for future policy extensions. +$canManageAllTenants = $tenantScope['scope'] === 'all'; +$canManageSso = (bool) ($capabilities['can_manage_sso'] ?? false); +$canManageCustomFields = (bool) ($capabilities['can_manage_custom_fields'] ?? false); $tenantSsoService = app(\MintyPHP\Service\Auth\TenantSsoService::class); $errorBag = formErrors(); @@ -121,16 +126,16 @@ if ($request->isMethod('POST')) { } $action = (string) ($post['action'] ?? 'create'); if ($action === 'create_close') { - Flash::success('Tenant created', $closeTarget, 'tenant_created'); + Flash::success(t('Tenant created'), $closeTarget, 'tenant_created'); Router::redirect($closeTarget); } else { $uuid = (string) ($result['uuid'] ?? ''); if ($uuid !== '') { $target = requestPathWithReturnTarget("admin/tenants/edit/{$uuid}", $returnTarget); - Flash::success('Tenant created', $target, 'tenant_created'); + Flash::success(t('Tenant created'), $target, 'tenant_created'); Router::redirect($target); } else { - Flash::success('Tenant created', $closeTarget, 'tenant_created'); + Flash::success(t('Tenant created'), $closeTarget, 'tenant_created'); Router::redirect($closeTarget); } } diff --git a/tests/Architecture/AuthzAdminMasterDataContractTest.php b/tests/Architecture/AuthzAdminMasterDataContractTest.php index ce9f2d8..f6fd76a 100644 --- a/tests/Architecture/AuthzAdminMasterDataContractTest.php +++ b/tests/Architecture/AuthzAdminMasterDataContractTest.php @@ -43,7 +43,7 @@ class AuthzAdminMasterDataContractTest extends TestCase $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW', $dataContent); $createContent = $this->readProjectFile('pages/admin/roles/create().php'); - $this->assertStringContainsString('AuthorizationService::class', $createContent); + $this->assertStringContainsString('actionCreateContext(', $createContent); $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_CREATE', $createContent); $editContent = $this->readProjectFile('pages/admin/roles/edit($id).php'); @@ -67,7 +67,7 @@ class AuthzAdminMasterDataContractTest extends TestCase $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW', $dataContent); $createContent = $this->readProjectFile('pages/admin/permissions/create().php'); - $this->assertStringContainsString('AuthorizationService::class', $createContent); + $this->assertStringContainsString('actionCreateContext(', $createContent); $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_CREATE', $createContent); $editContent = $this->readProjectFile('pages/admin/permissions/edit($id).php'); diff --git a/tests/Architecture/AuthzAdminTenantsContractTest.php b/tests/Architecture/AuthzAdminTenantsContractTest.php index 7651484..9001afa 100644 --- a/tests/Architecture/AuthzAdminTenantsContractTest.php +++ b/tests/Architecture/AuthzAdminTenantsContractTest.php @@ -19,7 +19,7 @@ class AuthzAdminTenantsContractTest extends TestCase $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW', $dataContent); $createContent = $this->readProjectFile('pages/admin/tenants/create().php'); - $this->assertStringContainsString('AuthorizationService::class', $createContent); + $this->assertStringContainsString('actionCreateContext(', $createContent); $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE', $createContent); $editContent = $this->readProjectFile('pages/admin/tenants/edit($id).php');