refactor(actions): complete admin request/csrf guard centralization
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"task_id": "2026-04-24-action-csrf-centralization-step3",
|
||||||
|
"issue_summary": "Nach Wave 2 waren in pages/admin noch einzelne Endpunkte mit direkten requestInput()->method() und Session::checkCsrfToken()-Checks vorhanden (JSON/Data und einige User-POST-Endpoints). Ziel ist die komplette Zentralisierung der Request/CSRF-Guards in pages/admin.",
|
||||||
|
"affected_layers": ["pages", "core/Support", "tests"],
|
||||||
|
"affected_files": [
|
||||||
|
{ "path": "core/Support/helpers/request.php", "role": "Action guard helpers (POST/CSRF) extended for JSON-focused endpoints" },
|
||||||
|
{ "path": "pages/admin/users/theme().php", "role": "User theme switch endpoint (POST + optional JSON)" },
|
||||||
|
{ "path": "pages/admin/users/switch-tenant().php", "role": "Tenant context switch endpoint (POST + optional JSON)" },
|
||||||
|
{ "path": "pages/admin/session-ping/data().php", "role": "JSON keepalive endpoint (POST-only + CSRF)" },
|
||||||
|
{ "path": "pages/admin/tenants/ldap-test-connection/data().php", "role": "JSON LDAP test endpoint (POST-only + CSRF)" },
|
||||||
|
{ "path": "pages/admin/users/create().php", "role": "User create form action (POST + CSRF)" },
|
||||||
|
{ "path": "pages/admin/users/bulk($action).php", "role": "Bulk user mutation endpoint (JSON + CSRF)" },
|
||||||
|
{ "path": "pages/admin/users/api-token-create($id).php", "role": "API token create action (POST + CSRF)" },
|
||||||
|
{ "path": "pages/admin/users/api-token-revoke($id).php", "role": "API token revoke action (POST + CSRF)" },
|
||||||
|
{ "path": "pages/admin/imports/index().php", "role": "Import multi-stage POST flow with in-page error rendering" },
|
||||||
|
{ "path": "tests/Architecture/AdminActionRequestGuardContractTest.php", "role": "New architecture contract to block direct legacy method/csrf checks in pages/admin" }
|
||||||
|
],
|
||||||
|
"existing_patterns": [
|
||||||
|
"Most admin actions already migrated to actionRequirePost/actionRequireCsrf in waves 1+2",
|
||||||
|
"Remaining inline checks were concentrated in a small set of JSON/data and legacy user actions",
|
||||||
|
"Some endpoints need JSON payload/status customization and non-redirect CSRF failure handling"
|
||||||
|
],
|
||||||
|
"related_tests": [
|
||||||
|
"tests/Architecture/AdminActionRequestGuardContractTest.php",
|
||||||
|
"tests/Architecture/PostEndpointCsrfContractTest.php",
|
||||||
|
"tests/Architecture/PostRedirectGetContractTest.php",
|
||||||
|
"tests/Architecture/AuthzAdminSettingsContractTest.php",
|
||||||
|
"tests/Architecture/AuthzAdminMasterDataContractTest.php",
|
||||||
|
"tests/Architecture/AuthzAdminUsersContractTest.php"
|
||||||
|
],
|
||||||
|
"security_surface": {
|
||||||
|
"authz_relevant": true,
|
||||||
|
"tenant_scope_relevant": true,
|
||||||
|
"input_handling": true,
|
||||||
|
"crypto_relevant": false,
|
||||||
|
"file_upload_relevant": false,
|
||||||
|
"notes": "Focus remains on GR-SEC-001 CSRF hardening and preserving existing authz / redirect/json behavior."
|
||||||
|
},
|
||||||
|
"assumptions": [
|
||||||
|
"Scope is strictly pages/admin completion; non-admin pages and module pages are out-of-scope for this step.",
|
||||||
|
"Existing JSON response payload shapes must be preserved where already consumed by frontend code.",
|
||||||
|
"No service/repository business logic changes are required."
|
||||||
|
],
|
||||||
|
"risks_discovered": [
|
||||||
|
"JSON-only data endpoints need helper support beyond default Request::wantsJson detection and default payload shape.",
|
||||||
|
"imports/index keeps CSRF errors in-page (non-redirect), requiring non-redirect helper mode.",
|
||||||
|
"api-token create/revoke previously had no explicit POST method guard; adding it must keep redirect behavior safe."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"task_id": "2026-04-24-action-csrf-centralization-step3",
|
||||||
|
"plan_ref": ".agents/runs/2026-04-24-action-csrf-centralization-step3/plan.json",
|
||||||
|
"status": "done",
|
||||||
|
"changed_files": [
|
||||||
|
{ "path": "core/Support/helpers/request.php", "summary": "Extended helper API with jsonStatusCode/jsonPayload/forceJson options for POST/CSRF guard responses." },
|
||||||
|
{ "path": "pages/admin/users/theme().php", "summary": "Replaced inline method/csrf checks with actionRequirePost/actionRequireCsrf while preserving JSON error payload." },
|
||||||
|
{ "path": "pages/admin/users/switch-tenant().php", "summary": "Replaced inline method/csrf checks with helper-based flow and preserved flash/json behavior." },
|
||||||
|
{ "path": "pages/admin/session-ping/data().php", "summary": "Migrated to helper guards using forceJson and status-preserving JSON payloads." },
|
||||||
|
{ "path": "pages/admin/tenants/ldap-test-connection/data().php", "summary": "Migrated method/csrf checks to helper guards with forceJson and original error payloads." },
|
||||||
|
{ "path": "pages/admin/users/create().php", "summary": "Replaced inline CSRF block with actionRequireCsrf in POST branch." },
|
||||||
|
{ "path": "pages/admin/users/bulk($action).php", "summary": "Added helper-based POST guard and migrated CSRF check to actionRequireCsrf in JSON mode." },
|
||||||
|
{ "path": "pages/admin/users/api-token-create($id).php", "summary": "Added actionRequirePost and actionRequireCsrf; kept redirect/flash semantics." },
|
||||||
|
{ "path": "pages/admin/users/api-token-revoke($id).php", "summary": "Added actionRequirePost and actionRequireCsrf; kept redirect/flash semantics." },
|
||||||
|
{ "path": "pages/admin/imports/index().php", "summary": "Replaced inline CSRF check with non-redirect actionRequireCsrf mode to keep in-page validation behavior." },
|
||||||
|
{ "path": "tests/Architecture/AdminActionRequestGuardContractTest.php", "summary": "Added contract test to prevent direct legacy method/csrf checks in pages/admin." }
|
||||||
|
],
|
||||||
|
"guard_evidence": [
|
||||||
|
{
|
||||||
|
"guard_id": "GR-CORE-003",
|
||||||
|
"status": "pass",
|
||||||
|
"evidence": "All affected admin actions keep requestInput() usage; no superglobal access introduced."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"guard_id": "GR-CORE-012",
|
||||||
|
"status": "pass",
|
||||||
|
"evidence": "PRG flows remain intact where applicable; PostRedirectGetContractTest passes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"guard_id": "GR-SEC-001",
|
||||||
|
"status": "pass",
|
||||||
|
"evidence": "Inline Session::checkCsrfToken removed from pages/admin; helper-based CSRF validation now centralized."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"guard_id": "GR-SEC-008",
|
||||||
|
"status": "pass",
|
||||||
|
"evidence": "Authorization checks remain unchanged in migrated actions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"guard_id": "GR-SEC-009",
|
||||||
|
"status": "pass",
|
||||||
|
"evidence": "Tenant scope and actor context validation flows remain unchanged."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"guard_id": "GR-TEST-001",
|
||||||
|
"status": "pass",
|
||||||
|
"evidence": "Targeted architecture tests and full PHPUnit suite pass."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
{ "cmd": "docker compose exec -T php vendor/bin/phpunit tests/Architecture/AdminActionRequestGuardContractTest.php", "result": "pass" },
|
||||||
|
{ "cmd": "docker compose exec -T php vendor/bin/phpunit tests/Architecture/PostEndpointCsrfContractTest.php tests/Architecture/PostRedirectGetContractTest.php", "result": "pass" },
|
||||||
|
{ "cmd": "docker compose exec -T php vendor/bin/phpunit tests/Architecture/AuthzAdminSettingsContractTest.php tests/Architecture/AuthzAdminMasterDataContractTest.php tests/Architecture/AuthzAdminUsersContractTest.php", "result": "pass" },
|
||||||
|
{ "cmd": "docker compose exec -T php vendor/bin/phpunit", "result": "pass" },
|
||||||
|
{ "cmd": "docker compose exec -T php vendor/bin/phpstan analyse -c phpstan.neon --no-progress", "result": "pass" },
|
||||||
|
{ "cmd": "docker compose exec -T php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php", "result": "pass" },
|
||||||
|
{ "cmd": "docker compose exec -T php vendor/bin/php-cs-fixer fix --config=tools/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --diff --verbose", "result": "pass" },
|
||||||
|
{ "cmd": "bin/docs-link-check.sh", "result": "pass" },
|
||||||
|
{ "cmd": "bin/docs-drift-check.sh", "result": "pass" },
|
||||||
|
{ "cmd": "bin/codex-skills-sync.sh --check", "result": "pass" }
|
||||||
|
],
|
||||||
|
"quality_gate_results": [
|
||||||
|
{ "gate_id": "QG-001", "result": "pass", "notes": "PHPUnit full run green: 2025 tests, 29191 assertions." },
|
||||||
|
{ "gate_id": "QG-002", "result": "pass", "notes": "PHPStan level 5: no errors." },
|
||||||
|
{ "gate_id": "QG-003", "result": "pass", "notes": "CoreStarterkitContractTest green: 13 tests, 6980 assertions." },
|
||||||
|
{ "gate_id": "QG-006", "result": "pass", "notes": "php-cs-fixer dry-run green." },
|
||||||
|
{ "gate_id": "QG-008", "result": "pass", "notes": "docs-link-check and docs-drift-check green." },
|
||||||
|
{ "gate_id": "QG-009", "result": "pass", "notes": "codex-skills-sync --check green." }
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"step3_target_files": 9,
|
||||||
|
"before_requestinput_method_calls": 4,
|
||||||
|
"after_requestinput_method_calls": 0,
|
||||||
|
"before_inline_session_csrf_checks": 9,
|
||||||
|
"after_inline_session_csrf_checks": 0,
|
||||||
|
"admin_inline_method_checks_total_after": 0,
|
||||||
|
"admin_inline_csrf_checks_total_after": 0
|
||||||
|
},
|
||||||
|
"open_items": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"task_id": "2026-04-24-action-csrf-centralization-step3",
|
||||||
|
"ready_to_finalize": true,
|
||||||
|
"code_review": "pass",
|
||||||
|
"security_review": "pass",
|
||||||
|
"acceptance_review": "pass",
|
||||||
|
"ci_status": "required-gates-pass",
|
||||||
|
"final_action": "commit",
|
||||||
|
"hold_reason": "",
|
||||||
|
"commit_message": "refactor(actions): complete admin request/csrf guard centralization",
|
||||||
|
"notes": "Final admin wave completed; pages/admin no longer contains direct requestInput()->method() or Session::checkCsrfToken() checks."
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"task_id": "2026-04-24-action-csrf-centralization-step3",
|
||||||
|
"analysis_ref": ".agents/runs/2026-04-24-action-csrf-centralization-step3/analysis.json",
|
||||||
|
"summary": "Final wave to complete POST/CSRF guard centralization for all remaining admin actions, including JSON/data endpoints and architecture enforcement for future drift prevention.",
|
||||||
|
"scope": {
|
||||||
|
"in": [
|
||||||
|
"Extend actionRequirePost/actionRequireCsrf for JSON status/payload and forceJson use-cases",
|
||||||
|
"Migrate all remaining inline method/csrf checks in pages/admin to helper-based guards",
|
||||||
|
"Add architecture test that forbids direct requestInput()->method() and Session::checkCsrfToken() usage in pages/admin",
|
||||||
|
"Run required quality gates and admin-related architecture checks"
|
||||||
|
],
|
||||||
|
"out": [
|
||||||
|
"Non-admin core pages",
|
||||||
|
"Module pages under modules/*/pages",
|
||||||
|
"Business-logic changes in services/repositories"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"guardrails": {
|
||||||
|
"required_guard_ids": [
|
||||||
|
"GR-CORE-003",
|
||||||
|
"GR-CORE-012",
|
||||||
|
"GR-SEC-001",
|
||||||
|
"GR-SEC-008",
|
||||||
|
"GR-SEC-009",
|
||||||
|
"GR-TEST-001"
|
||||||
|
],
|
||||||
|
"required_quality_gate_ids": [
|
||||||
|
"QG-001",
|
||||||
|
"QG-002",
|
||||||
|
"QG-003",
|
||||||
|
"QG-006",
|
||||||
|
"QG-008",
|
||||||
|
"QG-009"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"success_criteria": [
|
||||||
|
{
|
||||||
|
"id": "SC-001",
|
||||||
|
"criterion": "All remaining pages/admin inline method checks are replaced by actionRequirePost() or requestInput()->isMethod('POST') + actionRequireCsrf() patterns."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SC-002",
|
||||||
|
"criterion": "All remaining pages/admin inline Session::checkCsrfToken() checks are replaced by actionRequireCsrf()."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SC-003",
|
||||||
|
"criterion": "New architecture contract prevents reintroduction of direct requestInput()->method() and Session::checkCsrfToken() in pages/admin."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SC-004",
|
||||||
|
"criterion": "CSRF and PRG architecture tests remain green and authz admin contract suites remain green."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SC-005",
|
||||||
|
"criterion": "Required quality gate set QG-001/002/003/006/008/009 passes."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"implementation_steps": [
|
||||||
|
{
|
||||||
|
"id": "S1",
|
||||||
|
"title": "Helper capability extension",
|
||||||
|
"description": "Add optional JSON status/payload and forceJson flags to actionRequirePost/actionRequireCsrf without breaking existing callers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S2",
|
||||||
|
"title": "Remaining admin endpoint migration",
|
||||||
|
"description": "Refactor users/theme, users/switch-tenant, session-ping/data, tenants/ldap-test-connection/data, users/create, users/bulk, users/api-token-create, users/api-token-revoke, imports/index."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S3",
|
||||||
|
"title": "Architecture hardening",
|
||||||
|
"description": "Add and run admin contract test to enforce helper-based guards in pages/admin going forward."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S4",
|
||||||
|
"title": "Verification and gate execution",
|
||||||
|
"description": "Run targeted architecture checks and full required gates; capture metrics and run artifacts."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tests": [
|
||||||
|
"docker compose exec -T php vendor/bin/phpunit tests/Architecture/AdminActionRequestGuardContractTest.php",
|
||||||
|
"docker compose exec -T php vendor/bin/phpunit tests/Architecture/PostEndpointCsrfContractTest.php tests/Architecture/PostRedirectGetContractTest.php",
|
||||||
|
"docker compose exec -T php vendor/bin/phpunit tests/Architecture/AuthzAdminSettingsContractTest.php tests/Architecture/AuthzAdminMasterDataContractTest.php tests/Architecture/AuthzAdminUsersContractTest.php",
|
||||||
|
"docker compose exec -T php vendor/bin/phpunit",
|
||||||
|
"docker compose exec -T php vendor/bin/phpstan analyse -c phpstan.neon --no-progress",
|
||||||
|
"docker compose exec -T php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php",
|
||||||
|
"docker compose exec -T php vendor/bin/php-cs-fixer fix --config=tools/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --diff --verbose",
|
||||||
|
"bin/docs-link-check.sh",
|
||||||
|
"bin/docs-drift-check.sh",
|
||||||
|
"bin/codex-skills-sync.sh --check"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"task_id": "2026-04-24-action-csrf-centralization-step3",
|
||||||
|
"verdict": "pass",
|
||||||
|
"checked_criterion_ids": [
|
||||||
|
"SC-001",
|
||||||
|
"SC-002",
|
||||||
|
"SC-003",
|
||||||
|
"SC-004",
|
||||||
|
"SC-005"
|
||||||
|
],
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"criterion_id": "SC-001",
|
||||||
|
"criterion": "All remaining pages/admin inline method checks are replaced by actionRequirePost() or requestInput()->isMethod('POST') + actionRequireCsrf() patterns.",
|
||||||
|
"result": "pass",
|
||||||
|
"evidence": "Direct requestInput()->method() usage in pages/admin was reduced to 0."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"criterion_id": "SC-002",
|
||||||
|
"criterion": "All remaining pages/admin inline Session::checkCsrfToken() checks are replaced by actionRequireCsrf().",
|
||||||
|
"result": "pass",
|
||||||
|
"evidence": "Direct Session::checkCsrfToken() usage in pages/admin was reduced to 0."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"criterion_id": "SC-003",
|
||||||
|
"criterion": "New architecture contract prevents reintroduction of direct requestInput()->method() and Session::checkCsrfToken() in pages/admin.",
|
||||||
|
"result": "pass",
|
||||||
|
"evidence": "AdminActionRequestGuardContractTest added and passing."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"criterion_id": "SC-004",
|
||||||
|
"criterion": "CSRF and PRG architecture tests remain green and authz admin contract suites remain green.",
|
||||||
|
"result": "pass",
|
||||||
|
"evidence": "PostEndpointCsrfContractTest, PostRedirectGetContractTest, and Authz admin suites pass."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"criterion_id": "SC-005",
|
||||||
|
"criterion": "Required quality gate set QG-001/002/003/006/008/009 passes.",
|
||||||
|
"result": "pass",
|
||||||
|
"evidence": "All required gates passed in this run."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"missing_or_wrong": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"task_id": "2026-04-24-action-csrf-centralization-step3",
|
||||||
|
"verdict": "pass",
|
||||||
|
"checked_guard_ids": [
|
||||||
|
"GR-CORE-003",
|
||||||
|
"GR-CORE-012",
|
||||||
|
"GR-TEST-001"
|
||||||
|
],
|
||||||
|
"checked_quality_gate_ids": [
|
||||||
|
"QG-001",
|
||||||
|
"QG-002",
|
||||||
|
"QG-003",
|
||||||
|
"QG-006",
|
||||||
|
"QG-008",
|
||||||
|
"QG-009"
|
||||||
|
],
|
||||||
|
"findings": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"task_id": "2026-04-24-action-csrf-centralization-step3",
|
||||||
|
"verdict": "pass",
|
||||||
|
"checked_guard_ids": [
|
||||||
|
"GR-SEC-001",
|
||||||
|
"GR-SEC-008",
|
||||||
|
"GR-SEC-009"
|
||||||
|
],
|
||||||
|
"findings": []
|
||||||
|
}
|
||||||
@@ -50,16 +50,22 @@ function flashFormErrors(FormErrors $errors, ?string $scope = null, string $keyP
|
|||||||
function actionRequirePost(
|
function actionRequirePost(
|
||||||
string $redirect,
|
string $redirect,
|
||||||
bool $jsonAware = false,
|
bool $jsonAware = false,
|
||||||
string $jsonError = 'method_not_allowed'
|
string $jsonError = 'method_not_allowed',
|
||||||
|
int $jsonStatusCode = 405,
|
||||||
|
array $jsonPayload = [],
|
||||||
|
bool $forceJson = false
|
||||||
): bool {
|
): bool {
|
||||||
if (requestInput()->isMethod('POST')) {
|
if (requestInput()->isMethod('POST')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($jsonAware && Request::wantsJson()) {
|
if ($jsonAware && ($forceJson || Request::wantsJson())) {
|
||||||
header('Allow: POST');
|
header('Allow: POST');
|
||||||
http_response_code(405);
|
http_response_code($jsonStatusCode);
|
||||||
Router::json(['error' => trim($jsonError) !== '' ? trim($jsonError) : 'method_not_allowed']);
|
$payload = $jsonPayload !== []
|
||||||
|
? $jsonPayload
|
||||||
|
: ['error' => trim($jsonError) !== '' ? trim($jsonError) : 'method_not_allowed'];
|
||||||
|
Router::json($payload);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,15 +85,21 @@ function actionRequireCsrf(
|
|||||||
bool $jsonAware = false,
|
bool $jsonAware = false,
|
||||||
string $jsonError = 'csrf',
|
string $jsonError = 'csrf',
|
||||||
bool $flashOnFailure = true,
|
bool $flashOnFailure = true,
|
||||||
bool $redirectOnFailure = true
|
bool $redirectOnFailure = true,
|
||||||
|
int $jsonStatusCode = 400,
|
||||||
|
array $jsonPayload = [],
|
||||||
|
bool $forceJson = false
|
||||||
): bool {
|
): bool {
|
||||||
if (Session::checkCsrfToken()) {
|
if (Session::checkCsrfToken()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($jsonAware && Request::wantsJson()) {
|
if ($jsonAware && ($forceJson || Request::wantsJson())) {
|
||||||
http_response_code(400);
|
http_response_code($jsonStatusCode);
|
||||||
Router::json(['error' => trim($jsonError) !== '' ? trim($jsonError) : 'csrf']);
|
$payload = $jsonPayload !== []
|
||||||
|
? $jsonPayload
|
||||||
|
: ['error' => trim($jsonError) !== '' ? trim($jsonError) : 'csrf'];
|
||||||
|
Router::json($payload);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ $hasImportPermission = static function (string $type) use ($currentUserId, $impo
|
|||||||
|
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
$stage = trim((string) $request->body('stage', ''));
|
$stage = trim((string) $request->body('stage', ''));
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!actionRequireCsrf('admin/imports', flashOnFailure: false, redirectOnFailure: false)) {
|
||||||
$errorBag->addGlobal(t('Form expired, please try again'));
|
$errorBag->addGlobal(t('Form expired, please try again'));
|
||||||
} elseif ($stage === 'analyze') {
|
} elseif ($stage === 'analyze') {
|
||||||
if ($selectedType === '' || !$hasImportPermission($selectedType)) {
|
if ($selectedType === '' || !$hasImportPermission($selectedType)) {
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
use MintyPHP\Service\Settings\SettingsSessionGateway;
|
use MintyPHP\Service\Settings\SettingsSessionGateway;
|
||||||
use MintyPHP\Session;
|
|
||||||
|
|
||||||
if ((requestInput()->method()) !== 'POST') {
|
if (!actionRequirePost(
|
||||||
http_response_code(405);
|
'admin',
|
||||||
Router::json(['ok' => false, 'error' => 'method_not_allowed']);
|
jsonAware: true,
|
||||||
|
jsonPayload: ['ok' => false, 'error' => 'method_not_allowed'],
|
||||||
|
forceJson: true
|
||||||
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,9 +22,15 @@ if ($currentUserId <= 0) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!actionRequireCsrf(
|
||||||
http_response_code(403);
|
'admin',
|
||||||
Router::json(['ok' => false, 'error' => 'csrf']);
|
jsonAware: true,
|
||||||
|
jsonStatusCode: 403,
|
||||||
|
jsonPayload: ['ok' => false, 'error' => 'csrf'],
|
||||||
|
flashOnFailure: false,
|
||||||
|
redirectOnFailure: false,
|
||||||
|
forceJson: true
|
||||||
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||||
use MintyPHP\Session;
|
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
if (requestInput()->method() !== 'POST') {
|
if (!actionRequirePost(
|
||||||
http_response_code(405);
|
'admin/tenants',
|
||||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
jsonAware: true,
|
||||||
|
jsonError: 'Method not allowed',
|
||||||
|
jsonPayload: ['ok' => false, 'error' => 'Method not allowed'],
|
||||||
|
forceJson: true
|
||||||
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!actionRequireCsrf(
|
||||||
http_response_code(403);
|
'admin/tenants',
|
||||||
echo json_encode(['ok' => false, 'error' => 'CSRF token invalid']);
|
jsonAware: true,
|
||||||
|
jsonError: 'CSRF token invalid',
|
||||||
|
jsonStatusCode: 403,
|
||||||
|
jsonPayload: ['ok' => false, 'error' => 'CSRF token invalid'],
|
||||||
|
flashOnFailure: false,
|
||||||
|
redirectOnFailure: false,
|
||||||
|
forceJson: true
|
||||||
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||||
use MintyPHP\Session;
|
|
||||||
use MintyPHP\Support\Flash;
|
use MintyPHP\Support\Flash;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ $user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
|||||||
if (!$user) {
|
if (!$user) {
|
||||||
Router::redirect('admin/users');
|
Router::redirect('admin/users');
|
||||||
}
|
}
|
||||||
|
$redirectTarget = "admin/users/edit/{$uuid}";
|
||||||
|
|
||||||
$userId = (int) ($user['id'] ?? 0);
|
$userId = (int) ($user['id'] ?? 0);
|
||||||
$currentUserId = (int) ($session['user']['id'] ?? 0);
|
$currentUserId = (int) ($session['user']['id'] ?? 0);
|
||||||
@@ -32,27 +32,29 @@ if (!$decision->isAllowed()) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!actionRequirePost($redirectTarget)) {
|
||||||
Flash::error(t('Form expired, please try again'), "admin/users/edit/{$uuid}", 'csrf_expired');
|
return;
|
||||||
Router::redirect("admin/users/edit/{$uuid}");
|
}
|
||||||
|
|
||||||
|
if (!actionRequireCsrf($redirectTarget, $redirectTarget, 'csrf_expired')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = trim((string) (requestInput()->bodyAll()['token_name'] ?? ''));
|
$name = trim((string) (requestInput()->bodyAll()['token_name'] ?? ''));
|
||||||
|
|
||||||
if ($name === '') {
|
if ($name === '') {
|
||||||
Flash::error(t('Token name is required'), "admin/users/edit/{$uuid}", 'api_token_error');
|
Flash::error(t('Token name is required'), $redirectTarget, 'api_token_error');
|
||||||
Router::redirect("admin/users/edit/{$uuid}");
|
Router::redirect($redirectTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $apiTokenService->create($userId, $name, null, null, $currentUserId);
|
$result = $apiTokenService->create($userId, $name, null, null, $currentUserId);
|
||||||
|
|
||||||
if ($result['ok'] ?? false) {
|
if ($result['ok'] ?? false) {
|
||||||
$sessionStore->set('api_token_created', ['token' => $result['token'], 'uuid' => $uuid]);
|
$sessionStore->set('api_token_created', ['token' => $result['token'], 'uuid' => $uuid]);
|
||||||
Flash::success(t('API token created. Copy it now — it will not be shown again.'), "admin/users/edit/{$uuid}", 'api_token_created');
|
Flash::success(t('API token created. Copy it now — it will not be shown again.'), $redirectTarget, 'api_token_created');
|
||||||
} else {
|
} else {
|
||||||
$error = $result['error'] ?? 'create_failed';
|
$error = $result['error'] ?? 'create_failed';
|
||||||
Flash::error(t('Could not create API token: %s', $error), "admin/users/edit/{$uuid}", 'api_token_error');
|
Flash::error(t('Could not create API token: %s', $error), $redirectTarget, 'api_token_error');
|
||||||
}
|
}
|
||||||
|
|
||||||
Router::redirect("admin/users/edit/{$uuid}");
|
Router::redirect($redirectTarget);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||||
use MintyPHP\Session;
|
|
||||||
use MintyPHP\Support\Flash;
|
use MintyPHP\Support\Flash;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
@@ -18,6 +17,7 @@ $user = $uuid !== '' ? $userAccountService->findByUuid($uuid) : null;
|
|||||||
if (!$user) {
|
if (!$user) {
|
||||||
Router::redirect('admin/users');
|
Router::redirect('admin/users');
|
||||||
}
|
}
|
||||||
|
$redirectTarget = "admin/users/edit/{$uuid}";
|
||||||
|
|
||||||
$userId = (int) ($user['id'] ?? 0);
|
$userId = (int) ($user['id'] ?? 0);
|
||||||
$currentUserId = (int) ($session['user']['id'] ?? 0);
|
$currentUserId = (int) ($session['user']['id'] ?? 0);
|
||||||
@@ -30,9 +30,11 @@ if (!$decision->isAllowed()) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!actionRequirePost($redirectTarget)) {
|
||||||
Flash::error(t('Form expired, please try again'), "admin/users/edit/{$uuid}", 'csrf_expired');
|
return;
|
||||||
Router::redirect("admin/users/edit/{$uuid}");
|
}
|
||||||
|
|
||||||
|
if (!actionRequireCsrf($redirectTarget, $redirectTarget, 'csrf_expired')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ $tokenId = (int) (requestInput()->bodyAll()['token_id'] ?? 0);
|
|||||||
|
|
||||||
if ($tokenId > 0) {
|
if ($tokenId > 0) {
|
||||||
$apiTokenService->revoke($tokenId);
|
$apiTokenService->revoke($tokenId);
|
||||||
Flash::success(t('API token revoked'), "admin/users/edit/{$uuid}", 'api_token_revoked');
|
Flash::success(t('API token revoked'), $redirectTarget, 'api_token_revoked');
|
||||||
}
|
}
|
||||||
|
|
||||||
Router::redirect("admin/users/edit/{$uuid}");
|
Router::redirect($redirectTarget);
|
||||||
|
|||||||
@@ -4,14 +4,29 @@ use MintyPHP\Http\SessionStoreInterface;
|
|||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||||
use MintyPHP\Service\User\UserAccessTemplateService;
|
use MintyPHP\Service\User\UserAccessTemplateService;
|
||||||
use MintyPHP\Session;
|
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
$session = app(SessionStoreInterface::class)->all();
|
$session = app(SessionStoreInterface::class)->all();
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
|
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!actionRequirePost(
|
||||||
Router::json(['ok' => false, 'error' => 'csrf_expired']);
|
'admin/users',
|
||||||
|
jsonAware: true,
|
||||||
|
jsonPayload: ['ok' => false, 'error' => 'method_not_allowed'],
|
||||||
|
forceJson: true
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actionRequireCsrf(
|
||||||
|
'admin/users',
|
||||||
|
jsonAware: true,
|
||||||
|
jsonStatusCode: 200,
|
||||||
|
jsonPayload: ['ok' => false, 'error' => 'csrf_expired'],
|
||||||
|
flashOnFailure: false,
|
||||||
|
redirectOnFailure: false,
|
||||||
|
forceJson: true
|
||||||
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use MintyPHP\Service\Access\UiAccessService;
|
|||||||
use MintyPHP\Service\Access\UiCapabilityMap;
|
use MintyPHP\Service\Access\UiCapabilityMap;
|
||||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||||
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||||
use MintyPHP\Session;
|
|
||||||
use MintyPHP\Support\Flash;
|
use MintyPHP\Support\Flash;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
@@ -99,9 +98,7 @@ $form = [
|
|||||||
'active' => 1,
|
'active' => 1,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
|
if ($request->isMethod('POST') && !actionRequireCsrf($createTarget, $createTarget, 'csrf_expired')) {
|
||||||
Flash::error(t('Form expired, please try again'), $createTarget, 'csrf_expired');
|
|
||||||
Router::redirect($createTarget);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use MintyPHP\Http\Request;
|
use MintyPHP\Http\Request;
|
||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
use MintyPHP\Session;
|
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
$sessionStore = app(SessionStoreInterface::class);
|
$sessionStore = app(SessionStoreInterface::class);
|
||||||
@@ -13,20 +12,17 @@ Guard::requireLogin();
|
|||||||
$userTenantContextService = app(\MintyPHP\Service\User\UserTenantContextService::class);
|
$userTenantContextService = app(\MintyPHP\Service\User\UserTenantContextService::class);
|
||||||
$errorBag = formErrors();
|
$errorBag = formErrors();
|
||||||
|
|
||||||
if ((requestInput()->method()) !== 'POST') {
|
if (!actionRequirePost('admin')) {
|
||||||
Router::redirect('admin');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!actionRequireCsrf(
|
||||||
if (Request::wantsJson()) {
|
'admin',
|
||||||
http_response_code(400);
|
'admin',
|
||||||
Router::json(['ok' => false, 'error' => 'csrf']);
|
'switch_tenant',
|
||||||
return;
|
jsonAware: true,
|
||||||
}
|
jsonPayload: ['ok' => false, 'error' => 'csrf']
|
||||||
$errorBag->addGlobal(t('Form expired, please try again'));
|
)) {
|
||||||
flashFormErrors($errorBag, 'admin', 'switch_tenant');
|
|
||||||
Router::redirect('admin');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use MintyPHP\Http\Request;
|
use MintyPHP\Http\Request;
|
||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
use MintyPHP\Session;
|
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
$sessionStore = app(SessionStoreInterface::class);
|
$sessionStore = app(SessionStoreInterface::class);
|
||||||
@@ -13,18 +12,16 @@ Guard::requireLogin();
|
|||||||
$userAccountService = app(\MintyPHP\Service\User\UserAccountService::class);
|
$userAccountService = app(\MintyPHP\Service\User\UserAccountService::class);
|
||||||
$errorBag = formErrors();
|
$errorBag = formErrors();
|
||||||
|
|
||||||
if ((requestInput()->method()) !== 'POST') {
|
if (!actionRequirePost('admin')) {
|
||||||
Router::redirect('admin');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Session::checkCsrfToken()) {
|
if (!actionRequireCsrf(
|
||||||
if (Request::wantsJson()) {
|
'admin',
|
||||||
http_response_code(400);
|
jsonAware: true,
|
||||||
Router::json(['ok' => false, 'error' => 'csrf']);
|
jsonPayload: ['ok' => false, 'error' => 'csrf'],
|
||||||
return;
|
flashOnFailure: false
|
||||||
}
|
)) {
|
||||||
Router::redirect('admin');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
53
tests/Architecture/AdminActionRequestGuardContractTest.php
Normal file
53
tests/Architecture/AdminActionRequestGuardContractTest.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Tests\Architecture;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
|
||||||
|
class AdminActionRequestGuardContractTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProjectFileAssertionSupport;
|
||||||
|
|
||||||
|
public function testAdminActionsDoNotInlineLegacyMethodChecks(): void
|
||||||
|
{
|
||||||
|
foreach ($this->adminActionFiles() as $file) {
|
||||||
|
$content = $this->readProjectFile($file);
|
||||||
|
$this->assertStringNotContainsString('requestInput()->method()', $content, $file);
|
||||||
|
$this->assertStringNotContainsString('(requestInput()->method())', $content, $file);
|
||||||
|
$this->assertStringNotContainsString("strtoupper((string) (requestInput()->method()))", $content, $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAdminActionsDoNotInlineLegacyCsrfChecks(): void
|
||||||
|
{
|
||||||
|
foreach ($this->adminActionFiles() as $file) {
|
||||||
|
$content = $this->readProjectFile($file);
|
||||||
|
$this->assertStringNotContainsString('Session::checkCsrfToken()', $content, $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function adminActionFiles(): array
|
||||||
|
{
|
||||||
|
$adminRoot = $this->projectRootPath() . '/pages/admin';
|
||||||
|
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($adminRoot));
|
||||||
|
$files = [];
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if (!$file->isFile() || $file->getExtension() !== 'php') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relativePath = 'pages/admin/' . ltrim(str_replace($adminRoot, '', $file->getPathname()), '/');
|
||||||
|
$files[] = $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
sort($files);
|
||||||
|
$this->assertNotEmpty($files, 'Expected admin action files in pages/admin.');
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user