AccessService.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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 routePermission(?string $routeName): array|string|null
  58. {
  59. if (!$routeName) {
  60. return null;
  61. }
  62. $exact = config('access_routes.exact', [])[$routeName] ?? null;
  63. if ($exact) {
  64. return $exact;
  65. }
  66. foreach (config('access_routes.prefixes', []) as $prefix => $permissions) {
  67. if (!str_starts_with($routeName, $prefix)) {
  68. continue;
  69. }
  70. $suffix = substr($routeName, strlen($prefix));
  71. return $permissions[$suffix] ?? $permissions['*'] ?? null;
  72. }
  73. return null;
  74. }
  75. public function canAccessRoute(User $user, ?string $routeName): bool
  76. {
  77. $permission = $this->routePermission($routeName);
  78. if ($permission === null) {
  79. return false;
  80. }
  81. return is_array($permission)
  82. ? $this->canAny($user, $permission)
  83. : $this->can($user, $permission);
  84. }
  85. public function roleHasPermission(Role $role, string $permission): bool
  86. {
  87. if ($role->slug === Role::ADMIN) {
  88. return true;
  89. }
  90. $permissionModel = $role->permissions()
  91. ->where('slug', $permission)
  92. ->first();
  93. return $permissionModel?->pivot?->effect === 'allow';
  94. }
  95. public function getEffectivePermissions(User $user): Collection
  96. {
  97. if ($this->isDirectAdmin($user)) {
  98. try {
  99. return Permission::query()->pluck('slug')->mapWithKeys(fn (string $slug): array => [$slug => 'allow']);
  100. } catch (QueryException) {
  101. return collect();
  102. }
  103. }
  104. return Cache::remember(
  105. $this->cacheKey($user),
  106. now()->addHour(),
  107. fn (): Collection => $this->resolveEffectivePermissions($user)
  108. );
  109. }
  110. public function bumpCacheVersion(): void
  111. {
  112. Cache::forever(self::CACHE_VERSION_KEY, $this->cacheVersion() + 1);
  113. }
  114. private function resolveEffectivePermissions(User $user): Collection
  115. {
  116. if (!$this->tablesExist()) {
  117. return collect();
  118. }
  119. $effects = collect();
  120. $denied = collect();
  121. $role = $user->roleModel()->with('permissions')->first();
  122. if ($role) {
  123. foreach ($role->permissions as $permission) {
  124. $effect = $permission->pivot->effect;
  125. if ($effect === 'deny') {
  126. $denied->put($permission->slug, true);
  127. $effects->put($permission->slug, 'deny');
  128. continue;
  129. }
  130. if (!$denied->has($permission->slug)) {
  131. $effects->put($permission->slug, $effect);
  132. }
  133. }
  134. }
  135. $userPermissions = $user->permissions()
  136. ->where(function ($query) {
  137. $query->whereNull('expires_at')
  138. ->orWhere('expires_at', '>', now());
  139. })
  140. ->get();
  141. foreach ($userPermissions as $permission) {
  142. $effect = $permission->pivot->effect;
  143. if ($effect === 'deny') {
  144. $denied->put($permission->slug, true);
  145. $effects->put($permission->slug, 'deny');
  146. continue;
  147. }
  148. if (!$denied->has($permission->slug)) {
  149. $effects->put($permission->slug, $effect);
  150. }
  151. }
  152. return $effects;
  153. }
  154. private function isDirectAdmin(User $user): bool
  155. {
  156. return $user->resolvedRoleSlug() === Role::ADMIN;
  157. }
  158. private function fieldPermissionSlug(string $module, string $field, string $action): string
  159. {
  160. return "{$module}.fields.{$field}.{$action}";
  161. }
  162. private function cacheKey(User $user): string
  163. {
  164. return 'permissions:user:' . $user->id . ':v' . $this->cacheVersion();
  165. }
  166. private function cacheVersion(): int
  167. {
  168. return (int) Cache::get(self::CACHE_VERSION_KEY, 1);
  169. }
  170. private function tablesExist(): bool
  171. {
  172. return Schema::hasTable('roles')
  173. && Schema::hasTable('permissions')
  174. && Schema::hasTable('role_permissions')
  175. && Schema::hasTable('user_permissions');
  176. }
  177. }