258 lines
9.2 KiB
PHP
258 lines
9.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
final class PermissionService
|
|
{
|
|
public const ROLES = [
|
|
'admin' => 'Administrateur',
|
|
'responsable' => 'Responsable',
|
|
'benevole' => 'Bénévole',
|
|
'lecture' => 'Lecture seule',
|
|
];
|
|
public const MODULES = [
|
|
'dashboard' => ['Dashboard', 'Priorités et situation quotidienne'],
|
|
'animals' => ['Animaux', 'Fiches, portées, liens, médias et adoptions'],
|
|
'medical' => ['Médical', 'Notes, traitements, vaccinations et décès'],
|
|
'care' => ['Tournées', 'Passages tablette et configuration des box'],
|
|
'directory' => ['Annuaire', 'Personnes, structures et coordonnées'],
|
|
'agenda' => ['Agenda', 'Rendez-vous, transports, visites et tâches partagées'],
|
|
'statistics' => ['Statistiques', 'Analyses annuelles et tendances'],
|
|
'administrative' => ['Administratif', 'Registre, documents et exports'],
|
|
'accounting' => ['Comptabilité', 'Factures, avoirs et règlements'],
|
|
'grants' => ['Subventions', 'Bilans, demandes et justificatifs'],
|
|
];
|
|
public static function roleLabel(string $key): string
|
|
{
|
|
return t('permission.role.' . $key, [], self::ROLES[$key] ?? $key);
|
|
}
|
|
public static function moduleLabel(string $key): string
|
|
{
|
|
return t('permission.module.' . $key, [], self::MODULES[$key][0] ?? $key);
|
|
}
|
|
public static function moduleDescription(string $key): string
|
|
{
|
|
return t('permission.module_help.' . $key, [], self::MODULES[$key][1] ?? '');
|
|
}
|
|
|
|
private static array $cache = [];
|
|
public static function can(string $module, string $action = 'view', ?string $role = null): bool
|
|
{
|
|
$role ??= (string) (Auth::user()['role'] ?? '');
|
|
if ($role === 'admin') {
|
|
return true;
|
|
}
|
|
if (!isset(self::MODULES[$module], self::ROLES[$role])) {
|
|
return false;
|
|
}
|
|
if (!isset(self::$cache[$role])) {
|
|
$s = DB::pdo()->prepare('SELECT module,can_view,can_edit FROM role_permissions WHERE role=?');
|
|
$s->execute([$role]);
|
|
self::$cache[$role] = [];
|
|
foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
self::$cache[$role][$row['module']] = [
|
|
'view' => (bool) $row['can_view'],
|
|
'edit' => (bool) $row['can_edit'],
|
|
];
|
|
}
|
|
}
|
|
return (bool) (self::$cache[$role][$module][$action === 'edit' ? 'edit' : 'view'] ?? false);
|
|
}
|
|
public static function matrix(): array
|
|
{
|
|
$matrix = [];
|
|
foreach (array_keys(self::ROLES) as $role) {
|
|
foreach (array_keys(self::MODULES) as $module) {
|
|
$matrix[$role][$module] = [
|
|
'view' => self::can($module, 'view', $role),
|
|
'edit' => self::can($module, 'edit', $role),
|
|
];
|
|
}
|
|
}
|
|
return $matrix;
|
|
}
|
|
public static function moduleForPath(string $path): ?string
|
|
{
|
|
if ($path === '/') {
|
|
return 'dashboard';
|
|
}
|
|
if (str_starts_with($path, '/animal/documents')) {
|
|
return 'medical';
|
|
}
|
|
if (
|
|
str_starts_with($path, '/animals') ||
|
|
str_starts_with($path, '/animal') ||
|
|
str_starts_with($path, '/litter') ||
|
|
str_starts_with($path, '/bonded')
|
|
) {
|
|
foreach (
|
|
[
|
|
'/animal/add-medical',
|
|
'/animal/medical-status',
|
|
'/animal/add-treatment',
|
|
'/animal/add-vaccine',
|
|
'/animal/death',
|
|
'/animal/prescription',
|
|
'/animal/lab-report',
|
|
'/animal/medical-document',
|
|
'/animal/surgery',
|
|
]
|
|
as $medical
|
|
) {
|
|
if (str_starts_with($path, $medical)) {
|
|
return 'medical';
|
|
}
|
|
}
|
|
return 'animals';
|
|
}
|
|
if (str_starts_with($path, '/dashboard')) {
|
|
return 'dashboard';
|
|
}
|
|
if (str_starts_with($path, '/care-round')) {
|
|
return 'care';
|
|
}
|
|
if (str_starts_with($path, '/directory')) {
|
|
return 'directory';
|
|
}
|
|
if (str_starts_with($path, '/agenda')) {
|
|
return 'agenda';
|
|
}
|
|
if (str_starts_with($path, '/statistics')) {
|
|
return 'statistics';
|
|
}
|
|
if (str_starts_with($path, '/accounting')) {
|
|
return 'accounting';
|
|
}
|
|
if (str_starts_with($path, '/admin/grants')) {
|
|
return 'grants';
|
|
}
|
|
if (str_starts_with($path, '/admin') || str_starts_with($path, '/documents')) {
|
|
return 'administrative';
|
|
}
|
|
return null;
|
|
}
|
|
public static function enforce(string $path, string $method): void
|
|
{
|
|
if (
|
|
str_starts_with($path, '/settings') ||
|
|
str_starts_with($path, '/dev/') ||
|
|
in_array($path, ['/logout', '/evolutions'], true)
|
|
) {
|
|
return;
|
|
}
|
|
$module = self::moduleForPath($path);
|
|
if (!$module) {
|
|
return;
|
|
}
|
|
$action = strtoupper($method) === 'POST' || self::isEditPage($path) ? 'edit' : 'view';
|
|
$allowed = self::can($module, $action) && ($module !== 'medical' || self::can('animals', 'view'));
|
|
if (!$allowed) {
|
|
AuditService::log('authorization_denied', $path, 'Accès refusé au module ' . $module, null, null, [
|
|
'module' => $module,
|
|
'requested_action' => $action,
|
|
]);
|
|
http_response_code(403);
|
|
if (function_exists('render')) {
|
|
render('access_denied.php', [
|
|
'title' => t('access.denied'),
|
|
'deniedAction' => $action,
|
|
'deniedModule' => self::moduleLabel($module),
|
|
]);
|
|
} else {
|
|
echo t('access.denied');
|
|
}
|
|
exit();
|
|
}
|
|
}
|
|
public static function firstAllowedPath(): string
|
|
{
|
|
foreach (
|
|
[
|
|
'dashboard' => '/dashboard',
|
|
'animals' => '/animals',
|
|
'care' => '/care-round',
|
|
'directory' => '/directory',
|
|
'agenda' => '/agenda',
|
|
'statistics' => '/statistics',
|
|
'accounting' => '/accounting',
|
|
'administrative' => '/admin/register',
|
|
'grants' => '/admin/grants',
|
|
]
|
|
as $module => $path
|
|
) {
|
|
if (self::can($module)) {
|
|
return $path;
|
|
}
|
|
}
|
|
return '/evolutions';
|
|
}
|
|
public static function canOpenPath(string $path): bool
|
|
{
|
|
$module = self::moduleForPath($path);
|
|
return $module === null ||
|
|
(self::can($module, self::isEditPage($path) ? 'edit' : 'view') &&
|
|
($module !== 'medical' || self::can('animals')));
|
|
}
|
|
public static function currentPermissions(): array
|
|
{
|
|
$out = [];
|
|
foreach (self::MODULES as $module => $unused) {
|
|
$out[$module] = ['view' => self::can($module), 'edit' => self::can($module, 'edit')];
|
|
}
|
|
return $out;
|
|
}
|
|
private static function isEditPage(string $path): bool
|
|
{
|
|
return in_array(
|
|
$path,
|
|
[
|
|
'/animal/new',
|
|
'/animal/edit',
|
|
'/animal/death',
|
|
'/animal/surgery/new',
|
|
'/litter/new',
|
|
'/litter/add-existing',
|
|
'/litter/add-existing/save',
|
|
'/bonded/new',
|
|
'/directory/new',
|
|
'/directory/edit',
|
|
'/care-round/setup',
|
|
],
|
|
true,
|
|
);
|
|
}
|
|
public static function save(array $input, ?int $userId): void
|
|
{
|
|
$db = DB::pdo();
|
|
$stmt = $db->prepare(
|
|
"INSERT INTO role_permissions(role,module,can_view,can_edit,updated_by,updated_at) VALUES(:role,:module,:view,:edit,:user,datetime('now')) ON CONFLICT(role,module) DO UPDATE SET can_view=excluded.can_view,can_edit=excluded.can_edit,updated_by=excluded.updated_by,updated_at=excluded.updated_at",
|
|
);
|
|
$db->beginTransaction();
|
|
try {
|
|
foreach (self::ROLES as $role => $unused) {
|
|
if ($role === 'admin') {
|
|
continue;
|
|
}
|
|
foreach (self::MODULES as $module => $unusedModule) {
|
|
$view = isset($input[$role][$module]['view']) ? 1 : 0;
|
|
if ($module === 'medical' && !isset($input[$role]['animals']['view'])) {
|
|
$view = 0;
|
|
}
|
|
$edit = $view && isset($input[$role][$module]['edit']) ? 1 : 0;
|
|
$stmt->execute([
|
|
':role' => $role,
|
|
':module' => $module,
|
|
':view' => $view,
|
|
':edit' => $edit,
|
|
':user' => $userId,
|
|
]);
|
|
}
|
|
}
|
|
$db->commit();
|
|
self::$cache = [];
|
|
} catch (Throwable $e) {
|
|
$db->rollBack();
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|