Files
breadcrumb-the-shire/core/Support/Guard.php
fs 3f0db68b27 refactor: fix all 105 PHPStan 2 strict type findings across core and modules
Resolve all non-false-positive PHPStan 2 findings, reducing the baseline
from 486 to 382 entries (only unused-public false positives remain).

Categories fixed:
- Remove 39 redundant type checks (is_string, is_array, is_object, method_exists)
- Remove 12 no-op array_values() on already-sequential lists
- Simplify 13 null-coalesce on guaranteed offsets
- Remove 8 always-true instanceof checks
- Replace 12 redundant test assertions (assertTrue(true) → addToAssertionCount)
- Simplify 6 unnecessary nullsafe operators (?-> → ->)
- Fix 6 always-true !== '' comparisons
- Fix remaining: unset.offset, return.unusedType, staticMethod narrowing

53 files changed across core/, modules/, pages/, and tests/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:54:20 +02:00

182 lines
6.4 KiB
PHP

<?php
namespace MintyPHP\Support;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Service\Access\AuthorizationDecision;
use MintyPHP\Service\Access\AuthorizationService;
use MintyPHP\Service\Auth\AuthService;
use MintyPHP\Service\Tenant\TenantService;
// Session-based web auth guard. Enforces login and RBAC for page actions.
// For API token authentication, see ApiAuth instead.
class Guard
{
// Re-check tenant assignment at most once per minute to pick up permission changes without re-login.
private const TENANT_REFRESH_INTERVAL = 60;
/** @var (callable(): AuthService)|null */
private static $authServiceResolver = null;
/** @var (callable(): TenantService)|null */
private static $tenantServiceResolver = null;
/** @var (callable(): AuthorizationService)|null */
private static $authorizationServiceResolver = null;
public static function configure(
callable $authServiceResolver,
callable $tenantServiceResolver,
callable $authorizationServiceResolver
): void {
self::$authServiceResolver = $authServiceResolver;
self::$tenantServiceResolver = $tenantServiceResolver;
self::$authorizationServiceResolver = $authorizationServiceResolver;
}
public static function requireLogin(string $redirect = 'login'): void
{
if (!isset($_SESSION['user'])) {
Flash::error('Login required', 'login', 'login_required');
Router::redirect($redirect);
}
self::refreshTenantContext();
if (!empty($_SESSION['no_active_tenant'])) {
self::authService()->logout();
Flash::error('No active tenant assigned', 'login', 'no_active_tenant');
Router::redirect($redirect);
}
}
private static function validateCurrentTenant(): void
{
$currentTenant = $_SESSION['current_tenant'] ?? null;
if (!$currentTenant || empty($currentTenant['id'])) {
return;
}
$tenantId = (int) $currentTenant['id'];
$tenant = self::tenantService()->findById($tenantId);
if ($tenant) {
return;
}
// Current tenant was deleted — reload session so the user is switched to another active tenant or logged out.
$userId = (int) ($_SESSION['user']['id'] ?? 0);
if ($userId > 0) {
self::authService()->loadTenantDataIntoSession($userId);
} else {
unset($_SESSION['current_tenant']);
}
}
private static function refreshTenantContext(): void
{
$userId = (int) ($_SESSION['user']['id'] ?? 0);
if ($userId <= 0) {
return;
}
$last = (int) ($_SESSION['tenant_context_refreshed_at'] ?? 0);
$now = time();
if ($last > 0 && ($now - $last) < self::TENANT_REFRESH_INTERVAL) {
return;
}
self::authService()->loadTenantDataIntoSession($userId);
$_SESSION['tenant_context_refreshed_at'] = $now;
}
public static function requireAbility(string $ability, string $redirect = 'admin', array $context = []): void
{
self::requireLogin();
self::validateCurrentTenant();
$userId = (int) ($_SESSION['user']['id'] ?? 0);
if (!self::isAllowedByAbility($userId, $ability, $context)) {
Flash::error('Permission denied', $redirect, 'permission_denied');
Router::redirect($redirect);
}
}
public static function requireAbilityOrForbidden(string $ability, array $context = []): void
{
self::requireAbilityDecisionOrForbidden($ability, $context);
}
public static function requireAbilityDecisionOrForbidden(string $ability, array $context = []): AuthorizationDecision
{
self::requireLogin();
self::validateCurrentTenant();
$userId = (int) ($_SESSION['user']['id'] ?? 0);
$decision = self::authorizationService()->authorize($ability, [
'actor_user_id' => $userId,
...$context,
]);
if (!$decision->isAllowed()) {
// JSON requests get a 403/404 response body; browser requests get a redirect to the forbidden page.
if (Request::wantsJson()) {
http_response_code($decision->status());
header('Content-Type: application/json; charset=utf-8');
$error = trim($decision->error());
echo json_encode(['error' => $error !== '' ? $error : 'forbidden']);
exit;
}
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
}
return $decision;
}
public static function deny(string $redirect = 'error/forbidden'): void
{
if (Request::wantsJson()) {
http_response_code(403);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'forbidden']);
exit;
}
if ($redirect === 'error/forbidden') {
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
return;
}
Router::redirect($redirect);
}
private static function authorizationService(): AuthorizationService
{
if (!is_callable(self::$authorizationServiceResolver)) {
throw new \RuntimeException('Guard is not configured for dependency: ' . AuthorizationService::class);
}
return (self::$authorizationServiceResolver)();
}
private static function authService(): AuthService
{
if (!is_callable(self::$authServiceResolver)) {
throw new \RuntimeException('Guard is not configured for dependency: ' . AuthService::class);
}
return (self::$authServiceResolver)();
}
private static function tenantService(): TenantService
{
if (!is_callable(self::$tenantServiceResolver)) {
throw new \RuntimeException('Guard is not configured for dependency: ' . TenantService::class);
}
return (self::$tenantServiceResolver)();
}
private static function isAllowedByAbility(int $userId, string $ability, array $context = []): bool
{
$ability = trim($ability);
if ($userId <= 0 || $ability === '') {
return false;
}
$decision = self::authorizationService()->authorize($ability, [
'actor_user_id' => $userId,
...$context,
]);
return $decision->isAllowed();
}
}