Role i uprawnienia (RBAC)
View on GitHubDodaj nowy poziom roli systemowej
Przećwiczony przykład: dodanie piątego poziomu, Contributor, znajdującego się pomiędzy Member a Viewer — może widzieć wszystko to, co Member, oraz tworzyć issues/komentarze, ale nie może niczego usuwać ani zarządzać członkami/rolami/ustawieniami.
Poziom to po prostu rola systemowa (roles.is_system = true,
roles.role = <wartość RoleType>), której dokładnie jeden wiersz ma
każdy projekt, więc dodanie go to głównie dane: nowy przypadek
RoleType, jego domyślny zestaw uprawnień w RoleService oraz
migracja danych tworząca nową rolę systemową w każdym istniejącym
projekcie (nowe projekty dostają ją automatycznie z
ensureSystemRoles()). Nie ma list poziomów do utrzymywania w
Policy ani Controllerach — każde sprawdzenie to
Project::hasPermission($user, Permission::X), więc nowy poziom jest
autoryzowany wyłącznie uprawnieniami swojej roli. Zmiana schematu też
nie jest potrzebna: roles.role to zwykły string, więc nowy przypadek
RoleType jest poprawny w momencie, gdy enum o nim wie.
Po znacznie krótszy przewodnik o zabezpieczaniu nowej akcji zobacz
./01-add-a-new-permission.mdRole i uprawnienia (RBAC)Dodaj nowe uprawnienieOgólny, krok-po-kroku wzorzec dodawania dowolnego nowego uprawnienia projects./issues./comments. i sprawienia, żeby poprawnie pojawiło się w Settings → Rol….
Krok 1 — Dodaj przypadek enuma
Plik: app/Enums/Permissions/RoleType.php
<?php
namespace App\Enums\Permissions;
enum RoleType: string
{
case OWNER = 'owner';
case ADMIN = 'admin';
case MEMBER = 'member';
case CONTRIBUTOR = 'contributor';
case VIEWER = 'viewer';
case CUSTOM = 'custom';
}
Krok 2 — Zarejestruj go jako poziom systemowy i zdecyduj o domyślnych uprawnieniach
Plik: app/Services/RoleService.php
Dodaj przypadek do SYSTEM_ROLE_TYPES (to właśnie sprawia, że
ensureSystemRoles() tworzy/zasiewa wiersz roles dla niego w każdym
projekcie) i nadaj mu własną stałą *_DEFAULT_PERMISSIONS:
private const array SYSTEM_ROLE_TYPES = [
RoleType::OWNER,
RoleType::ADMIN,
RoleType::MEMBER,
RoleType::CONTRIBUTOR,
RoleType::VIEWER,
];
/**
* A step down from Member: can view and contribute (issues, comments)
* but can't delete anything or touch members/roles/settings.
*/
private const array CONTRIBUTOR_DEFAULT_PERMISSIONS = [
Permission::PROJECT_VIEW,
Permission::MEMBERS_VIEW,
Permission::ROLES_VIEW,
Permission::INTEGRATIONS_VIEW,
Permission::ISSUES_VIEW,
Permission::ISSUES_CREATE,
Permission::ISSUES_UPDATE,
Permission::COMMENTS_CREATE,
Permission::COMMENTS_UPDATE_OWN,
];
Następnie dodaj odpowiednią gałąź do defaultPermissionIdsFor():
private function defaultPermissionIdsFor(RoleType $role): array
{
$keys = match ($role) {
RoleType::OWNER, RoleType::ADMIN => array_map(fn (Permission $permission) => $permission->value, Permission::cases()),
RoleType::MEMBER => array_map(fn (Permission $permission) => $permission->value, self::MEMBER_DEFAULT_PERMISSIONS),
RoleType::CONTRIBUTOR => array_map(fn (Permission $permission) => $permission->value, self::CONTRIBUTOR_DEFAULT_PERMISSIONS),
RoleType::VIEWER => array_map(fn (Permission $permission) => $permission->value, self::VIEWER_DEFAULT_PERMISSIONS),
RoleType::CUSTOM => [],
};
return PermissionModel::query()->whereIn('key', $keys)->pluck('id')->all();
}
Nic więcej w RoleService nie musi się zmienić —
ensureSystemRoles(), syncSystemRoleForMember(), createRole(),
syncPermissions() oraz zabezpieczenia dla Ownera/ról systemowych
operują na SYSTEM_ROLE_TYPES/RoleType w sposób ogólny.
Krok 3 — Policy nie wymagają zmian
W przeciwieństwie do starego modelu z listami poziomów, nie ma tu nic
do przeglądania: IssuePolicy, CommentPolicy, ProjectPolicy i
SettingsController wywołują wyłącznie hasPermission(), więc
Contributor może tworzyć issues i komentarze (oraz edytować własne
komentarze), ponieważ CONTRIBUTOR_DEFAULT_PERMISSIONS z kroku 2
zawiera ISSUES_CREATE, ISSUES_UPDATE, COMMENTS_CREATE i
COMMENTS_UPDATE_OWN, a nie może niczego usuwać ani zarządzać, bo
tych uprawnień nie zawiera. Decyzja, którą kiedyś trzeba było
powtarzać w każdej Policy, żyje teraz w jednym miejscu: w domyślnym
zestawie uprawnień wybranym w kroku 2 (który administratorzy mogą
potem dostosować per projekt w Settings → Roles — edycja dowolnej
roli systemowej poza Owner naprawdę zmienia to, co mogą jej
członkowie).
Dwa miejsca nadal patrzą na sam string poziomu, przez
Project::roleOf(): restricted_role_types typu issue
(IssuePolicy::createOfType()) przechowuje stringi poziomów, więc nowy
poziom po prostu staje się dodatkową wartością, którą admin może tam
wybrać, oraz kod wyświetlający (odznaki, listy rozwijane) odczytujący
poziom. Sprawdź obsługę restricted_role_types, jeśli Contributor
ma być tam wybieralny.
Krok 4 — Spraw, by dało się go przypisać
Plik: app/Http/Controllers/ProjectMemberController.php oraz
app/Http/Controllers/ProjectInvitationController.php
Oba już walidują przychodzącą rolę za pomocą
Rule::enum(RoleType::class)->except([RoleType::OWNER, RoleType::CUSTOM])
— czyli listą wykluczeń, nie listą dozwolonych, więc zupełnie nowy
przypadek jest przypisywalny automatycznie. Nic tu nie trzeba zmieniać,
chyba że chcesz, aby Contributor był nieprzypisywalny przez te
przepływy (mało prawdopodobne), w którym to przypadku dodaj go do
tablicy except([...]) w obu plikach.
Krok 5 — Typy i motyw na froncie
Plik: resources/js/types/Roles.ts
export type RoleTypeValue =
| 'owner'
| 'admin'
| 'member'
| 'contributor'
| 'viewer'
| 'custom';
Plik: resources/js/types/ProjectMembers.ts
export type ProjectMemberRole =
| 'owner'
| 'admin'
| 'member'
| 'contributor'
| 'viewer';
export type AssignableProjectMemberRole = Exclude<ProjectMemberRole, 'owner'>;
Plik: resources/js/utils/roleTheme.ts — dodaj wpis contributor do
ROLE_TYPE_THEME (to właśnie stąd każdy odznaka roli, lista rozwijana
poziomu i pasek boczny ról odczytują etykietę/kolor/ikonę — pomiń to,
a Contributor wyrenderuje się z niezdefiniowanym stylem wszędzie tam,
gdzie jest poprawnie otypowany, ale nie ma wpisu w motywie):
contributor: {
label: 'Contributor',
dot: 'bg-teal-400',
ring: 'stroke-teal-400',
badgeClass: 'bg-teal-400/10 text-teal-400',
gradient: 'from-teal-400/20 to-teal-400/5',
icon: 'PenLine',
},
Krok 6 — Uzupełnij rolę systemową w istniejących projektach
ensureSystemRoles() tworzy rolę Contributor w projekcie przy
najbliższym uruchomieniu (utworzenie projektu, zmiana roli członka,
zaproszenie), ale do tego czasu istniejący projekt po prostu nie ma
roli Contributor i admin nie mógłby jej wybrać. Utwórz ją wszędzie
migracją danych, na wzór
database/migrations/2026_10_13_100000_backfill_system_roles_for_existing_members.php:
return new class extends Migration
{
public function up(): void
{
$roleService = app(RoleService::class);
Project::query()->chunkById(100, function ($projects) use ($roleService) {
foreach ($projects as $project) {
$roleService->ensureSystemRoles($project);
}
});
CacheVersion::bump(CacheVersion::MEMBERSHIP);
}
public function down(): void {}
};
ensureSystemRoles() jest idempotentne: zasiewa domyślne uprawnienia
tylko dla roli, którą właśnie utworzyło, więc istniejące role
Owner/Admin/Member/Viewer — i wszelkie ich dostosowania przez adminów —
zostają nietknięte. Ta migracja nikomu nie przypisuje nowej roli;
członkowie przechodzą na nią zwykłym przepływem zmiany roli
(RoleService::syncSystemRoleForMember()).
Sama tabela permissions nie wymaga żadnych zmian dla nowego poziomu
(jest już w pełni zasiana przez PermissionSeeder, zobacz
./01-add-a-new-permission.mdRole i uprawnienia (RBAC)Dodaj nowe uprawnienieOgólny, krok-po-kroku wzorzec dodawania dowolnego nowego uprawnienia projects./issues./comments. i sprawienia, żeby poprawnie pojawiło się w Settings → Rol…
krok 6). Aby zobaczyć rolę od razu w istniejącym lokalnym projekcie
bez migrowania:
php artisan tinker --execute="app(App\Services\RoleService::class)->ensureSystemRoles(App\Models\Project::first())"
# or, inside Docker:
make shell
php artisan tinker --execute="app(App\Services\RoleService::class)->ensureSystemRoles(App\Models\Project::first())"
Krok 7 — Testy
tests/Feature/RoleServiceTest.php— test'it creates the owner, admin, member and viewer system roles with their default permissions'asercuje dokładną listę kluczy poziomów (['owner', 'admin', 'member', 'viewer']), a'it is idempotent and does not duplicate system roles'dokładną liczbę "4 roli systemowych" — oba wymagają aktualizacji do 5/z uwzględnieniemcontributor.tests/Feature/ProjectMemberServiceTest.php/tests/Feature/ProjectMemberControllerTest.php— dodaj test "can promote a member to contributor" na wzór istniejącego "can promote a member to admin" i potwierdź, żesyncSystemRoleForMemberpoprawnie podmienia pivot dla nowego poziomu. W testach dodawaj członków przezaddProjectMember($project, $userId, 'contributor')ztests/Pest.php(wiersz członkostwa + rola systemowa), nigdy przezattach(['role' => ...]).tests/Feature/Models/RoleTest.php— jeśli gdzieś wylicza przypadkiRoleType, dodaj Contributora do oczekiwanej listy.- Zachowanie samego poziomu jest pokryte przez testy Controllerów
akcji, które może/nie może wykonywać — dodaj "a contributor can
create an issue but cannot delete one" do
tests/Feature/IssueControllerTest.php, na wzór istniejących testów dla poziomu Member. Żadna Policy nie została zmieniona, więc to ćwiczyCONTRIBUTOR_DEFAULT_PERMISSIONSod początku do końca. resources/js/utils/roleTheme.test.ts(jeśli istnieje) lub którykolwiek test iterujący poROLE_TYPE_THEME/RoleTypeValue— dodajcontributor, żeby brakujący wpis w motywie zawiódł głośno zamiast renderować się pusto.
