major update
This commit is contained in:
@@ -4,16 +4,30 @@ namespace MintyPHP\Support;
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\PermissionGateway;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Service\Directory\DirectoryServicesFactory;
|
||||
use MintyPHP\Service\Access\AuthorizationDecision;
|
||||
use MintyPHP\Service\Access\AuthorizationService;
|
||||
use MintyPHP\Service\Auth\AuthService;
|
||||
use MintyPHP\Service\Tenant\TenantService;
|
||||
|
||||
class Guard
|
||||
{
|
||||
private const TENANT_REFRESH_INTERVAL = 60;
|
||||
private static ?AccessServicesFactory $accessServicesFactory = null;
|
||||
private static ?PermissionGateway $permissionGateway = null;
|
||||
/** @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
|
||||
{
|
||||
@@ -23,7 +37,7 @@ class Guard
|
||||
}
|
||||
self::refreshTenantContext();
|
||||
if (!empty($_SESSION['no_active_tenant'])) {
|
||||
(new AuthServicesFactory())->createAuthService()->logout();
|
||||
self::authService()->logout();
|
||||
Flash::error('No active tenant assigned', 'login', 'no_active_tenant');
|
||||
Router::redirect($redirect);
|
||||
}
|
||||
@@ -37,7 +51,7 @@ class Guard
|
||||
}
|
||||
|
||||
$tenantId = (int) $currentTenant['id'];
|
||||
$tenant = (new DirectoryServicesFactory())->createTenantService()->findById($tenantId);
|
||||
$tenant = self::tenantService()->findById($tenantId);
|
||||
if ($tenant) {
|
||||
return;
|
||||
}
|
||||
@@ -45,7 +59,7 @@ class Guard
|
||||
// Current tenant was deleted, refresh session data
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId > 0) {
|
||||
(new AuthServicesFactory())->createAuthService()->loadTenantDataIntoSession($userId);
|
||||
self::authService()->loadTenantDataIntoSession($userId);
|
||||
} else {
|
||||
unset($_SESSION['current_tenant']);
|
||||
}
|
||||
@@ -62,54 +76,114 @@ class Guard
|
||||
if ($last > 0 && ($now - $last) < self::TENANT_REFRESH_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
(new AuthServicesFactory())->createAuthService()->loadTenantDataIntoSession($userId);
|
||||
self::authService()->loadTenantDataIntoSession($userId);
|
||||
$_SESSION['tenant_context_refreshed_at'] = $now;
|
||||
}
|
||||
|
||||
public static function requirePermission(string $permissionKey, string $redirect = 'admin'): void
|
||||
public static function requireAbility(string $ability, string $redirect = 'admin', array $context = []): void
|
||||
{
|
||||
self::requireLogin();
|
||||
self::validateCurrentTenant();
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if (!$userId || !self::permissionGateway()->userHas($userId, $permissionKey)) {
|
||||
if (!self::isAllowedByAbility($userId, $ability, $context)) {
|
||||
Flash::error('Permission denied', $redirect, 'permission_denied');
|
||||
Router::redirect($redirect);
|
||||
}
|
||||
}
|
||||
|
||||
public static function requirePermissionOrForbidden(string $permissionKey): void
|
||||
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);
|
||||
if (!$userId || !self::permissionGateway()->userHas($userId, $permissionKey)) {
|
||||
$decision = self::authorizationService()->authorize($ability, [
|
||||
'actor_user_id' => $userId,
|
||||
...$context,
|
||||
]);
|
||||
if (!$decision->isAllowed()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
http_response_code($decision->status());
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'forbidden']);
|
||||
$error = trim($decision->error());
|
||||
echo json_encode(['error' => $error !== '' ? $error : 'forbidden']);
|
||||
exit;
|
||||
}
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
}
|
||||
|
||||
return $decision;
|
||||
}
|
||||
|
||||
private static function permissionGateway(): PermissionGateway
|
||||
public static function deny(string $redirect = 'error/forbidden'): void
|
||||
{
|
||||
if (self::$permissionGateway instanceof PermissionGateway) {
|
||||
return self::$permissionGateway;
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
self::$permissionGateway = self::accessServicesFactory()->createPermissionGateway();
|
||||
return self::$permissionGateway;
|
||||
if ($redirect === 'error/forbidden') {
|
||||
Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery()));
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect($redirect);
|
||||
}
|
||||
|
||||
private static function accessServicesFactory(): AccessServicesFactory
|
||||
private static function authorizationService(): AuthorizationService
|
||||
{
|
||||
if (self::$accessServicesFactory instanceof AccessServicesFactory) {
|
||||
return self::$accessServicesFactory;
|
||||
if (!is_callable(self::$authorizationServiceResolver)) {
|
||||
throw new \RuntimeException('Guard is not configured for dependency: ' . AuthorizationService::class);
|
||||
}
|
||||
$service = (self::$authorizationServiceResolver)();
|
||||
if (!$service instanceof AuthorizationService) {
|
||||
throw new \RuntimeException('Guard resolver returned invalid dependency: ' . AuthorizationService::class);
|
||||
}
|
||||
return $service;
|
||||
}
|
||||
|
||||
private static function authService(): AuthService
|
||||
{
|
||||
if (!is_callable(self::$authServiceResolver)) {
|
||||
throw new \RuntimeException('Guard is not configured for dependency: ' . AuthService::class);
|
||||
}
|
||||
$service = (self::$authServiceResolver)();
|
||||
if (!$service instanceof AuthService) {
|
||||
throw new \RuntimeException('Guard resolver returned invalid dependency: ' . AuthService::class);
|
||||
}
|
||||
return $service;
|
||||
}
|
||||
|
||||
private static function tenantService(): TenantService
|
||||
{
|
||||
if (!is_callable(self::$tenantServiceResolver)) {
|
||||
throw new \RuntimeException('Guard is not configured for dependency: ' . TenantService::class);
|
||||
}
|
||||
$service = (self::$tenantServiceResolver)();
|
||||
if (!$service instanceof TenantService) {
|
||||
throw new \RuntimeException('Guard resolver returned invalid dependency: ' . TenantService::class);
|
||||
}
|
||||
return $service;
|
||||
}
|
||||
|
||||
private static function isAllowedByAbility(int $userId, string $ability, array $context = []): bool
|
||||
{
|
||||
$ability = trim($ability);
|
||||
if ($userId <= 0 || $ability === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
self::$accessServicesFactory = new AccessServicesFactory();
|
||||
return self::$accessServicesFactory;
|
||||
$decision = self::authorizationService()->authorize($ability, [
|
||||
'actor_user_id' => $userId,
|
||||
...$context,
|
||||
]);
|
||||
|
||||
return $decision->isAllowed();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user