Files
breadcrumb-the-shire/core/Service/User/UserAssignmentService.php

324 lines
12 KiB
PHP
Raw Normal View History

2026-02-23 12:58:19 +01:00
<?php
namespace MintyPHP\Service\User;
2026-03-05 08:26:51 +01:00
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
use MintyPHP\Repository\Org\UserDepartmentRepositoryInterface;
2026-03-04 15:56:58 +01:00
use MintyPHP\Repository\Support\DatabaseSessionRepository;
2026-03-05 08:26:51 +01:00
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
2026-03-13 21:11:48 +01:00
use MintyPHP\Service\Access\AssignableRoleService;
use MintyPHP\Service\Access\PermissionService;
2026-02-23 12:58:19 +01:00
class UserAssignmentService
{
public function __construct(
2026-03-05 08:26:51 +01:00
private readonly UserWriteRepositoryInterface $userWriteRepository,
private readonly UserTenantRepositoryInterface $userTenantRepository,
private readonly UserRoleRepositoryInterface $userRoleRepository,
private readonly UserDepartmentRepositoryInterface $userDepartmentRepository,
2026-02-23 12:58:19 +01:00
private readonly UserDirectoryGateway $directoryGateway,
private readonly PermissionService $permissionService,
2026-03-13 21:11:48 +01:00
private readonly DatabaseSessionRepository $databaseSessionRepository,
private readonly AssignableRoleService $assignableRoleService
2026-02-23 12:58:19 +01:00
) {
}
public function buildAssignmentsForUser(int $userId): array
{
if ($userId <= 0) {
return [
'tenants' => [],
'departments' => [],
'roles' => [],
];
}
$tenantIds = $this->userTenantRepository->listTenantIdsByUserId($userId);
$departmentIds = $this->userDepartmentRepository->listDepartmentIdsByUserId($userId);
$roleIds = $this->userRoleRepository->listRoleIdsByUserId($userId);
$tenantsById = [];
foreach ($this->directoryGateway->listTenantsByIds($tenantIds) as $tenant) {
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0) {
continue;
}
$tenantsById[$tenantId] = [
'id' => $tenantId,
'uuid' => (string) ($tenant['uuid'] ?? ''),
'description' => (string) ($tenant['description'] ?? ''),
'status' => (string) ($tenant['status'] ?? ''),
];
}
$departmentsById = [];
foreach ($this->directoryGateway->listDepartmentsByIds($departmentIds, true) as $department) {
$departmentId = (int) ($department['id'] ?? 0);
if ($departmentId <= 0) {
continue;
}
$departmentTenantId = (int) ($department['tenant_id'] ?? 0);
$departmentsById[$departmentId] = [
'id' => $departmentId,
'uuid' => (string) ($department['uuid'] ?? ''),
'description' => (string) ($department['description'] ?? ''),
'active' => (bool) ($department['active'] ?? 0),
'tenant_id' => $departmentTenantId,
'tenant_uuid' => (string) (($tenantsById[$departmentTenantId]['uuid'] ?? '')),
];
}
$rolesById = [];
foreach ($this->directoryGateway->listRolesByIds($roleIds) as $role) {
$roleId = (int) ($role['id'] ?? 0);
if ($roleId <= 0) {
continue;
}
$rolesById[$roleId] = [
'id' => $roleId,
'uuid' => (string) ($role['uuid'] ?? ''),
'description' => (string) ($role['description'] ?? ''),
'active' => (bool) ($role['active'] ?? 0),
];
}
$tenants = [];
foreach ($tenantIds as $tenantId) {
if (isset($tenantsById[$tenantId])) {
$tenants[] = $tenantsById[$tenantId];
}
}
$departments = [];
foreach ($departmentIds as $departmentId) {
if (isset($departmentsById[$departmentId])) {
$departments[] = $departmentsById[$departmentId];
}
}
$roles = [];
foreach ($roleIds as $roleId) {
if (isset($rolesById[$roleId])) {
$roles[] = $rolesById[$roleId];
}
}
return [
'tenants' => $tenants,
'departments' => $departments,
'roles' => $roles,
];
}
public function findTenantSummaryInAssignments(array $assignments, int $tenantId): ?array
{
if ($tenantId <= 0) {
return null;
}
foreach (($assignments['tenants'] ?? []) as $tenant) {
$currentTenantId = (int) ($tenant['id'] ?? 0);
if ($currentTenantId !== $tenantId) {
continue;
}
return [
'uuid' => (string) ($tenant['uuid'] ?? ''),
'description' => (string) ($tenant['description'] ?? ''),
'status' => (string) ($tenant['status'] ?? ''),
];
}
return null;
}
public function mapAssignmentsToPublic(array $assignments): array
{
return [
'tenants' => array_values(array_map(
static fn (array $tenant): array => [
'uuid' => (string) ($tenant['uuid'] ?? ''),
'description' => (string) ($tenant['description'] ?? ''),
'status' => (string) ($tenant['status'] ?? ''),
],
$assignments['tenants'] ?? []
)),
'departments' => array_values(array_map(
static fn (array $department): array => [
'uuid' => (string) ($department['uuid'] ?? ''),
'description' => (string) ($department['description'] ?? ''),
'active' => (bool) ($department['active'] ?? 0),
'tenant_uuid' => (string) ($department['tenant_uuid'] ?? ''),
],
$assignments['departments'] ?? []
)),
'roles' => array_values(array_map(
static fn (array $role): array => [
'uuid' => (string) ($role['uuid'] ?? ''),
'description' => (string) ($role['description'] ?? ''),
'active' => (bool) ($role['active'] ?? 0),
],
$assignments['roles'] ?? []
)),
];
}
public function syncTenants(int $userId, array $tenantIds, bool $bumpAuthz = true): bool
{
$ids = $this->normalizeTenantIds($tenantIds);
$result = $this->userTenantRepository->replaceForUser($userId, $ids);
if ($result && $bumpAuthz) {
$this->bumpAuthzVersion($userId);
}
return $result;
}
2026-03-13 21:11:48 +01:00
public function syncRoles(int $userId, array $roleIds, bool $bumpAuthz = true, int $actorUserId = 0): bool
2026-02-23 12:58:19 +01:00
{
$ids = toIntIds($roleIds);
2026-02-23 12:58:19 +01:00
$validIds = $this->directoryGateway->listActiveRoleIds();
if ($validIds) {
$validMap = array_fill_keys($validIds, true);
$ids = array_values(array_filter($ids, static fn ($id) => isset($validMap[$id])));
}
2026-03-13 21:11:48 +01:00
// Enforce assignable-roles: the actor can only touch roles within their assignable set.
// Roles outside the set are "frozen" and remain as-is on the target user.
if ($actorUserId > 0) {
$currentRoleIds = $this->userRoleRepository->listRoleIdsByUserId($userId);
$ids = $this->assignableRoleService->computeEffectiveRoleIds($actorUserId, $ids, $currentRoleIds);
}
2026-02-23 12:58:19 +01:00
$result = $this->userRoleRepository->replaceForUser($userId, $ids);
$this->permissionService->clearUserCache($userId);
2026-02-23 12:58:19 +01:00
if ($result && $bumpAuthz) {
$this->bumpAuthzVersion($userId);
}
return $result;
}
public function syncDepartments(int $userId, array $departmentIds, bool $bumpAuthz = true): bool
{
$ids = toIntIds($departmentIds);
2026-02-23 12:58:19 +01:00
// Important: use the user's assigned tenants directly (no permission bypass),
// otherwise privileged users (e.g. admins) would incorrectly allow departments
// from tenants that were just removed from their assignments.
$userTenantIds = toIntIds($this->userTenantRepository->listTenantIdsByUserId($userId));
2026-02-23 12:58:19 +01:00
if (!$userTenantIds) {
$ids = [];
} else {
$allowedIds = $this->directoryGateway->listActiveDepartmentIdsByTenantIds($userTenantIds);
if ($allowedIds) {
$allowedMap = array_fill_keys($allowedIds, true);
$ids = array_values(array_filter($ids, static fn ($id) => isset($allowedMap[$id])));
} else {
$ids = [];
}
}
$result = $this->userDepartmentRepository->replaceForUser($userId, $ids);
if ($result && $bumpAuthz) {
$this->bumpAuthzVersion($userId);
}
return $result;
}
2026-03-06 00:44:52 +01:00
// Incrementing authz_version causes the session to be re-checked on the user's next request.
2026-02-23 12:58:19 +01:00
public function bumpAuthzVersion(int $userId): void
{
if ($userId <= 0) {
return;
}
$this->userWriteRepository->bumpAuthzVersion($userId);
}
2026-03-13 21:11:48 +01:00
public function syncAllAssignments(int $userId, array $tenantIds, array $roleIds, array $departmentIds, int $actorUserId = 0): bool
2026-03-04 15:56:58 +01:00
{
if ($userId <= 0) {
return false;
}
$transactionStarted = false;
try {
$this->databaseSessionRepository->beginTransaction();
$transactionStarted = true;
2026-03-06 00:44:52 +01:00
// bumpAuthz=false on each sync — bump once at the end to avoid redundant DB writes.
2026-03-04 15:56:58 +01:00
$ok = $this->syncTenants($userId, $tenantIds, false)
2026-03-13 21:11:48 +01:00
&& $this->syncRoles($userId, $roleIds, false, $actorUserId)
2026-03-04 15:56:58 +01:00
&& $this->syncDepartments($userId, $departmentIds, false);
if (!$ok) {
$this->rollbackQuietly();
return false;
}
$this->bumpAuthzVersion($userId);
$this->databaseSessionRepository->commitTransaction();
$transactionStarted = false;
return true;
} catch (\Throwable $exception) {
if ($transactionStarted) {
$this->rollbackQuietly();
}
return false;
}
}
2026-03-06 00:44:52 +01:00
// Accepts arrays, comma-separated strings, and nested arrays — normalizes to a flat list of IDs.
2026-02-23 12:58:19 +01:00
public function normalizeIdInput($value): array
{
$raw = is_array($value) ? $value : [$value];
$flat = [];
$collect = static function ($item) use (&$collect, &$flat): void {
if (is_array($item)) {
foreach ($item as $nested) {
$collect($nested);
}
return;
}
$text = trim((string) $item);
if ($text === '') {
return;
}
if (str_contains($text, ',')) {
foreach (explode(',', $text) as $part) {
$part = trim($part);
if ($part !== '') {
$flat[] = $part;
}
}
return;
}
$flat[] = $text;
};
foreach ($raw as $item) {
$collect($item);
}
return toIntIds($flat);
2026-02-23 12:58:19 +01:00
}
public function normalizeTenantIds(array $tenantIds): array
{
$ids = toIntIds($tenantIds);
2026-02-23 12:58:19 +01:00
$validIds = $this->directoryGateway->listTenantIds();
if ($validIds) {
$validMap = array_fill_keys($validIds, true);
$ids = array_values(array_filter($ids, static fn ($id) => isset($validMap[$id])));
}
return $ids;
}
2026-03-04 15:56:58 +01:00
private function rollbackQuietly(): void
{
try {
$this->databaseSessionRepository->rollbackTransaction();
} catch (\Throwable $exception) {
// no-op
}
}
2026-02-23 12:58:19 +01:00
}