172 lines
5.5 KiB
PHP
172 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Service\Tenant;
|
|
|
|
use MintyPHP\Repository\Org\DepartmentRepositoryInterface;
|
|
use MintyPHP\Repository\Tenant\TenantRepositoryInterface;
|
|
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
|
|
use MintyPHP\Service\Access\PermissionService;
|
|
|
|
class TenantScopeService
|
|
{
|
|
public function __construct(
|
|
private readonly TenantRepositoryInterface $tenantRepository,
|
|
private readonly DepartmentRepositoryInterface $departmentRepository,
|
|
private readonly UserTenantRepositoryInterface $userTenantRepository,
|
|
private readonly PermissionService $permissionService
|
|
) {
|
|
}
|
|
|
|
// Strict mode is the default — permissive mode must be explicitly enabled via the constant.
|
|
public function isStrict(): bool
|
|
{
|
|
return defined('TENANT_SCOPE_STRICT') ? (bool) TENANT_SCOPE_STRICT : true;
|
|
}
|
|
|
|
public function getUserTenantIds(int $userId, bool $onlyActive = true): array
|
|
{
|
|
if ($userId <= 0) {
|
|
return [];
|
|
}
|
|
|
|
if ($this->canBypass($userId)) {
|
|
$ids = $this->tenantRepository->listIds();
|
|
} else {
|
|
$ids = array_values(array_unique(array_map(
|
|
'intval',
|
|
$this->userTenantRepository->listTenantIdsByUserId($userId)
|
|
)));
|
|
}
|
|
|
|
if ($onlyActive) {
|
|
return $this->filterActiveTenantIds($ids);
|
|
}
|
|
|
|
return $ids;
|
|
}
|
|
|
|
public function canAccess(string $resource, int $resourceId, int $userId): bool
|
|
{
|
|
if ($resourceId <= 0 || $userId <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->canBypass($userId)) {
|
|
return true;
|
|
}
|
|
|
|
$resourceTenantIdsRaw = $this->getResourceTenantIds($resource, $resourceId);
|
|
$userTenantIdsRaw = $this->getUserTenantIds($userId, false);
|
|
$resourceTenantIds = $this->filterActiveTenantIds($resourceTenantIdsRaw);
|
|
$userTenantIds = $this->filterActiveTenantIds($userTenantIdsRaw);
|
|
|
|
// Resource had tenants, but all are inactive — deny access rather than treating it as unscoped.
|
|
if ($resourceTenantIdsRaw && !$resourceTenantIds) {
|
|
return false;
|
|
}
|
|
// Resource has no tenant assignment — allow in permissive mode, deny in strict mode.
|
|
if (!$resourceTenantIds) {
|
|
return $this->isStrict() ? false : true;
|
|
}
|
|
if (!$userTenantIds) {
|
|
return false;
|
|
}
|
|
|
|
return (bool) array_intersect($resourceTenantIds, $userTenantIds);
|
|
}
|
|
|
|
public function filterTenantIdsForUser(array $tenantIds, int $userId): array
|
|
{
|
|
$tenantIds = array_values(array_unique(array_map('intval', $tenantIds)));
|
|
$userTenantIds = $this->getUserTenantIds($userId);
|
|
if (!$userTenantIds) {
|
|
return [];
|
|
}
|
|
|
|
return array_values(array_intersect($tenantIds, $userTenantIds));
|
|
}
|
|
|
|
public function mergeTenantIdsPreservingOutOfScope(
|
|
array $requestedTenantIds,
|
|
array $existingTenantIds,
|
|
array $allowedTenantIds
|
|
): array {
|
|
$requestedTenantIds = array_values(array_unique(array_map('intval', $requestedTenantIds)));
|
|
$existingTenantIds = array_values(array_unique(array_map('intval', $existingTenantIds)));
|
|
$allowedTenantIds = array_values(array_unique(array_map('intval', $allowedTenantIds)));
|
|
|
|
if (!$allowedTenantIds) {
|
|
return $existingTenantIds;
|
|
}
|
|
|
|
$outOfScope = array_values(array_diff($existingTenantIds, $allowedTenantIds));
|
|
return array_values(array_unique(array_merge($requestedTenantIds, $outOfScope)));
|
|
}
|
|
|
|
public function hasGlobalAccess(int $userId): bool
|
|
{
|
|
return $this->canBypass($userId);
|
|
}
|
|
|
|
public function resourceBelongsToTenant(string $resource, int $resourceId, int $tenantId): bool
|
|
{
|
|
if ($resourceId <= 0 || $tenantId <= 0) {
|
|
return false;
|
|
}
|
|
|
|
$resourceTenantIds = $this->getResourceTenantIds($resource, $resourceId);
|
|
if (!$resourceTenantIds) {
|
|
return false;
|
|
}
|
|
|
|
$activeResourceTenantIds = $this->filterActiveTenantIds($resourceTenantIds);
|
|
if (!$activeResourceTenantIds) {
|
|
return false;
|
|
}
|
|
|
|
return in_array($tenantId, $activeResourceTenantIds, true);
|
|
}
|
|
|
|
private function getResourceTenantIds(string $resource, int $resourceId): array
|
|
{
|
|
$resource = strtolower(trim($resource));
|
|
if ($resource === 'tenants') {
|
|
return $resourceId > 0 ? [$resourceId] : [];
|
|
}
|
|
|
|
if ($resource === 'users') {
|
|
return array_values(array_unique(array_map(
|
|
'intval',
|
|
$this->userTenantRepository->listTenantIdsByUserId($resourceId)
|
|
)));
|
|
}
|
|
|
|
if ($resource === 'departments') {
|
|
$department = $this->departmentRepository->find($resourceId);
|
|
$tenantId = (int) ($department['tenant_id'] ?? 0);
|
|
return $tenantId > 0 ? [$tenantId] : [];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private function filterActiveTenantIds(array $tenantIds): array
|
|
{
|
|
$tenantIds = array_values(array_unique(array_map('intval', $tenantIds)));
|
|
if (!$tenantIds) {
|
|
return [];
|
|
}
|
|
|
|
return $this->tenantRepository->listActiveIdsByIds($tenantIds);
|
|
}
|
|
|
|
private function canBypass(int $userId): bool
|
|
{
|
|
if ($userId <= 0) {
|
|
return false;
|
|
}
|
|
|
|
return $this->permissionService->userHas($userId, PermissionService::TENANT_SCOPE_GLOBAL);
|
|
}
|
|
}
|