Role i uprawnienia (RBAC)
View on GitHubDodaj nowe uprawnienie
Ogólny, krok-po-kroku wzorzec dodawania dowolnego nowego uprawnienia projects.*/issues.*/comments.* i sprawienia, żeby poprawnie pojawiło się w Settings → Roles & management — decydowanie, kto może co robić w projekcie, niezależnie od tego, do jakiego obszaru funkcjonalnego uprawnienie należy. Przećwiczony przykład: dwa uprawnienia, których system integracji faktycznie dziś używa, projects.integrations.view i projects.integrations.update — ale każdy krok poniżej stosuje się dosłownie do uprawnienia dla dowolnej innej funkcji.
Krok 1 — Dodaj przypadek enuma
Plik: app/Enums/Permissions/Permission.php
enum Permission: string
{
// ... existing cases ...
case SETTINGS_VIEW = 'projects.settings.view';
case SETTINGS_UPDATE = 'projects.settings.update';
case INTEGRATIONS_VIEW = 'projects.integrations.view';
case INTEGRATIONS_UPDATE = 'projects.integrations.update';
}
Struktura kropek w wartości string ma znaczenie na froncie (zobacz krok 5) — group.subgroup.action renderuje się jako karta "Group" z sekcją "Subgroup" w środku. Trzymaj się istniejących prefiksów projects., issues., comments., chyba że wprowadzasz naprawdę nowy obszar najwyższego poziomu.
Krok 2 — Zdecyduj, kto dostaje je domyślnie
Plik: app/Services/RoleService.php
Każdy projekt ma cztery role systemowe (Owner, Admin, Member, Viewer). Owner i Admin dostają każde istniejące uprawnienie — defaultPermissionIdsFor() przyznaje im pełne Permission::cases(), więc w RoleService nie musisz niczego dotykać dla tych dwóch poziomów w nowych projektach. Projekty, w których role systemowe już istnieją, nie są jednak uzupełniane automatycznie — zobacz krok 6, jak to naprawić jednym poleceniem. Decydujesz, co domyślnie dostają Member i Viewer, dodając nowy przypadek(i) do odpowiedniej stałej *_DEFAULT_PERMISSIONS:
private const array MEMBER_DEFAULT_PERMISSIONS = [
Permission::PROJECT_VIEW,
Permission::MEMBERS_VIEW,
Permission::ROLES_VIEW,
Permission::SETTINGS_VIEW,
Permission::INTEGRATIONS_VIEW, // <- added
Permission::ISSUES_VIEW,
// ...
];
private const array VIEWER_DEFAULT_PERMISSIONS = [
Permission::PROJECT_VIEW,
Permission::MEMBERS_VIEW,
Permission::ROLES_VIEW,
Permission::INTEGRATIONS_VIEW, // <- added
Permission::ISSUES_VIEW,
];
Zasada stosowana w całym tym repozytorium: dawaj Member/Viewer domyślnie połowę *_VIEW pary, nigdy połowy *_UPDATE/*_CREATE/*_DELETE — zdolności mutujące są tylko dla Owner/Admin (albo nadawane jawnie przez rolę niestandardową), chyba że masz konkretny powód, by otworzyć je szerzej. Te wartości domyślne są zasiewane tylko wtedy, gdy rola systemowa projektu jest tworzona po raz pierwszy (ensureSystemRoles()), więc nowy wpis trafia wyłącznie do nowych projektów; istniejące projekty zachowują to, co skonfigurowali ich administratorzy.
Krok 3 — Dodaj sprawdzenie autoryzacji
Dwie opcje, wybierz w zależności od tego, czy zdolność dotyczy istniejącego modelu, czy całego obszaru funkcjonalności:
Opcja A — dodaj metodę do istniejącej Policy (najczęstsza). Plik: app/Policies/ProjectPolicy.php:
public function viewIntegrations(User $user, Project $project): bool
{
return $project->hasPermission($user, Permission::INTEGRATIONS_VIEW);
}
public function updateIntegrations(User $user, Project $project): bool
{
return $project->hasPermission($user, Permission::INTEGRATIONS_UPDATE);
}
hasPermission() (na App\Models\Project) to jedyne sprawdzenie — nie ma listy dozwolonych poziomów do utrzymywania. Owner zawsze przechodzi; każdy inny członek przechodzi, gdy którakolwiek z jego ról (rola systemowa lub niestandardowa) ma nadane konkretne Permission przez pivot permission_role. To, co mogą Admin/Member/Viewer, wynika więc wyłącznie z uprawnień na tych rolach (wartości domyślne z kroku 2, edytowane w Settings → Roles).
Wywołaj to z kontrolera przez $this->authorize('updateIntegrations', $project); (Laravel automatycznie rozwiązuje ProjectPolicy::updateIntegrations — nie trzeba jawnej rejestracji Policy, zobacz app/Policies/*.php po istniejącą konwencję).
Opcja B — stwórz dedykowaną Policy, gdy zdolność dotyczy tworzenia jeszcze nieistniejącego zasobu potomnego (odzwierciedla app/Policies/RolePolicy.php::create):
$this->authorize('create', [Role::class, $project]);
wraz z
public function create(User $user, Project $project): bool
{
return $this->hasRolesOrSettingsAccess($user, $project, Permission::ROLES_CREATE);
}
Użyj opcji B tylko wtedy, gdy nie ma jeszcze instancji, względem której można sprawdzić (np. "czy ten użytkownik może stworzyć nowy X dla tego projektu", zanim X w ogóle istnieje).
Krok 4 — Udostępnij je na froncie
Plik: app/Http/Controllers/SettingsController.php
Oblicz wartość boolean bezpośrednio z modelu Project (hasPermission) w akcji tej zakładki, która jej potrzebuje — każda zakładka ustawień ma własną akcję (zobacz ../settings-tabs/README.mdZakładki ustawień/settings to nie jedna strona — każda zakładka pod nim (Preferences, Profile, Notifications, Security & access, Export w sekcji Account; Labels, Issue Type…) — i nie przechodź tu przez Policy; Policy tutaj są zarezerwowane do autoryzacji rzeczywistych żądań mutujących ($this->authorize(...) w akcji kontrolera), podczas gdy SettingsController oblicza zwykłe wartości boolean, żeby UI mógł warunkowo renderować:
$hasIntegrationsAccess = $selectedProject?->hasPermission($user, PermissionEnum::INTEGRATIONS_VIEW) ?? false;
$canUpdateIntegrations = $hasIntegrationsAccess
&& $selectedProject->hasPermission($user, PermissionEnum::INTEGRATIONS_UPDATE);
Prywatny helper can() w Controllerze opakowuje to samo wywołanie hasPermission() (bezpieczne wobec braku wybranego projektu) dla najczęstszego przypadku. Dodaj obie wartości do tablicy props Inertia::render(...) tej akcji — i tylko tej, żeby żadna inna zakładka za nie nie płaciła — a potem przeprowadź je przez stronę zakładki (np. Settings/Integrations.tsx) do Twojego komponentu zakładki dokładnie tak, jak robi to już canUpdateIntegrations (zobacz ../integrations/04-frontend-backend-wiring-overview.mdIntegracjePrzegląd połączenia frontend ↔ backendMapa całej funkcji, przydatna do przeczytania przed zagłębieniem się w którykolwiek z pozostałych przewodników. Trzy oddzielne przepływy: czytanie strony u… po pełną listę przekazywania propsów).
Krok 5 — Spraw, żeby dobrze się renderowało w Settings → Roles & management
Uprawnienie pojawi się automatycznie, poprawnie zgrupowane, w chwili gdy zaistnieje w tabeli permissions w bazie danych (zobacz krok 6) — żaden kod grupujący na froncie nie musi się zmienić. Ale jego etykieta i opis wymagają ręcznego wpisu, inaczej renderuje się jako goły, pozbawiony kontekstu wyraz.
Plik: resources/js/utils/permissions.ts — dodaj do PERMISSION_META:
'projects.integrations.view': {
label: 'View integrations',
description: 'See which integrations are connected to the project.',
},
'projects.integrations.update': {
label: 'Manage integrations',
description:
'Connect, configure, or disconnect third-party integrations.',
},
Dlaczego to jest potrzebne: getPermissionLabel() odpytuje tę mapę po pełnym kluczu z kropkami; jeśli brak wpisu, spada do ostatniego segmentu po kropce, uczłowieczonego — więc projects.integrations.view bez wpisu renderuje się jako goły wyraz "View" (kolidujący wizualnie z każdym innym uprawnieniem *.view) i pusty opis. Zawsze dodawaj wpis w tym samym PR/commicie, który dodaje przypadek enuma.
Musisz dotknąć resources/js/utils/permissionGroups.ts (GROUP_META) tylko wtedy, gdy wprowadzasz nową grupę najwyższego poziomu (część przed pierwszą kropką) — projects, issues i comments już mają wpisy. Nowe uprawnienie projects.integrations.* ponownie wykorzystuje istniejący wpis grupy projects i za darmo dostaje własną podsekcję "Integrations", ponieważ getPermissionSection() w resources/js/utils/permissions.ts automatycznie wyprowadza etykietę podsekcji z drugiego segmentu po kropce (humanize(parts[1])) — do tej części mapa nie jest potrzebna.
Krok 6 — Zaseeduj je do bazy danych (nie pomijaj tego!)
database/seeders/PermissionSeeder.php przechodzi po Permission::cases() i robi updateOrCreate na wierszu dla każdego przypadku — to właśnie sprawia, że nowy przypadek enuma jest faktycznie odpytywalny przez PermissionModel/PermissionService::getAll(). Nie jest uruchamiany automatycznie ponownie, gdy dodajesz przypadek enuma:
- Testy są w porządku —
tests/Pest.phpwywołuje$this->seed(PermissionSeeder::class)w globalnymbeforeEach, więc każde uruchomienie testów zawsze ma każdy aktualny przypadek enuma jako wiersz. - Twoja lokalna baza deweloperska (i każde prawdziwe środowisko) nie jest w porządku, dopóki nie uruchomisz tego sam:
Jest to idempotentne (php artisan db:seed --class=PermissionSeeder # or, inside Docker: make shell php artisan db:seed --class=PermissionSeederupdateOrCreatekluczowane pokey) — bezpieczne do uruchamiania tak często, jak chcesz, także na produkcji po deployu, który dodaje nowe przypadki uprawnień.
Zasianie tworzy tylko wiersz w permissions. Role systemowe Owner i Admin w istniejących projektach nie dostają go automatycznie (są wypełniane tylko przy pierwszym utworzeniu), więc przyznaj je też im:
php artisan permissions:grant-to-existing-roles projects.integrations.view projects.integrations.update
# default is --roles=owner,admin; the command is additive and safe to re-run
(Zaimplementowane w app/Console/Commands/GrantPermissionsToExistingRoles.php.) Jeśli to pominiesz, Admini istniejących projektów nie dostaną nowej zdolności, dopóki ktoś nie zaznaczy jej w Settings → Roles. Owner nigdy nie jest dotknięty — przechodzi każde sprawdzenie hasPermission() bezwarunkowo.
Jeśli pominiesz samo zasianie, getPermissionLabel/cały interfejs Roles nie będzie "zepsuty" — uprawnienie po prostu w ogóle nie będzie istnieć jako wiersz, więc po cichu nigdy nie pojawi się na liście, i żadna rola nigdy nie będzie mogła go otrzymać. Jeśli kiedykolwiek zobaczysz "dodałem uprawnienie, ale nigdzie go nie ma w UI", to prawie zawsze jest tego powodem — sprawdź przez:
php artisan tinker --execute="App\Models\Permission::where('key', 'like', 'projects.integrations%')->get(['key','group'])"
Krok 7 — Testy
tests/Feature/PermissionSeederTest.php— nie trzeba zmian, już asertuje, że seeder jest idempotentny i pokrywa wszystkie aktualne przypadki generycznie.tests/Feature/RoleServiceTest.php— test'it creates the owner, admin, member and viewer system roles with their default permissions'asertuje dokładne zbiory id uprawnień na poziom; jeśli dodałeś nowy przypadek doMEMBER_DEFAULT_PERMISSIONS/VIEWER_DEFAULT_PERMISSIONS, zaktualizuj tam oczekiwaną listę.- Jakąkolwiek metodę Policy dodałeś — pokryj ją bezpośrednio, jeśli ma ciekawe rozgałęzienia, albo (częściej) jest pokryta pośrednio przez test kontrolera dla akcji, którą chroni (zobacz test "member without the integrations.update permission cannot toggle an integration" w
tests/Feature/ProjectIntegrationControllerTest.phppo kształt do skopiowania). tests/Feature/SettingsControllerTest.php— dodaj testy'settings page grants a[n] X access to Y'uderzające we własną trasę zakładki (/settings/<tab>) i asertujące nowy prop boolean dla Owner/Admin (true), zwykłego Membera (tylko podgląd: view=true, update=false) i Vievera bez zsynchronizowanej roli systemowej (false) — zobacz trzy istniejące już testy*integrations*po dokładny wzorzec.
