AccessService.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. <?php
  2. namespace App\Services\Access;
  3. use App\Models\Permission;
  4. use App\Models\Role;
  5. use App\Models\User;
  6. use Illuminate\Database\QueryException;
  7. use Illuminate\Support\Collection;
  8. use Illuminate\Support\Facades\Cache;
  9. use Illuminate\Support\Facades\Schema;
  10. class AccessService
  11. {
  12. private const CACHE_VERSION_KEY = 'permissions:version';
  13. public function can(User $user, string $permission): bool
  14. {
  15. if ($this->isDirectAdmin($user)) {
  16. return true;
  17. }
  18. $effect = $this->getEffectivePermissions($user)->get($permission);
  19. return $effect === 'allow';
  20. }
  21. public function canAny(User $user, array $permissions): bool
  22. {
  23. foreach ($permissions as $permission) {
  24. if ($this->can($user, trim((string) $permission))) {
  25. return true;
  26. }
  27. }
  28. return false;
  29. }
  30. public function canViewField(User $user, string $module, string $field, ?string $entity = null): bool
  31. {
  32. return $this->can($user, $this->fieldPermissionSlug($module, $field, 'view'));
  33. }
  34. public function canUpdateField(User $user, string $module, string $field, ?string $entity = null): bool
  35. {
  36. return $this->can($user, $this->fieldPermissionSlug($module, $field, 'update'));
  37. }
  38. public function filterReadableFields(User $user, string $module, array $fields, ?string $entity = null): array
  39. {
  40. return array_values(array_filter(
  41. $fields,
  42. fn (string $field): bool => $this->canViewField($user, $module, $field, $entity)
  43. ));
  44. }
  45. public function filterWritableData(User $user, string $module, array $data, ?string $entity = null): array
  46. {
  47. return array_filter(
  48. $data,
  49. fn (string $field): bool => $this->canUpdateField($user, $module, $field, $entity),
  50. ARRAY_FILTER_USE_KEY
  51. );
  52. }
  53. public function assertCan(User $user, string $permission): void
  54. {
  55. abort_unless($this->can($user, $permission), 403);
  56. }
  57. public function roleHasPermission(Role $role, string $permission): bool
  58. {
  59. if ($role->slug === Role::ADMIN) {
  60. return true;
  61. }
  62. $permissionModel = $role->permissions()
  63. ->where('slug', $permission)
  64. ->first();
  65. return $permissionModel?->pivot?->effect === 'allow';
  66. }
  67. public function getEffectivePermissions(User $user): Collection
  68. {
  69. if ($this->isDirectAdmin($user)) {
  70. try {
  71. return Permission::query()->pluck('slug')->mapWithKeys(fn (string $slug): array => [$slug => 'allow']);
  72. } catch (QueryException) {
  73. return collect();
  74. }
  75. }
  76. return Cache::remember(
  77. $this->cacheKey($user),
  78. now()->addHour(),
  79. fn (): Collection => $this->resolveEffectivePermissions($user)
  80. );
  81. }
  82. public function bumpCacheVersion(): void
  83. {
  84. Cache::forever(self::CACHE_VERSION_KEY, $this->cacheVersion() + 1);
  85. }
  86. private function resolveEffectivePermissions(User $user): Collection
  87. {
  88. if (!$this->tablesExist()) {
  89. return collect();
  90. }
  91. $effects = collect();
  92. $role = $user->roleModel()->with('permissions')->first();
  93. if ($role) {
  94. foreach ($role->permissions as $permission) {
  95. $effects->put($permission->slug, $permission->pivot->effect);
  96. }
  97. }
  98. $userPermissions = $user->permissions()
  99. ->where(function ($query) {
  100. $query->whereNull('expires_at')
  101. ->orWhere('expires_at', '>', now());
  102. })
  103. ->get();
  104. foreach ($userPermissions as $permission) {
  105. $effects->put($permission->slug, $permission->pivot->effect);
  106. }
  107. return $effects;
  108. }
  109. private function isDirectAdmin(User $user): bool
  110. {
  111. return $user->resolvedRoleSlug() === Role::ADMIN;
  112. }
  113. private function fieldPermissionSlug(string $module, string $field, string $action): string
  114. {
  115. return "{$module}.fields.{$field}.{$action}";
  116. }
  117. private function cacheKey(User $user): string
  118. {
  119. return 'permissions:user:' . $user->id . ':v' . $this->cacheVersion();
  120. }
  121. private function cacheVersion(): int
  122. {
  123. return (int) Cache::get(self::CACHE_VERSION_KEY, 1);
  124. }
  125. private function tablesExist(): bool
  126. {
  127. return Schema::hasTable('roles')
  128. && Schema::hasTable('permissions')
  129. && Schema::hasTable('role_permissions')
  130. && Schema::hasTable('user_permissions');
  131. }
  132. }