$permissions */ public function sync(Model $assignee, ?Model $scope, array $permissions): void { $normalizedPermissions = $this->normalizePermissions($permissions, $assignee, $scope); DB::transaction(function () use ($assignee, $normalizedPermissions, $scope): void { $this->assignmentQuery($assignee, $scope)->delete(); if ($normalizedPermissions === []) { return; } PermissionAssignment::query()->insert( array_map( fn (Permission $permission): array => [ 'permission' => $permission->value, 'assignee_id' => $assignee->getKey(), 'assignee_type' => $assignee->getMorphClass(), 'created_at' => now(), 'scope_id' => $scope?->getKey(), 'scope_type' => $scope?->getMorphClass(), 'updated_at' => now(), ], $normalizedPermissions, ), ); }); } public function grant(Model $assignee, Permission|string $permission, ?Model $scope = null): void { $normalizedPermission = $this->normalizePermission($permission); $this->assertAssignmentContext($normalizedPermission, $assignee, $scope); $alreadyExists = $this->assignmentQuery($assignee, $scope) ->where('permission', $normalizedPermission->value) ->exists(); if ($alreadyExists) { return; } PermissionAssignment::query()->create([ 'permission' => $normalizedPermission->value, 'assignee_id' => $assignee->getKey(), 'assignee_type' => $assignee->getMorphClass(), 'scope_id' => $scope?->getKey(), 'scope_type' => $scope?->getMorphClass(), ]); } public function revoke(Model $assignee, Permission|string $permission, ?Model $scope = null): void { $normalizedPermission = $this->normalizePermission($permission); $this->assertAssignmentContext($normalizedPermission, $assignee, $scope); $this->assignmentQuery($assignee, $scope) ->where('permission', $normalizedPermission->value) ->delete(); } public function allows(User $user, Permission|string $permission, ?Model $scope = null): bool { $requiredPermission = $this->normalizePermission($permission); return collect($this->effectivePermissions($user, $scope)) ->contains(fn (Permission $effectivePermission): bool => $effectivePermission === $requiredPermission); } /** * @return array */ public function effectivePermissions(User $user, ?Model $scope = null): array { if ($this->hasGlobalAdmin($user)) { return Permission::cases(); } /** @var Collection $permissions */ $permissions = $this->relevantAssignments($user, $scope) ->map(fn (PermissionAssignment $assignment): Permission => $assignment->permission) ->values(); return $this->expandPermissions($permissions->all()); } /** * @return array */ public function permissionOptions(Model|string $assignee, Model|string|null $scope): array { return Permission::options( Permission::assignableFor($assignee, $scope), ); } /** * @return array> */ public function assignmentMap(string $assigneeClass, ?Model $scope = null): array { /** @var Model $assignee */ $assignee = new $assigneeClass; /** @var Collection $assignments */ $assignments = PermissionAssignment::query() ->select(['assignee_id', 'permission']) ->where('assignee_type', $assignee->getMorphClass()) ->tap(fn (Builder $query): Builder => $this->applyScopeConstraint($query, $scope)) ->orderBy('permission') ->get(); return $assignments ->groupBy(fn (PermissionAssignment $assignment): int => (int) $assignment->assignee_id) ->map(fn (Collection $group): array => $group ->map(fn (PermissionAssignment $assignment): string => $assignment->permission->value) ->values() ->all()) ->all(); } /** * @return array */ public function viewableProjectIds(User $user): array { if ($this->hasGlobalAdmin($user)) { return Project::query() ->pluck('id') ->map(fn (mixed $projectId): int => (int) $projectId) ->all(); } /** @var Collection $assignments */ $assignments = PermissionAssignment::query() ->select(['scope_id', 'permission']) ->where('scope_type', (new Project)->getMorphClass()) ->where(function (Builder $query) use ($user): void { $query->where(function (Builder $directAssignmentsQuery) use ($user): void { $directAssignmentsQuery ->where('assignee_type', $user->getMorphClass()) ->where('assignee_id', $user->getKey()); }); $teamIds = $user->teams() ->pluck('teams.id') ->map(fn (mixed $teamId): int => (int) $teamId) ->all(); if ($teamIds !== []) { $query->orWhere(function (Builder $teamAssignmentsQuery) use ($teamIds): void { $teamAssignmentsQuery ->where('assignee_type', (new Team)->getMorphClass()) ->whereIn('assignee_id', $teamIds); }); } }) ->get(); return $assignments ->filter(fn (PermissionAssignment $assignment): bool => $assignment->permission->includes(Permission::ProjectRead)) ->pluck('scope_id') ->filter() ->map(fn (mixed $projectId): int => (int) $projectId) ->unique() ->values() ->all(); } /** * @return array */ public function manageableTeamIds(User $user): array { if ($this->hasGlobalAdmin($user)) { return Team::query() ->pluck('id') ->map(fn (mixed $teamId): int => (int) $teamId) ->all(); } /** @var Collection $assignments */ $assignments = PermissionAssignment::query() ->select(['scope_id', 'permission']) ->where('assignee_type', $user->getMorphClass()) ->where('assignee_id', $user->getKey()) ->where('scope_type', (new Team)->getMorphClass()) ->get(); return $assignments ->filter(fn (PermissionAssignment $assignment): bool => $assignment->permission->includes(Permission::TeamManage)) ->pluck('scope_id') ->filter() ->map(fn (mixed $teamId): int => (int) $teamId) ->unique() ->values() ->all(); } public function canManageAnyTeam(User $user): bool { return $this->manageableTeamIds($user) !== []; } private function hasGlobalAdmin(User $user): bool { return PermissionAssignment::query() ->where('assignee_type', $user->getMorphClass()) ->where('assignee_id', $user->getKey()) ->whereNull('scope_type') ->whereNull('scope_id') ->where('permission', Permission::GlobalAdmin->value) ->exists(); } /** * @return Collection */ private function relevantAssignments(User $user, ?Model $scope): Collection { $query = PermissionAssignment::query(); $this->applyScopeConstraint($query, $scope); $query->where(function (Builder $permissionQuery) use ($scope, $user): void { $permissionQuery->where(function (Builder $directAssignmentsQuery) use ($user): void { $directAssignmentsQuery ->where('assignee_type', $user->getMorphClass()) ->where('assignee_id', $user->getKey()); }); if ($scope instanceof Project) { $teamIds = $user->teams() ->pluck('teams.id') ->map(fn (mixed $teamId): int => (int) $teamId) ->all(); if ($teamIds !== []) { $permissionQuery->orWhere(function (Builder $teamAssignmentsQuery) use ($teamIds): void { $teamAssignmentsQuery ->where('assignee_type', (new Team)->getMorphClass()) ->whereIn('assignee_id', $teamIds); }); } } }); return $query->get(); } /** * @param array $permissions * @return array */ private function normalizePermissions(array $permissions, Model $assignee, ?Model $scope): array { return collect($permissions) ->map(fn (Permission|string $permission): Permission => $this->normalizePermission($permission)) ->each(fn (Permission $permission): bool => $this->assertAssignmentContext($permission, $assignee, $scope)) ->unique(fn (Permission $permission): string => $permission->value) ->values() ->all(); } private function normalizePermission(Permission|string $permission): Permission { if ($permission instanceof Permission) { return $permission; } return Permission::from(trim($permission)); } private function assertAssignmentContext(Permission $permission, Model $assignee, ?Model $scope): bool { if (! $permission->isAssignableTo($assignee) || ! $permission->matchesScope($scope)) { throw new InvalidArgumentException("The {$permission->value} permission cannot be assigned in this context."); } return true; } private function assignmentQuery(Model $assignee, ?Model $scope): Builder { $query = PermissionAssignment::query() ->where('assignee_type', $assignee->getMorphClass()) ->where('assignee_id', $assignee->getKey()); $this->applyScopeConstraint($query, $scope); return $query; } private function applyScopeConstraint(Builder $query, ?Model $scope): Builder { if ($scope === null) { return $query ->whereNull('scope_type') ->whereNull('scope_id'); } return $query ->where('scope_type', $scope->getMorphClass()) ->where('scope_id', $scope->getKey()); } /** * @param array $permissions * @return array */ private function expandPermissions(array $permissions): array { $expandedPermissions = []; foreach ($permissions as $permission) { $expandedPermissions[$permission->value] = $permission; foreach ($permission->impliedPermissions() as $impliedPermission) { $expandedPermissions[$impliedPermission->value] = $impliedPermission; } } return array_values($expandedPermissions); } }