From d6be536fbf841b73e6e8f9de6dc3573761432ed5 Mon Sep 17 00:00:00 2001 From: fs Date: Sun, 26 Apr 2026 14:44:09 +0200 Subject: [PATCH] refactor(departments-create): migrate to actionCreateContext (step 9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First production use of actionCreateContext — the second aggregator introduced in step 1 and unit-tested at the building-block level, but not yet exercised against a real caller. Helper file stays 0-diff for the sixth consecutive migration. The migration uncovers one real API gap and resolves it at the policy layer rather than at the helper: * actionCreateContext always calls actionEnforceCanViewPage. The Departments create-decision was the only Departments authorize branch that did not emit can_view_page (View and EditContext both did). Adding 'can_view_page' => true to the create-capabilities map is tautological — every actor that survives the deny() guards at lines 69-70 and 75-76 can by definition see the page. No new forbidden path is created. View, Create, and EditContext now share the same capability shape. Three drift decisions reproduced where applicable: * notFoundFlashScopeKey is N/A (no model lookup in create flow). * t() consistency: all three Flash::success('Department created', …) calls now flow through t(). * Defensive scope consumption: $canManageAllTenants reads $tenantScope['scope'] === 'all', mirroring the edit-action pattern. The GET tenant filter rewrites from is_array($allowedTenantIds) to the three-way scope-tuple form. AuthzAdminMasterDataContractTest gets a single-line assertion update (AuthorizationService::class → actionCreateContext() pattern). The aggregator wraps the same authorize call internally, so this is a pattern-rename, not a semantic shift. ActionContextCsrfPairingContractTest now covers six callers (five edits + departments-create) and stays green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Access/DepartmentAuthorizationPolicy.php | 1 + pages/admin/departments/create().php | 31 +++++++++++-------- .../AuthzAdminMasterDataContractTest.php | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/core/Service/Access/DepartmentAuthorizationPolicy.php b/core/Service/Access/DepartmentAuthorizationPolicy.php index 52ae92d..71ae0c4 100644 --- a/core/Service/Access/DepartmentAuthorizationPolicy.php +++ b/core/Service/Access/DepartmentAuthorizationPolicy.php @@ -78,6 +78,7 @@ class DepartmentAuthorizationPolicy implements AuthorizationPolicyInterface return AuthorizationDecision::allow([ 'capabilities' => [ + 'can_view_page' => true, 'allowed_tenant_ids' => $allowedTenantIds, 'is_strict_scope' => $isStrictScope, ], diff --git a/pages/admin/departments/create().php b/pages/admin/departments/create().php index 5d18f8b..62b0e58 100644 --- a/pages/admin/departments/create().php +++ b/pages/admin/departments/create().php @@ -18,16 +18,19 @@ $directoryScopeGateway = app(\MintyPHP\Service\Tenant\TenantScopeService::class) $departmentService = app(\MintyPHP\Service\Org\DepartmentService::class); $currentUserId = (int) ($session['user']['id'] ?? 0); -$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class); -$decision = $authorizationService->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE, [ - 'actor_user_id' => $currentUserId, + +$context = actionCreateContext([ + 'abilityKey' => DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE, + 'context' => ['actor_user_id' => $currentUserId], + 'viewAuthFlags' => [], + 'forbiddenStrategy' => 'deny', ]); -if (!$decision->isAllowed()) { - Guard::deny(); -} -$capabilities = $decision->attribute('capabilities', []); -$allowedTenantIds = is_array($capabilities) ? toIntIds($capabilities['allowed_tenant_ids'] ?? []) : []; -$isStrictScope = is_array($capabilities) ? (bool) ($capabilities['is_strict_scope'] ?? false) : false; +$capabilities = $context['capabilities']; +$tenantScope = $context['tenantScope']; +$viewAuth = $context['viewAuth']; +$canManageAllTenants = $tenantScope['scope'] === 'all'; +$allowedTenantIds = toIntIds($capabilities['allowed_tenant_ids'] ?? []); +$isStrictScope = (bool) ($capabilities['is_strict_scope'] ?? false); $errorBag = formErrors(); $errors = []; @@ -40,7 +43,9 @@ $form = [ 'active' => '1', ]; $tenants = $tenantService->list(); -if ($allowedTenantIds) { +if ($canManageAllTenants) { + // No filtering — actor may pick any tenant. +} elseif ($allowedTenantIds) { $tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool { $tenantId = (int) ($tenant['id'] ?? 0); return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true); @@ -72,7 +77,7 @@ if ($request->isMethod('POST')) { if ($warnings) { Flash::info(implode(' ', $warnings), $closeTarget, 'department_warning'); } - Flash::success('Department created', $closeTarget, 'department_created'); + Flash::success(t('Department created'), $closeTarget, 'department_created'); Router::redirect($closeTarget); } else { $uuid = (string) ($result['uuid'] ?? ''); @@ -81,13 +86,13 @@ if ($request->isMethod('POST')) { if ($warnings) { Flash::info(implode(' ', $warnings), $target, 'department_warning'); } - Flash::success('Department created', $target, 'department_created'); + Flash::success(t('Department created'), $target, 'department_created'); Router::redirect($target); } else { if ($warnings) { Flash::info(implode(' ', $warnings), $closeTarget, 'department_warning'); } - Flash::success('Department created', $closeTarget, 'department_created'); + Flash::success(t('Department created'), $closeTarget, 'department_created'); Router::redirect($closeTarget); } } diff --git a/tests/Architecture/AuthzAdminMasterDataContractTest.php b/tests/Architecture/AuthzAdminMasterDataContractTest.php index 796e910..ce9f2d8 100644 --- a/tests/Architecture/AuthzAdminMasterDataContractTest.php +++ b/tests/Architecture/AuthzAdminMasterDataContractTest.php @@ -19,7 +19,7 @@ class AuthzAdminMasterDataContractTest extends TestCase $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW', $dataContent); $createContent = $this->readProjectFile('pages/admin/departments/create().php'); - $this->assertStringContainsString('AuthorizationService::class', $createContent); + $this->assertStringContainsString('actionCreateContext(', $createContent); $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE', $createContent); $editContent = $this->readProjectFile('pages/admin/departments/edit($id).php');