permissions refactoring

This commit is contained in:
2026-04-12 19:21:04 +02:00
parent 51014395a2
commit 9dcdfb981a
64 changed files with 1842 additions and 656 deletions
@@ -0,0 +1,343 @@
<?php
namespace App\Actions\Permissions;
use App\Enums\Permission;
use App\Models\PermissionAssignment;
use App\Models\Project;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
class PermissionService
{
/**
* @param array<int, Permission|string> $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<int, Permission>
*/
public function effectivePermissions(User $user, ?Model $scope = null): array
{
if ($this->hasGlobalAdmin($user)) {
return Permission::cases();
}
/** @var Collection<int, Permission> $permissions */
$permissions = $this->relevantAssignments($user, $scope)
->map(fn (PermissionAssignment $assignment): Permission => $assignment->permission)
->values();
return $this->expandPermissions($permissions->all());
}
/**
* @return array<int, array{label: string, value: string}>
*/
public function permissionOptions(Model|string $assignee, Model|string|null $scope): array
{
return Permission::options(
Permission::assignableFor($assignee, $scope),
);
}
/**
* @return array<string, array<int, string>>
*/
public function assignmentMap(string $assigneeClass, ?Model $scope = null): array
{
/** @var Model $assignee */
$assignee = new $assigneeClass;
/** @var Collection<int, PermissionAssignment> $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<int, int>
*/
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<int, PermissionAssignment> $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<int, int>
*/
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<int, PermissionAssignment> $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<int, PermissionAssignment>
*/
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<int, Permission|string> $permissions
* @return array<int, Permission>
*/
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<int, Permission> $permissions
* @return array<int, Permission>
*/
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);
}
}
@@ -1,247 +0,0 @@
<?php
namespace App\Actions\ProjectPermissions;
use App\Models\Project;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ProjectPermissionService
{
/**
* @var array<int, string>
*/
public const PERMISSIONS = [
'create',
'view',
'edit',
'delete',
];
/**
* @return array<int, string>
*/
public static function permissions(): array
{
return self::PERMISSIONS;
}
/**
* @return array<int, array{label: string, value: string}>
*/
public function permissionOptions(): array
{
return collect(self::PERMISSIONS)
->map(fn (string $permission): array => [
'label' => Str::headline($permission),
'value' => $permission,
])
->all();
}
public function grantAllProjectPermissions(Project $project, User $user): void
{
$this->syncUserPermissions($project, $user, self::PERMISSIONS);
}
/**
* @param array<int, string> $permissions
*/
public function syncUserPermissions(Project $project, User $user, array $permissions): void
{
$sanitizedPermissions = $this->sanitizePermissions($permissions);
DB::transaction(function () use ($project, $user, $sanitizedPermissions): void {
DB::table('project_user_permissions')
->where('project_id', $project->id)
->where('user_id', $user->id)
->delete();
if ($sanitizedPermissions === []) {
return;
}
DB::table('project_user_permissions')->insert(
array_map(
fn (string $permission): array => [
'project_id' => $project->id,
'user_id' => $user->id,
'permission' => $permission,
'created_at' => now(),
'updated_at' => now(),
],
$sanitizedPermissions,
),
);
});
}
/**
* @param array<int, string> $permissions
*/
public function syncTeamPermissions(Project $project, Team $team, array $permissions): void
{
$sanitizedPermissions = $this->sanitizePermissions($permissions);
DB::transaction(function () use ($project, $team, $sanitizedPermissions): void {
DB::table('project_team_permissions')
->where('project_id', $project->id)
->where('team_id', $team->id)
->delete();
if ($sanitizedPermissions === []) {
return;
}
DB::table('project_team_permissions')->insert(
array_map(
fn (string $permission): array => [
'project_id' => $project->id,
'team_id' => $team->id,
'permission' => $permission,
'created_at' => now(),
'updated_at' => now(),
],
$sanitizedPermissions,
),
);
});
}
/**
* @param array<int, int> $userIds
*/
public function syncTeamMembers(Team $team, array $userIds): void
{
$team->users()->sync($userIds);
}
public function userHasPermission(User $user, Project $project, string $permission): bool
{
if ($user->is_admin) {
return true;
}
$sanitizedPermission = Str::lower(trim($permission));
if (! in_array($sanitizedPermission, self::PERMISSIONS, true)) {
return false;
}
$hasDirectPermission = DB::table('project_user_permissions')
->where('project_id', $project->id)
->where('user_id', $user->id)
->where('permission', $sanitizedPermission)
->exists();
if ($hasDirectPermission) {
return true;
}
return DB::table('project_team_permissions')
->join('team_user', 'team_user.team_id', '=', 'project_team_permissions.team_id')
->where('project_team_permissions.project_id', $project->id)
->where('team_user.user_id', $user->id)
->where('project_team_permissions.permission', $sanitizedPermission)
->exists();
}
public function userCanViewProject(User $user, Project $project): bool
{
if ($user->is_admin) {
return true;
}
return in_array($project->id, $this->viewableProjectIds($user), true);
}
public function userCanManageProjectAccess(User $user, Project $project): bool
{
return $this->userHasPermission($user, $project, 'edit');
}
/**
* @return array<int, int>
*/
public function viewableProjectIds(User $user): array
{
if ($user->is_admin) {
return Project::query()->pluck('id')->all();
}
$directProjectIds = DB::table('project_user_permissions')
->where('user_id', $user->id)
->pluck('project_id');
$teamProjectIds = DB::table('project_team_permissions')
->join('team_user', 'team_user.team_id', '=', 'project_team_permissions.team_id')
->where('team_user.user_id', $user->id)
->pluck('project_team_permissions.project_id');
return $directProjectIds
->merge($teamProjectIds)
->map(fn (mixed $projectId): int => (int) $projectId)
->unique()
->values()
->all();
}
/**
* @return array<string, array<int, string>>
*/
public function userPermissionMap(Project $project): array
{
/** @var Collection<int, object{user_id: int, permission: string}> $permissions */
$permissions = DB::table('project_user_permissions')
->select(['user_id', 'permission'])
->where('project_id', $project->id)
->orderBy('permission')
->get();
return $permissions
->groupBy(fn (object $permission): int => (int) $permission->user_id)
->map(fn (Collection $group): array => $group
->pluck('permission')
->values()
->all())
->all();
}
/**
* @return array<string, array<int, string>>
*/
public function teamPermissionMap(Project $project): array
{
/** @var Collection<int, object{team_id: int, permission: string}> $permissions */
$permissions = DB::table('project_team_permissions')
->select(['team_id', 'permission'])
->where('project_id', $project->id)
->orderBy('permission')
->get();
return $permissions
->groupBy(fn (object $permission): int => (int) $permission->team_id)
->map(fn (Collection $group): array => $group
->pluck('permission')
->values()
->all())
->all();
}
/**
* @param array<int, string> $permissions
* @return array<int, string>
*/
private function sanitizePermissions(array $permissions): array
{
return collect($permissions)
->map(fn (mixed $permission): string => Str::lower(trim((string) $permission)))
->filter(fn (string $permission): bool => in_array($permission, self::PERMISSIONS, true))
->unique()
->values()
->all();
}
}
+175
View File
@@ -0,0 +1,175 @@
<?php
namespace App\Enums;
use App\Models\Project;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
enum Permission: string
{
case GlobalAdmin = 'global:admin';
case ProjectCreate = 'project:create';
case ProjectRead = 'project:read';
case ProjectWrite = 'project:write';
case ProjectAdmin = 'project:admin';
case TeamInvite = 'team:invite';
case TeamManage = 'team:manage';
case TeamAdmin = 'team:admin';
public function label(): string
{
return match ($this) {
self::GlobalAdmin => 'Global admin',
self::ProjectCreate => 'Project create',
self::ProjectRead => 'Project read',
self::ProjectWrite => 'Project write',
self::ProjectAdmin => 'Project admin',
self::TeamInvite => 'Team invite',
self::TeamManage => 'Team manage',
self::TeamAdmin => 'Team admin',
};
}
public function scope(): PermissionScope
{
return match ($this) {
self::GlobalAdmin, self::ProjectCreate => PermissionScope::Global,
self::ProjectRead, self::ProjectWrite, self::ProjectAdmin => PermissionScope::Project,
self::TeamInvite, self::TeamManage, self::TeamAdmin => PermissionScope::Team,
};
}
/**
* @return array<int, self>
*/
public function impliedPermissions(): array
{
return match ($this) {
self::GlobalAdmin => array_values(array_filter(
self::cases(),
fn (self $permission): bool => $permission !== self::GlobalAdmin,
)),
self::ProjectAdmin => [self::ProjectWrite, self::ProjectRead],
self::ProjectWrite => [self::ProjectRead],
self::TeamAdmin => [self::TeamManage, self::TeamInvite],
default => [],
};
}
public function includes(self $permission): bool
{
if ($this === $permission) {
return true;
}
foreach ($this->impliedPermissions() as $impliedPermission) {
if ($impliedPermission->includes($permission)) {
return true;
}
}
return false;
}
public function isAssignableTo(Model|string $assignee): bool
{
$assigneeClass = $assignee instanceof Model
? $assignee::class
: $assignee;
return match ($this->scope()) {
PermissionScope::Global => is_a($assigneeClass, User::class, true),
PermissionScope::Project => is_a($assigneeClass, User::class, true) || is_a($assigneeClass, Team::class, true),
PermissionScope::Team => is_a($assigneeClass, User::class, true),
};
}
public function matchesScope(Model|string|null $scope): bool
{
$scopeClass = $scope instanceof Model
? $scope::class
: $scope;
return match ($this->scope()) {
PermissionScope::Global => $scopeClass === null,
PermissionScope::Project => is_a($scopeClass, Project::class, true),
PermissionScope::Team => is_a($scopeClass, Team::class, true),
};
}
/**
* @param array<int, self>|null $permissions
* @return array<int, string>
*/
public static function values(?array $permissions = null): array
{
return array_map(
fn (self $permission): string => $permission->value,
$permissions ?? self::cases(),
);
}
/**
* @return array<int, self>
*/
public static function globalAssignable(): array
{
return [
self::GlobalAdmin,
self::ProjectCreate,
];
}
/**
* @return array<int, self>
*/
public static function projectAssignable(): array
{
return [
self::ProjectRead,
self::ProjectWrite,
self::ProjectAdmin,
];
}
/**
* @return array<int, self>
*/
public static function teamAssignable(): array
{
return [
self::TeamInvite,
self::TeamManage,
self::TeamAdmin,
];
}
/**
* @return array<int, self>
*/
public static function assignableFor(Model|string $assignee, Model|string|null $scope): array
{
return array_values(array_filter(
self::cases(),
fn (self $permission): bool => $permission->isAssignableTo($assignee)
&& $permission->matchesScope($scope),
));
}
/**
* @param array<int, self> $permissions
* @return array<int, array{label: string, value: string}>
*/
public static function options(array $permissions): array
{
return array_map(
fn (self $permission): array => [
'label' => $permission->label(),
'value' => $permission->value,
],
$permissions,
);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum PermissionScope: string
{
case Global = 'global';
case Project = 'project';
case Team = 'team';
}
@@ -2,8 +2,9 @@
namespace App\Http\Controllers\Admin;
use App\Actions\Permissions\PermissionService;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UpdateUserAdminStatusRequest;
use App\Http\Requests\Admin\SyncUserGlobalPermissionsRequest;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Arr;
@@ -12,28 +13,30 @@ use Inertia\Response;
class UserManagementController extends Controller
{
public function __construct(
private readonly PermissionService $permissionService,
) {}
public function index(): Response
{
return Inertia::render('admin/users/Index', [
'directPermissions' => $this->permissionService->assignmentMap(User::class),
'permissionOptions' => $this->permissionService->permissionOptions(User::class, null),
'status' => session('status'),
'users' => User::query()
->orderByDesc('is_admin')
->orderBy('name')
->get(),
]);
}
public function update(UpdateUserAdminStatusRequest $request, User $user): RedirectResponse
public function update(SyncUserGlobalPermissionsRequest $request, User $user): RedirectResponse
{
$validated = $request->validated();
$user->forceFill([
'is_admin' => Arr::get($validated, 'is_admin'),
])->save();
$permissions = Arr::get($validated, 'permissions', []);
$status = $user->is_admin
? "{$user->name} now has admin access."
: "{$user->name} no longer has admin access.";
$this->permissionService->sync($user, null, $permissions);
return to_route('admin.users.index')->with('status', $status);
return to_route('admin.users.index')
->with('status', "{$user->name} global permissions were updated.");
}
}
@@ -2,8 +2,9 @@
namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Actions\Tickets\CreateProjectTicket;
use App\Enums\Permission;
use App\Http\Requests\StoreProjectTicketRequest;
use App\Models\Project;
use App\Models\Ticket;
@@ -16,7 +17,7 @@ class ProjectBacklogController extends Controller
{
public function __construct(
private readonly CreateProjectTicket $createProjectTicket,
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function index(Request $request, Project $project): Response
@@ -24,14 +25,14 @@ class ProjectBacklogController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userCanViewProject($user, $project),
$this->permissionService->allows($user, Permission::ProjectRead, $project),
403,
);
return Inertia::render('projects/Backlog', [
'boardColumns' => $project->boardColumns()->get(),
'canEditTickets' => $this->projectPermissionService->userHasPermission($user, $project, 'edit'),
'canCreateTickets' => $this->projectPermissionService->userHasPermission($user, $project, 'create'),
'canEditTickets' => $this->permissionService->allows($user, Permission::ProjectWrite, $project),
'canCreateTickets' => $this->permissionService->allows($user, Permission::ProjectWrite, $project),
'project' => $project,
'status' => session('status'),
'ticketPriorities' => $project->ticketPriorities()->get(),
@@ -51,7 +52,7 @@ class ProjectBacklogController extends Controller
$validated = $request->validated();
abort_unless(
$this->projectPermissionService->userHasPermission($user, $project, 'create'),
$this->permissionService->allows($user, Permission::ProjectWrite, $project),
403,
);
@@ -2,8 +2,9 @@
namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Actions\Tickets\CreateTicketWorklogEntry;
use App\Enums\Permission;
use App\Http\Requests\MoveProjectTicketRequest;
use App\Models\Project;
use App\Models\Ticket;
@@ -17,7 +18,7 @@ class ProjectBoardController extends Controller
{
public function __construct(
private readonly CreateTicketWorklogEntry $createTicketWorklogEntry,
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function index(Request $request, Project $project): Response
@@ -25,7 +26,7 @@ class ProjectBoardController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userCanViewProject($user, $project),
$this->permissionService->allows($user, Permission::ProjectRead, $project),
403,
);
@@ -36,7 +37,7 @@ class ProjectBoardController extends Controller
->with(['ticketPriority', 'ticketType']),
])
->get(),
'canEditTickets' => $this->projectPermissionService->userHasPermission($user, $project, 'edit'),
'canEditTickets' => $this->permissionService->allows($user, Permission::ProjectWrite, $project),
'project' => $project,
'status' => session('status'),
]);
@@ -50,7 +51,7 @@ class ProjectBoardController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userHasPermission($user, $project, 'edit'),
$this->permissionService->allows($user, Permission::ProjectWrite, $project),
403,
);
@@ -2,7 +2,8 @@
namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Enums\Permission;
use App\Http\Requests\StoreBoardColumnRequest;
use App\Http\Requests\UpdateBoardColumnRequest;
use App\Models\BoardColumn;
@@ -18,13 +19,13 @@ use Inertia\Response;
class ProjectBoardSettingsController extends Controller
{
public function __construct(
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function show(Request $request, Project $project): Response
{
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
$this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project),
403,
);
@@ -40,7 +41,7 @@ class ProjectBoardSettingsController extends Controller
Project $project,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
$this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project),
403,
);
$validated = $request->validated();
@@ -68,7 +69,7 @@ class ProjectBoardSettingsController extends Controller
BoardColumn $boardColumn,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
$this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project),
403,
);
$validated = $request->validated();
@@ -91,7 +92,7 @@ class ProjectBoardSettingsController extends Controller
BoardColumn $boardColumn,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
$this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project),
403,
);
+23 -16
View File
@@ -2,8 +2,9 @@
namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Enums\Permission;
use App\Http\Requests\DeleteProjectRequest;
use App\Http\Requests\StoreProjectRequest;
use App\Http\Requests\SyncProjectPermissionsRequest;
@@ -20,7 +21,7 @@ class ProjectController extends Controller
{
public function __construct(
private readonly InitializeProjectTicketMetadata $initializeProjectTicketMetadata,
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function index(Request $request): Response
@@ -30,12 +31,13 @@ class ProjectController extends Controller
$projectQuery = Project::query()
->orderByDesc('id');
if (! $user->is_admin) {
$viewableProjectIds = $this->projectPermissionService->viewableProjectIds($user);
if (! $this->permissionService->allows($user, Permission::GlobalAdmin)) {
$viewableProjectIds = $this->permissionService->viewableProjectIds($user);
if ($viewableProjectIds === []) {
return Inertia::render('projects/Index', [
'projects' => [],
'status' => session('status'),
]);
}
@@ -49,8 +51,13 @@ class ProjectController extends Controller
]);
}
public function create(): Response
public function create(Request $request): Response
{
abort_unless(
$this->permissionService->allows($request->user(), Permission::ProjectCreate),
403,
);
return Inertia::render('projects/Create');
}
@@ -58,7 +65,7 @@ class ProjectController extends Controller
{
$project = Project::query()->create($request->validated());
$this->initializeProjectTicketMetadata->handle($project);
$this->projectPermissionService->grantAllProjectPermissions($project, $request->user());
$this->permissionService->grant($request->user(), Permission::ProjectAdmin, $project);
return to_route('projects.show', $project);
}
@@ -68,12 +75,12 @@ class ProjectController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userCanViewProject($user, $project),
$this->permissionService->allows($user, Permission::ProjectRead, $project),
403,
);
return Inertia::render('projects/Show', [
'canManageAccess' => $this->projectPermissionService->userCanManageProjectAccess($user, $project),
'canManageAccess' => $this->permissionService->allows($user, Permission::ProjectAdmin, $project),
'project' => $project,
]);
}
@@ -90,16 +97,16 @@ class ProjectController extends Controller
{
$user = $request->user();
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($user, $project),
$this->permissionService->allows($user, Permission::ProjectAdmin, $project),
403,
);
return Inertia::render('projects/Access', [
'directPermissions' => $this->projectPermissionService->userPermissionMap($project),
'permissionOptions' => $this->projectPermissionService->permissionOptions(),
'directPermissions' => $this->permissionService->assignmentMap(User::class, $project),
'permissionOptions' => $this->permissionService->permissionOptions(User::class, Project::class),
'project' => $project,
'status' => session('status'),
'teamPermissions' => $this->projectPermissionService->teamPermissionMap($project),
'teamPermissions' => $this->permissionService->assignmentMap(Team::class, $project),
'teams' => Team::query()
->with('users')
->orderBy('name')
@@ -125,14 +132,14 @@ class ProjectController extends Controller
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
$this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project),
403,
);
$validated = $request->validated();
$permissions = Arr::get($validated, 'permissions', []);
$this->projectPermissionService->syncUserPermissions($project, $user, $permissions);
$this->permissionService->sync($user, $project, $permissions);
return to_route('projects.access', $project)
->with('status', "{$user->name} permissions were updated.");
@@ -144,14 +151,14 @@ class ProjectController extends Controller
Team $team,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
$this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project),
403,
);
$validated = $request->validated();
$permissions = Arr::get($validated, 'permissions', []);
$this->projectPermissionService->syncTeamPermissions($project, $team, $permissions);
$this->permissionService->sync($team, $project, $permissions);
return to_route('projects.access', $project)
->with('status', "{$team->name} permissions were updated.");
@@ -4,7 +4,8 @@ namespace App\Http\Controllers;
use App\Actions\Git\Data\GitRepository;
use App\Actions\Git\GitProviderManager;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Enums\Permission;
use App\Http\Requests\StoreRepositoryConnectionRequest;
use App\Http\Requests\UpdateRepositoryConnectionRequest;
use App\Models\Project;
@@ -24,14 +25,14 @@ class ProjectRepositorySettingsController extends Controller
{
public function __construct(
private readonly GitProviderManager $gitProviderManager,
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function show(Request $request, Project $project): Response
{
$user = $request->user();
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($user, $project),
$this->permissionService->allows($user, Permission::ProjectAdmin, $project),
403,
);
@@ -66,7 +67,7 @@ class ProjectRepositorySettingsController extends Controller
): RedirectResponse {
$user = $request->user();
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($user, $project),
$this->permissionService->allows($user, Permission::ProjectAdmin, $project),
403,
);
@@ -96,7 +97,7 @@ class ProjectRepositorySettingsController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($user, $project),
$this->permissionService->allows($user, Permission::ProjectAdmin, $project),
403,
);
@@ -128,7 +129,7 @@ class ProjectRepositorySettingsController extends Controller
RepositoryConnection $repositoryConnection,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
$this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project),
403,
);
@@ -5,7 +5,8 @@ namespace App\Http\Controllers;
use App\Actions\Git\Data\GitBranch;
use App\Actions\Git\GitProviderManager;
use App\Actions\Git\ValidateGitBranchName;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Enums\Permission;
use App\Http\Requests\StoreProjectTicketBranchRequest;
use App\Http\Requests\ValidateProjectTicketBranchRequest;
use App\Models\Project;
@@ -23,7 +24,7 @@ class ProjectTicketBranchController extends Controller
public function __construct(
private readonly GitProviderManager $gitProviderManager,
private readonly ValidateGitBranchName $validateGitBranchName,
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function validate(
@@ -34,7 +35,7 @@ class ProjectTicketBranchController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userHasPermission($user, $project, 'edit'),
$this->permissionService->allows($user, Permission::ProjectWrite, $project),
403,
);
@@ -110,7 +111,7 @@ class ProjectTicketBranchController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userHasPermission($user, $project, 'edit'),
$this->permissionService->allows($user, Permission::ProjectWrite, $project),
403,
);
@@ -5,8 +5,9 @@ namespace App\Http\Controllers;
use App\Actions\Git\BuildSuggestedTicketBranchName;
use App\Actions\Git\ResolveTicketBranchMatches;
use App\Actions\Git\ResolveTicketPullRequestMatches;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Actions\Tickets\UpdateProjectTicket;
use App\Enums\Permission;
use App\Http\Requests\UpdateProjectTicketRequest;
use App\Models\Project;
use App\Models\Ticket;
@@ -22,7 +23,7 @@ class ProjectTicketController extends Controller
{
public function __construct(
private readonly BuildSuggestedTicketBranchName $buildSuggestedTicketBranchName,
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
private readonly ResolveTicketBranchMatches $resolveTicketBranchMatches,
private readonly ResolveTicketPullRequestMatches $resolveTicketPullRequestMatches,
private readonly UpdateProjectTicket $updateProjectTicket,
@@ -33,14 +34,14 @@ class ProjectTicketController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userCanViewProject($user, $project),
$this->permissionService->allows($user, Permission::ProjectRead, $project),
403,
);
$excludedParentIds = [$ticket->id, ...$this->descendantIds($ticket)];
return Inertia::render('projects/Ticket', [
'canEditTicket' => $this->projectPermissionService->userHasPermission($user, $project, 'edit'),
'canEditTicket' => $this->permissionService->allows($user, Permission::ProjectWrite, $project),
'childTicketTree' => $this->buildChildTicketTree($ticket),
'linkedTickets' => $this->linkedTickets($ticket),
'parentTicketOptions' => Ticket::query()
@@ -103,7 +104,7 @@ class ProjectTicketController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userHasPermission($user, $project, 'edit'),
$this->permissionService->allows($user, Permission::ProjectWrite, $project),
403,
);
@@ -2,8 +2,9 @@
namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Actions\Tickets\CreateTicketWorklogEntry;
use App\Enums\Permission;
use App\Http\Requests\StoreTicketLinkRequest;
use App\Models\Project;
use App\Models\Ticket;
@@ -17,7 +18,7 @@ class ProjectTicketLinkController extends Controller
{
public function __construct(
private readonly CreateTicketWorklogEntry $createTicketWorklogEntry,
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function store(
@@ -28,7 +29,7 @@ class ProjectTicketLinkController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userHasPermission($user, $project, 'edit'),
$this->permissionService->allows($user, Permission::ProjectWrite, $project),
403,
);
@@ -93,7 +94,7 @@ class ProjectTicketLinkController extends Controller
$user = $request->user();
abort_unless(
$this->projectPermissionService->userHasPermission($user, $project, 'edit'),
$this->permissionService->allows($user, Permission::ProjectWrite, $project),
403,
);
@@ -2,8 +2,9 @@
namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Enums\Permission;
use App\Http\Requests\StoreProjectNamedSettingRequest;
use App\Http\Requests\StoreTicketFieldDefinitionRequest;
use App\Http\Requests\UpdateProjectNamedSettingRequest;
@@ -26,7 +27,7 @@ class ProjectTicketMetadataSettingsController extends Controller
{
public function __construct(
private readonly InitializeProjectTicketMetadata $initializeProjectTicketMetadata,
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function show(Request $request, Project $project): Response
@@ -355,7 +356,7 @@ class ProjectTicketMetadataSettingsController extends Controller
private function abortUnlessCanManage(Request $request, Project $project): void
{
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
$this->permissionService->allows($request->user(), Permission::ProjectAdmin, $project),
403,
);
}
+56 -11
View File
@@ -2,12 +2,15 @@
namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Permissions\PermissionService;
use App\Enums\Permission;
use App\Http\Requests\StoreTeamRequest;
use App\Http\Requests\SyncTeamUserPermissionsRequest;
use App\Http\Requests\UpdateTeamMembersRequest;
use App\Models\Team;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Inertia\Inertia;
use Inertia\Response;
@@ -15,17 +18,36 @@ use Inertia\Response;
class TeamController extends Controller
{
public function __construct(
private readonly ProjectPermissionService $projectPermissionService,
private readonly PermissionService $permissionService,
) {}
public function index(): Response
public function index(Request $request): Response
{
$user = $request->user();
$canCreateTeams = $this->permissionService->allows($user, Permission::GlobalAdmin);
$manageableTeamIds = $this->permissionService->manageableTeamIds($user);
abort_unless($canCreateTeams || $manageableTeamIds !== [], 403);
$teams = Team::query()
->with('users')
->when(
! $canCreateTeams,
fn ($query) => $query->whereIn('id', $manageableTeamIds),
)
->orderBy('name')
->get();
return Inertia::render('teams/Index', [
'canCreateTeams' => $canCreateTeams,
'permissionOptions' => $this->permissionService->permissionOptions(User::class, Team::class),
'status' => session('status'),
'teams' => Team::query()
->with('users')
->orderBy('name')
->get()
'teamPermissions' => $teams
->mapWithKeys(fn (Team $team): array => [
$team->id => $this->permissionService->assignmentMap(User::class, $team),
])
->all(),
'teams' => $teams
->map(fn (Team $team): array => [
'id' => $team->id,
'name' => $team->name,
@@ -44,19 +66,42 @@ class TeamController extends Controller
{
$team = Team::query()->create($request->validated());
return to_route('admin.teams.index')
return to_route('teams.index')
->with('status', "{$team->name} was created.");
}
public function updateMembers(UpdateTeamMembersRequest $request, Team $team): RedirectResponse
{
$validated = $request->validated();
abort_unless(
$this->permissionService->allows($request->user(), Permission::TeamManage, $team),
403,
);
$validated = $request->validated();
$userIds = Arr::get($validated, 'user_ids', []);
$this->projectPermissionService->syncTeamMembers($team, $userIds);
$team->users()->sync($userIds);
return to_route('admin.teams.index')
return to_route('teams.index')
->with('status', "{$team->name} members were updated.");
}
public function updateUserPermissions(
SyncTeamUserPermissionsRequest $request,
Team $team,
User $user,
): RedirectResponse {
abort_unless(
$this->permissionService->allows($request->user(), Permission::TeamManage, $team),
403,
);
$validated = $request->validated();
$permissions = Arr::get($validated, 'permissions', []);
$this->permissionService->sync($user, $team, $permissions);
return to_route('teams.index')
->with('status', "{$user->name} permissions on {$team->name} were updated.");
}
}
+14 -1
View File
@@ -2,11 +2,17 @@
namespace App\Http\Middleware;
use App\Actions\Permissions\PermissionService;
use App\Enums\Permission;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function __construct(
private readonly PermissionService $permissionService,
) {}
/**
* The root template that's loaded on the first page visit.
*
@@ -35,11 +41,18 @@ class HandleInertiaRequests extends Middleware
*/
public function share(Request $request): array
{
$user = $request->user();
return [
...parent::share($request),
'name' => config('app.name'),
'auth' => [
'user' => $request->user(),
'permissions' => [
'canManageTeams' => $user !== null && $this->permissionService->canManageAnyTeam($user),
'globalAdmin' => $user !== null && $this->permissionService->allows($user, Permission::GlobalAdmin),
'projectCreate' => $user !== null && $this->permissionService->allows($user, Permission::ProjectCreate),
],
'user' => $user,
],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
];
@@ -2,10 +2,12 @@
namespace App\Http\Requests\Admin;
use App\Enums\Permission;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateUserAdminStatusRequest extends FormRequest
class SyncUserGlobalPermissionsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
@@ -23,7 +25,12 @@ class UpdateUserAdminStatusRequest extends FormRequest
public function rules(): array
{
return [
'is_admin' => ['required', 'boolean'],
'permissions' => ['nullable', 'array'],
'permissions.*' => [
'string',
'distinct',
Rule::in(Permission::values(Permission::globalAssignable())),
],
];
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ class DeleteProjectRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
return $this->user()?->can('access-admin') ?? false;
}
/**
+1 -1
View File
@@ -14,7 +14,7 @@ class StoreProjectRequest extends FormRequest
*/
public function authorize(): bool
{
return $this->user() !== null;
return $this->user()?->can('create-project') ?? false;
}
protected function prepareForValidation(): void
@@ -2,7 +2,7 @@
namespace App\Http\Requests;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Enums\Permission;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -29,7 +29,7 @@ class SyncProjectPermissionsRequest extends FormRequest
'permissions.*' => [
'string',
'distinct',
Rule::in(ProjectPermissionService::permissions()),
Rule::in(Permission::values(Permission::projectAssignable())),
],
];
}
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use App\Enums\Permission;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SyncTeamUserPermissionsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user() !== null;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'permissions' => ['nullable', 'array'],
'permissions.*' => [
'string',
'distinct',
Rule::in(Permission::values(Permission::teamAssignable())),
],
];
}
}
@@ -13,7 +13,7 @@ class UpdateTeamMembersRequest extends FormRequest
*/
public function authorize(): bool
{
return $this->user()?->can('access-admin') ?? false;
return $this->user() !== null;
}
/**
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use App\Enums\Permission;
use Database\Factories\PermissionAssignmentFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
#[Fillable(['permission', 'assignee_type', 'assignee_id', 'scope_type', 'scope_id'])]
class PermissionAssignment extends Model
{
/** @use HasFactory<PermissionAssignmentFactory> */
use HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'permission' => Permission::class,
];
}
public function assignee(): MorphTo
{
return $this->morphTo();
}
public function scope(): MorphTo
{
return $this->morphTo();
}
}
+13
View File
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Str;
#[Fillable(['name', 'key'])]
@@ -16,6 +17,13 @@ class Project extends Model
/** @use HasFactory<ProjectFactory> */
use HasFactory;
protected static function booted(): void
{
static::deleting(function (self $project): void {
$project->permissionAssignments()->delete();
});
}
protected function key(): Attribute
{
return Attribute::make(
@@ -23,6 +31,11 @@ class Project extends Model
);
}
public function permissionAssignments(): MorphMany
{
return $this->morphMany(PermissionAssignment::class, 'scope');
}
public function boardColumns(): HasMany
{
return $this->hasMany(BoardColumn::class)
+19
View File
@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
#[Fillable(['name'])]
class Team extends Model
@@ -14,6 +15,24 @@ class Team extends Model
/** @use HasFactory<TeamFactory> */
use HasFactory;
protected static function booted(): void
{
static::deleting(function (self $team): void {
$team->permissionAssignments()->delete();
$team->scopedPermissionAssignments()->delete();
});
}
public function permissionAssignments(): MorphMany
{
return $this->morphMany(PermissionAssignment::class, 'assignee');
}
public function scopedPermissionAssignments(): MorphMany
{
return $this->morphMany(PermissionAssignment::class, 'scope');
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)
+13 -1
View File
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
@@ -20,6 +21,13 @@ class User extends Authenticatable
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable, TwoFactorAuthenticatable;
protected static function booted(): void
{
static::deleting(function (self $user): void {
$user->permissionAssignments()->delete();
});
}
/**
* Get the attributes that should be cast.
*
@@ -29,12 +37,16 @@ class User extends Authenticatable
{
return [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
'password' => 'hashed',
'two_factor_confirmed_at' => 'datetime',
];
}
public function permissionAssignments(): MorphMany
{
return $this->morphMany(PermissionAssignment::class, 'assignee');
}
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class)
+17 -1
View File
@@ -2,8 +2,13 @@
namespace App\Providers;
use App\Actions\Permissions\PermissionService;
use App\Enums\Permission;
use App\Models\Project;
use App\Models\Team;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
@@ -40,7 +45,18 @@ class AppServiceProvider extends ServiceProvider
*/
protected function configureAuthorization(): void
{
Gate::define('access-admin', fn (User $user): bool => $user->is_admin);
Relation::enforceMorphMap([
'project' => Project::class,
'team' => Team::class,
'user' => User::class,
]);
/** @var PermissionService $permissionService */
$permissionService = $this->app->make(PermissionService::class);
Gate::define('access-admin', fn (User $user): bool => $permissionService->allows($user, Permission::GlobalAdmin));
Gate::define('create-project', fn (User $user): bool => $permissionService->allows($user, Permission::ProjectCreate));
Gate::define('manage-teams', fn (User $user): bool => $permissionService->canManageAnyTeam($user));
}
/**