| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- <?php
- declare(strict_types=1);
- namespace App\Services;
- use App\Models\DocumentationFolder;
- use App\Models\DocumentationFolderPermission;
- use App\Models\Role;
- use App\Models\User;
- use Illuminate\Database\Eloquent\Collection;
- class DocumentationAccessService
- {
- /** @var array<int, DocumentationFolder> */
- private array $folders = [];
- /** @var array<int, Collection<int, DocumentationFolderPermission>> */
- private array $effectiveRules = [];
- public function isAdministrator(User $user): bool
- {
- return $user->resolvedRoleSlug() === Role::ADMIN;
- }
- public function canRead(User $user, DocumentationFolder $folder): bool
- {
- return $this->can($user, $folder, false);
- }
- public function canWrite(User $user, DocumentationFolder $folder): bool
- {
- return $this->can($user, $folder, true);
- }
- public function canManagePermissions(User $user, DocumentationFolder $folder): bool
- {
- return $this->isAdministrator($user)
- || ($user->hasPermission('documents.update') && $this->canWrite($user, $folder));
- }
- /**
- * Returns readable folders plus their ancestors required to render the tree.
- * Ancestors without read access are navigation-only nodes.
- *
- * @return Collection<int, DocumentationFolder>
- */
- public function navigationFolders(User $user): Collection
- {
- $folders = DocumentationFolder::query()
- ->with(['permissionRules.role', 'creator'])
- ->orderBy('name')
- ->get();
- foreach ($folders as $folder) {
- $this->folders[$folder->id] = $folder;
- }
- $included = [];
- foreach ($folders as $folder) {
- $canRead = $this->canRead($user, $folder);
- $folder->setAttribute('can_read', $canRead);
- $folder->setAttribute('can_write', $this->canWrite($user, $folder));
- if (! $canRead) {
- continue;
- }
- $current = $folder;
- while ($current) {
- $included[$current->id] = true;
- $current = $current->parent_id ? ($this->folders[$current->parent_id] ?? null) : null;
- }
- }
- return $folders
- ->filter(fn (DocumentationFolder $folder): bool => isset($included[$folder->id]))
- ->values();
- }
- public function forgetCachedRules(): void
- {
- $this->effectiveRules = [];
- $this->folders = [];
- }
- private function can(User $user, DocumentationFolder $folder, bool $write): bool
- {
- if ($this->isAdministrator($user)) {
- return true;
- }
- $this->folders[$folder->id] = $folder;
- foreach ($this->rulesFor($folder) as $rule) {
- if (! $this->matches($rule, $user)) {
- continue;
- }
- if ($write && $rule->can_write) {
- return true;
- }
- if (! $write && ($rule->can_read || $rule->can_write)) {
- return true;
- }
- }
- return false;
- }
- /** @return Collection<int, DocumentationFolderPermission> */
- private function rulesFor(DocumentationFolder $folder): Collection
- {
- if (isset($this->effectiveRules[$folder->id])) {
- return $this->effectiveRules[$folder->id];
- }
- if ($folder->inherits_permissions && $folder->parent_id) {
- $parent = $this->folders[$folder->parent_id] ?? DocumentationFolder::query()
- ->with('permissionRules.role')
- ->find($folder->parent_id);
- if ($parent) {
- $this->folders[$parent->id] = $parent;
- return $this->effectiveRules[$folder->id] = $this->rulesFor($parent);
- }
- }
- $rules = $folder->relationLoaded('permissionRules')
- ? $folder->permissionRules
- : $folder->permissionRules()->with('role')->get();
- return $this->effectiveRules[$folder->id] = $rules;
- }
- private function matches(DocumentationFolderPermission $rule, User $user): bool
- {
- return match ($rule->subject_type) {
- DocumentationFolderPermission::SUBJECT_ALL => true,
- DocumentationFolderPermission::SUBJECT_USER => $rule->user_id === $user->id,
- DocumentationFolderPermission::SUBJECT_ROLE => $rule->role_id === $user->role_id
- || $rule->role?->slug === $user->resolvedRoleSlug(),
- default => false,
- };
- }
- }
|