From d0544d342890d7975f0079cd2b7603e33b2ed81b Mon Sep 17 00:00:00 2001 From: fs Date: Sun, 26 Apr 2026 10:05:58 +0200 Subject: [PATCH] refactor(tenants-edit): migrate to actionEditContext (step 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second pilot of the cluster rollout. Tenants-edit follows the same pattern as departments-edit (step 2): the CONTEXT-stage vorspiel collapses into one declarative actionEditContext call, the POST branch (CSRF → SUBMIT-authorize → can_update gate → service call → PRG) stays callsite-specific. Confirms the analyst hypothesis from this run: actionEditContext is strong enough for a second standard edit action without any further API extension. The notFoundFlashScopeKey arg added during step 2 is the only hook needed; helpers stay 0-diff. The three drift decisions from step 2 are reproduced verbatim: * notFoundFlashScopeKey: 'tenant_not_found' to preserve the existing Flash dedup-scope-key. * t() consistency: both Flash::success('Tenant updated', …) calls now flow through t(), so German users see fully translated success messages rather than a German/English mix. * Defensive scope consumption: $canManageAllTenants reads $tenantScope['scope'] === 'all'. Tenants-edit has no tenant-scope filtering of its own (the action edits tenants themselves), so $tenantScope['ids'] is intentionally not consumed; an inline comment documents that. ActionContextCsrfPairingContractTest now covers two callers (departments-edit, tenants-edit) and stays green. Co-Authored-By: Claude Opus 4.7 (1M context) --- pages/admin/tenants/edit($id).php | 73 ++++++++++++++++--------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/pages/admin/tenants/edit($id).php b/pages/admin/tenants/edit($id).php index cde72f8..d86e7bc 100644 --- a/pages/admin/tenants/edit($id).php +++ b/pages/admin/tenants/edit($id).php @@ -20,45 +20,46 @@ if ($currentUserId > 0) { } $uuid = trim((string) ($id ?? '')); $editTarget = requestPathWithReturnTarget("admin/tenants/edit/{$uuid}", $returnTarget); -$tenant = $uuid !== '' ? app(\MintyPHP\Service\Tenant\TenantService::class)->findByUuid($uuid) : null; -if (!$tenant) { - Flash::error('Tenant not found', $closeTarget, 'tenant_not_found'); - Router::redirect($closeTarget); -} +// Resolve the tenant BEFORE the aggregator so the authorize-context can carry +// the real target_tenant_id (matches today's order: lookup → CONTEXT authorize +// → can_view_page). The aggregator receives a trivial finder that returns the +// already-loaded model so its not-found branch fires only when findByUuid +// returned null. +$tenant = $uuid !== '' ? app(\MintyPHP\Service\Tenant\TenantService::class)->findByUuid($uuid) : null; $tenantId = (int) ($tenant['id'] ?? 0); + +$context = actionEditContext([ + 'finder' => static fn (string $_id): mixed => $tenant, + 'rawId' => $uuid, + 'notFoundFlashKey' => 'Tenant not found', + 'notFoundRedirectPath' => $closeTarget, + 'notFoundFlashScopeKey' => 'tenant_not_found', + 'abilityKey' => TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT, + 'context' => [ + 'actor_user_id' => $currentUserId, + 'target_tenant_id' => $tenantId, + ], + 'viewAuthFlags' => ['can_update_tenant', 'can_delete_tenant', 'can_manage_custom_fields', 'can_manage_sso'], + 'forbiddenStrategy' => 'redirect', +]); +$tenant = is_array($context['model']) ? $context['model'] : $tenant; +$capabilities = $context['capabilities']; +$tenantScope = $context['tenantScope']; +$viewAuth = $context['viewAuth']; +$canUpdateTenant = (bool) ($capabilities['can_update_tenant'] ?? false); +$canDeleteTenant = (bool) ($capabilities['can_delete_tenant'] ?? false); +$canManageCustomFields = (bool) ($capabilities['can_manage_custom_fields'] ?? false); +$canManageSso = (bool) ($capabilities['can_manage_sso'] ?? false); +// Tenants-Edit konsumiert $tenantScope['ids'] bewusst NICHT — wir editieren +// die Mandanten selbst, kein Filter-Szenario wie bei Departments-Edit. +// $canManageAllTenants bleibt als Hook fuer kuenftige Policy-Erweiterungen +// definiert. +$canManageAllTenants = $tenantScope['scope'] === 'all'; + $tenantSsoService = app(\MintyPHP\Service\Auth\TenantSsoService::class); $authService = app(\MintyPHP\Service\Auth\AuthService::class); $authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class); -$contextDecision = $authorizationService->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT, [ - 'actor_user_id' => $currentUserId, - 'target_tenant_id' => $tenantId, -]); -if (!$contextDecision->isAllowed()) { - Router::redirect('error/forbidden'); - return; -} - -$capabilities = $contextDecision->attribute('capabilities', []); -if (!is_array($capabilities)) { - $capabilities = []; -} - -$canViewPage = (bool) ($capabilities['can_view_page'] ?? false); -$canUpdateTenant = (bool) ($capabilities['can_update_tenant'] ?? false); -$canManageCustomFields = (bool) ($capabilities['can_manage_custom_fields'] ?? false); -$canManageSso = (bool) ($capabilities['can_manage_sso'] ?? false); -$canDeleteTenant = (bool) ($capabilities['can_delete_tenant'] ?? false); -$viewAuth['page'] = [ - 'can_update_tenant' => $canUpdateTenant, - 'can_delete_tenant' => $canDeleteTenant, - 'can_manage_custom_fields' => $canManageCustomFields, - 'can_manage_sso' => $canManageSso, -]; -if (!$canViewPage) { - Router::redirect('error/forbidden'); - return; -} $tenantCustomFieldService = app(TenantCustomFieldService::class); $customFieldDefinitions = $canManageCustomFields @@ -133,10 +134,10 @@ if ($request->isMethod('POST')) { } $action = (string) ($post['action'] ?? 'save'); if ($action === 'save_close') { - Flash::success('Tenant updated', $closeTarget, 'tenant_updated'); + Flash::success(t('Tenant updated'), $closeTarget, 'tenant_updated'); Router::redirect($closeTarget); } else { - Flash::success('Tenant updated', $editTarget, 'tenant_updated'); + Flash::success(t('Tenant updated'), $editTarget, 'tenant_updated'); Router::redirect($editTarget); } }