forked from fa/breadcrumb-the-shire
189 lines
5.8 KiB
PHP
189 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Http;
|
|
|
|
use MintyPHP\Service\Access\AccessServicesFactory;
|
|
use MintyPHP\Service\Auth\AuthScopeGateway;
|
|
use MintyPHP\Service\Auth\AuthServicesFactory;
|
|
use MintyPHP\Service\User\UserServicesFactory;
|
|
|
|
class ApiAuth
|
|
{
|
|
private static ?array $currentUser = null;
|
|
private static ?array $currentPermissions = null;
|
|
private static ?int $currentTenantId = null;
|
|
private static ?int $tokenTenantId = null;
|
|
private static ?array $currentTokenRecord = null;
|
|
private static bool $authenticated = false;
|
|
|
|
/**
|
|
* Extract the full Bearer token from the Authorization header.
|
|
*/
|
|
public static function extractBearerToken(): string
|
|
{
|
|
$header = trim((string) ($_SERVER['HTTP_AUTHORIZATION'] ?? ''));
|
|
if ($header === '') {
|
|
$header = trim((string) ($_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''));
|
|
}
|
|
if ($header === '' || stripos($header, 'Bearer ') !== 0) {
|
|
return '';
|
|
}
|
|
|
|
return trim(substr($header, 7));
|
|
}
|
|
|
|
/**
|
|
* Attempt to authenticate from Authorization: Bearer header.
|
|
*/
|
|
public static function authenticate(): bool
|
|
{
|
|
self::$currentUser = null;
|
|
self::$currentPermissions = null;
|
|
self::$currentTenantId = null;
|
|
self::$tokenTenantId = null;
|
|
self::$currentTokenRecord = null;
|
|
self::$authenticated = false;
|
|
|
|
$bearerToken = self::extractBearerToken();
|
|
if ($bearerToken === '') {
|
|
return false;
|
|
}
|
|
|
|
$authServicesFactory = new AuthServicesFactory();
|
|
$scopeGateway = $authServicesFactory->createAuthScopeGateway();
|
|
$result = $authServicesFactory->createApiTokenService()->validate($bearerToken);
|
|
if ($result === null) {
|
|
return false;
|
|
}
|
|
|
|
$user = $result['user'];
|
|
$userId = (int) ($user['id'] ?? 0);
|
|
$userServicesFactory = new UserServicesFactory();
|
|
$userRoleRepository = $userServicesFactory->createUserRoleRepository();
|
|
$rolePermissionRepository = (new AccessServicesFactory())->createRolePermissionRepository();
|
|
$userTenantContextService = $userServicesFactory->createUserTenantContextService();
|
|
|
|
// Load permissions directly (bypass session cache)
|
|
$roleIds = $userRoleRepository->listRoleIdsByUserId($userId);
|
|
$permissions = $rolePermissionRepository->listPermissionKeysByRoleIds($roleIds);
|
|
|
|
// Resolve tenant context
|
|
$tokenTenantId = $result['tenant_id'];
|
|
if ($tokenTenantId !== null) {
|
|
$userTenantIds = $scopeGateway->getUserTenantIds($userId);
|
|
if (!in_array($tokenTenantId, $userTenantIds, true)) {
|
|
return false;
|
|
}
|
|
$currentTenantId = $tokenTenantId;
|
|
self::$tokenTenantId = $tokenTenantId;
|
|
} else {
|
|
$currentTenantId = $userTenantContextService->getCurrentTenantId($userId);
|
|
self::$tokenTenantId = null;
|
|
}
|
|
|
|
self::$currentUser = $user;
|
|
self::$currentPermissions = $permissions;
|
|
self::$currentTenantId = $currentTenantId;
|
|
self::$currentTokenRecord = $result['token_record'];
|
|
self::$authenticated = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static function isAuthenticated(): bool
|
|
{
|
|
return self::$authenticated;
|
|
}
|
|
|
|
public static function user(): ?array
|
|
{
|
|
return self::$currentUser;
|
|
}
|
|
|
|
public static function userId(): int
|
|
{
|
|
return (int) (self::$currentUser['id'] ?? 0);
|
|
}
|
|
|
|
public static function permissions(): array
|
|
{
|
|
return self::$currentPermissions ?? [];
|
|
}
|
|
|
|
public static function hasPermission(string $key): bool
|
|
{
|
|
return in_array($key, self::$currentPermissions ?? [], true);
|
|
}
|
|
|
|
public static function tenantId(): ?int
|
|
{
|
|
return self::$currentTenantId;
|
|
}
|
|
|
|
public static function scopedTenantId(): ?int
|
|
{
|
|
return self::$tokenTenantId;
|
|
}
|
|
|
|
public static function tokenRecord(): ?array
|
|
{
|
|
return self::$currentTokenRecord;
|
|
}
|
|
|
|
public static function tokenId(): ?int
|
|
{
|
|
$id = (int) (self::$currentTokenRecord['id'] ?? 0);
|
|
return $id > 0 ? $id : null;
|
|
}
|
|
|
|
public static function isTenantScopedToken(): bool
|
|
{
|
|
return (self::$tokenTenantId ?? 0) > 0;
|
|
}
|
|
|
|
/**
|
|
* Require both tenant-scope and token-tenant access for a resource.
|
|
*/
|
|
public static function requireResourceAccess(string $resource, int $resourceId): void
|
|
{
|
|
$scopeGateway = (new AuthServicesFactory())->createAuthScopeGateway();
|
|
if (!$scopeGateway->canAccess($resource, $resourceId, self::userId())) {
|
|
ApiResponse::notFound();
|
|
}
|
|
self::requireTokenTenantAccess($resource, $resourceId);
|
|
}
|
|
|
|
public static function requireTokenTenantAccess(string $resource, int $resourceId): void
|
|
{
|
|
if (!self::isTenantScopedToken()) {
|
|
return;
|
|
}
|
|
|
|
$tenantId = (int) (self::$tokenTenantId ?? 0);
|
|
if ($tenantId <= 0) {
|
|
ApiResponse::forbidden();
|
|
}
|
|
|
|
$scopeGateway = (new AuthServicesFactory())->createAuthScopeGateway();
|
|
if (!$scopeGateway->resourceBelongsToTenant($resource, $resourceId, $tenantId)) {
|
|
ApiResponse::notFound();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the current user can self-manage API tokens.
|
|
*/
|
|
public static function canSelfManageTokens(): bool
|
|
{
|
|
return self::hasPermission(\MintyPHP\Service\Access\PermissionService::USERS_SELF_UPDATE)
|
|
|| self::hasPermission(\MintyPHP\Service\Access\PermissionService::API_TOKENS_MANAGE);
|
|
}
|
|
|
|
public static function requireSelfManageTokens(): void
|
|
{
|
|
if (!self::canSelfManageTokens()) {
|
|
ApiResponse::forbidden('api_tokens_self_manage_forbidden');
|
|
}
|
|
}
|
|
}
|