From 1974aba6c23916e8be5af1be9f2f7cdad96c199a Mon Sep 17 00:00:00 2001 From: fs Date: Sun, 26 Apr 2026 10:40:48 +0200 Subject: [PATCH] refactor(roles-edit): migrate to actionEditContext (step 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third pilot of the cluster rollout — first action that exercises forbiddenStrategy:'deny' in production. The CONTEXT-stage vorspiel collapses into one declarative actionEditContext call; the SUBMIT branch keeps its explicit Guard::deny() (Two-Level-Authorize). Confirms the analyst hypothesis: actionEditContext with forbiddenStrategy:'deny' is enough — no helper extension needed. Helper file stays 0-diff. The 'deny' path was built in step 1 and verified by testAuthorizeAndExtractCapabilitiesUsesGuardDenyStrategy, but only now proven against a real production caller. Three drift decisions from steps 2/3 reproduced: * notFoundFlashScopeKey: 'role_not_found' to preserve the existing Flash dedup-scope-key. * t() consistency: both Flash::success('Role updated', …) calls now flow through t() — German users see fully translated messages. * Defensive scope consumption: $canManageAllRoles reads $tenantScope['scope'] === 'all'. Roles are global, so $tenantScope['ids'] is intentionally not consumed; an inline comment documents that. Roles-specific: PermissionService-warmup absence is preserved (Roles don't need it — Departments/Tenants do, but cross-cluster consistency is not a goal here, behavioral identity is). ActionContextCsrfPairingContractTest now covers three callers (departments-edit, tenants-edit, roles-edit) and stays green. Co-Authored-By: Claude Opus 4.7 (1M context) --- pages/admin/roles/edit($id).php | 59 +++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/pages/admin/roles/edit($id).php b/pages/admin/roles/edit($id).php index 086054b..f5659be 100644 --- a/pages/admin/roles/edit($id).php +++ b/pages/admin/roles/edit($id).php @@ -14,30 +14,47 @@ $returnTarget = requestResolveReturnTarget(); $closeTarget = requestResolveReturnTarget('admin/roles'); $currentUserId = (int) ($session['user']['id'] ?? 0); +$uuid = trim((string) ($id ?? '')); +$editTarget = requestPathWithReturnTarget("admin/roles/edit/{$uuid}", $returnTarget); + +// Resolve the role BEFORE the aggregator so the authorize-context can carry +// the real target_role_id (matches today's order: lookup → CONTEXT authorize +// → Guard::deny()). The aggregator receives a trivial finder that returns the +// already-loaded model so its not-found branch fires only when findByUuid +// returned null. +$role = $uuid !== '' ? app(\MintyPHP\Service\Access\RoleService::class)->findByUuid($uuid) : null; +$roleId = (int) ($role['id'] ?? 0); + +$context = actionEditContext([ + 'finder' => static fn (string $_id): mixed => $role, + 'rawId' => $uuid, + 'notFoundFlashKey' => 'Role not found', + 'notFoundRedirectPath' => $closeTarget, + 'notFoundFlashScopeKey' => 'role_not_found', + 'abilityKey' => RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_CONTEXT, + 'context' => [ + 'actor_user_id' => $currentUserId, + 'target_role_id' => $roleId, + ], + 'viewAuthFlags' => ['can_edit_role', 'can_delete_role'], + 'forbiddenStrategy' => 'deny', +]); +$role = is_array($context['model']) ? $context['model'] : $role; +$capabilities = $context['capabilities']; +$tenantScope = $context['tenantScope']; +$viewAuth = $context['viewAuth']; +$canUpdateRole = (bool) ($capabilities['can_edit_role'] ?? false); +$canDeleteRole = (bool) ($capabilities['can_delete_role'] ?? false); +// Roles-Edit konsumiert $tenantScope['ids'] bewusst NICHT — Roles sind global, +// kein Filter-Szenario. $canManageAllRoles bleibt als Hook fuer kuenftige +// Policy-Erweiterungen definiert. +$canManageAllRoles = $tenantScope['scope'] === 'all'; + $authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class); $permissionRepository = app(\MintyPHP\Repository\Access\PermissionRepository::class); $rolePermissionRepository = app(\MintyPHP\Repository\Access\RolePermissionRepository::class); $userRoleRepository = app(\MintyPHP\Repository\Access\UserRoleRepository::class); $userWriteRepository = app(\MintyPHP\Repository\User\UserWriteRepository::class); -$uuid = trim((string) ($id ?? '')); -$editTarget = requestPathWithReturnTarget("admin/roles/edit/{$uuid}", $returnTarget); -$role = $uuid !== '' ? app(\MintyPHP\Service\Access\RoleService::class)->findByUuid($uuid) : null; -if (!$role) { - Flash::error('Role not found', $closeTarget, 'role_not_found'); - Router::redirect($closeTarget); -} - -$roleId = (int) ($role['id'] ?? 0); -$contextDecision = $authorizationService->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_CONTEXT, [ - 'actor_user_id' => $currentUserId, - 'target_role_id' => $roleId, -]); -if (!$contextDecision->isAllowed()) { - Guard::deny(); -} -$capabilities = $contextDecision->attribute('capabilities', []); -$canUpdateRole = is_array($capabilities) ? (bool) ($capabilities['can_edit_role'] ?? false) : false; -$canDeleteRole = is_array($capabilities) ? (bool) ($capabilities['can_delete_role'] ?? false) : false; app(\MintyPHP\Service\Audit\AuditMetadataEnricherInterface::class)->enrich($role); @@ -98,13 +115,13 @@ if ($request->isMethod('POST')) { if ($warnings) { Flash::info(implode(' ', $warnings), $closeTarget, 'role_warning'); } - Flash::success('Role updated', $closeTarget, 'role_updated'); + Flash::success(t('Role updated'), $closeTarget, 'role_updated'); Router::redirect($closeTarget); } else { if ($warnings) { Flash::info(implode(' ', $warnings), $editTarget, 'role_warning'); } - Flash::success('Role updated', $editTarget, 'role_updated'); + Flash::success(t('Role updated'), $editTarget, 'role_updated'); Router::redirect($editTarget); } }