140 lines
5.9 KiB
PHP
140 lines
5.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Domain\User;
|
|
|
|
require_once __DIR__ . '/PermissionMatrix.php';
|
|
require_once __DIR__ . '/RoleRecord.php';
|
|
|
|
/** PDO-independent contracts for custom-role permission assignment. */
|
|
final class RolePermissionService
|
|
{
|
|
public function __construct(
|
|
private readonly ?RoleRecord $roles = null,
|
|
private readonly ?PermissionMatrix $permissions = null,
|
|
) {
|
|
}
|
|
|
|
/** @return array{role_id: int, permissions: list<string>, valid: bool, errors: array<string, string>} */
|
|
public function validate(array $role, array $selected, array $available = []): array
|
|
{
|
|
return $this->validateAssignment($role, $selected, $available);
|
|
}
|
|
|
|
/**
|
|
* Validate and normalize a role's selected permissions. When an available
|
|
* list is supplied, selections outside that list are rejected rather than
|
|
* silently discarded.
|
|
*
|
|
* @return array{role_id: int, permissions: list<string>, valid: bool, errors: array<string, string>}
|
|
*/
|
|
public function validateAssignment(array $role, array $selected, array $available = []): array
|
|
{
|
|
$roleId = $this->positiveId($role['id'] ?? $role['role_id'] ?? null);
|
|
$normalized = ($this->permissions ?? new PermissionMatrix())->normalize($selected);
|
|
$errors = [];
|
|
if ($roleId === null) $errors['role_id'] = 'Role ID must be a positive integer.';
|
|
if (($this->roles ?? new RoleRecord())->isAdministrator($role)) {
|
|
$errors['role'] = 'The protected Administrator role permissions cannot be changed.';
|
|
}
|
|
|
|
if ($available !== []) {
|
|
$allowed = ($this->permissions ?? new PermissionMatrix())->normalize($available);
|
|
$unknown = array_values(array_diff($normalized, $allowed));
|
|
if ($unknown !== []) {
|
|
$errors['permissions'] = 'Unknown permissions cannot be assigned: ' . implode(', ', $unknown) . '.';
|
|
}
|
|
}
|
|
|
|
return ['role_id' => $roleId ?? 0, 'permissions' => $normalized, 'valid' => $errors === [], 'errors' => $errors];
|
|
}
|
|
|
|
/** Validate creation of a custom role and its optional permission set. */
|
|
public function validateForCreate(array $input, array $available = []): array
|
|
{
|
|
$record = $this->roles ?? new RoleRecord();
|
|
$result = $record->validate($input);
|
|
if ($record->isAdministrator($input)) {
|
|
$result['errors']['role'] = 'The protected Administrator role cannot be created or renamed.';
|
|
}
|
|
if (array_key_exists('permissions', $input)) {
|
|
$assignment = $this->validateAssignment(['id' => 2, 'name' => $result['name']], is_array($input['permissions']) ? $input['permissions'] : [], $available);
|
|
$result['permissions'] = $assignment['permissions'];
|
|
$result['errors'] = [...$result['errors'], ...$assignment['errors']];
|
|
}
|
|
$result['valid'] = $result['errors'] === [];
|
|
return $result;
|
|
}
|
|
|
|
/** @return array{role_id: int, permissions: list<string>, valid: bool, errors: array<string, string>} */
|
|
public function validateForAssignment(array $role, array $selected, array $available = []): array
|
|
{
|
|
return $this->validateAssignment($role, $selected, $available);
|
|
}
|
|
|
|
/**
|
|
* Validate a role edit and its optional permission set in one safe result.
|
|
* Administrator cannot be renamed or have permissions changed.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function validateForEdit(int $id, array $input, array $available = []): array
|
|
{
|
|
$record = $this->roles ?? new RoleRecord();
|
|
$result = $record->validate($input);
|
|
$result['id'] = $id;
|
|
if ($id < 1) $result['errors']['id'] = 'Role ID must be a positive integer.';
|
|
$current = ['id' => $id, 'name' => $id === 1 ? 'Administrator' : ($input['current_name'] ?? ($input['name'] ?? null))];
|
|
if (!$record->canRename($current, $result['name'])) $result['errors']['role'] = 'The protected Administrator role cannot be renamed.';
|
|
if (array_key_exists('permissions', $input)) {
|
|
$assignment = $this->validateAssignment(['id' => $id, 'name' => $current['name']], is_array($input['permissions']) ? $input['permissions'] : [], $available);
|
|
$result['permissions'] = $assignment['permissions'];
|
|
$result['errors'] = [...$result['errors'], ...$assignment['errors']];
|
|
}
|
|
$result['valid'] = $result['errors'] === [];
|
|
return $result;
|
|
}
|
|
|
|
/** Alias matching edit controller terminology. */
|
|
public function validateEdit(int $id, array $input, array $available = []): array
|
|
{
|
|
return $this->validateForEdit($id, $input, $available);
|
|
}
|
|
|
|
/** Validate deletion of a custom role; ID 1 is always protected. */
|
|
public function validateDelete(int $id, array $role = []): array
|
|
{
|
|
$errors = [];
|
|
if ($id < 1) $errors['id'] = 'Role ID must be a positive integer.';
|
|
if ($id === 1 || ($role !== [] && !($this->roles ?? new RoleRecord())->canDelete(['id' => $id, ...$role]))) {
|
|
$errors['role'] = 'The protected Administrator role cannot be deleted.';
|
|
}
|
|
return ['valid' => $errors === [], 'id' => $id, 'errors' => $errors];
|
|
}
|
|
|
|
public function delete(int $id, array $role = []): array
|
|
{
|
|
return $this->validateDelete($id, $role);
|
|
}
|
|
|
|
public function canAssignPermissions(array $role): bool
|
|
{
|
|
return !($this->roles ?? new RoleRecord())->isAdministrator($role);
|
|
}
|
|
|
|
public function assertCanAssignPermissions(array $role): void
|
|
{
|
|
($this->roles ?? new RoleRecord())->assertCanChangePermissions($role);
|
|
}
|
|
|
|
private function positiveId(mixed $value): ?int
|
|
{
|
|
if (is_int($value) && $value > 0) return $value;
|
|
if (is_string($value) && preg_match('/^[1-9]\d*$/', trim($value)) === 1) {
|
|
$id = filter_var(trim($value), FILTER_VALIDATE_INT);
|
|
return $id === false ? null : $id;
|
|
}
|
|
return null;
|
|
}
|
|
}
|