Modules
Týmy a dvoufázové ověření
Dvoufázové ověření a role po týmech zapnuté v aplikaci, která už tenhle panel má — co vlastní Fortify, co permission vrstva a těch pár řádků, které jsou vaše.
Karta na profilu s klíčem zaregistrovaným skutečnou ceremonií WebAuthn. Přidání otevře vlastní dialog platformy, odebrání jde přes vlastní akci balíčku.
Na této stránce
- Jak to funguje
- Jak zapnout dvoufázové ověření
- Odpovědět za sebe
- Jak zapnout týmy
- Správci týmů a administrátoři
- Role v týmu a super-admin nad nimi
- Kdo koho vidí na obrazovce uživatelů
- Kdo vidí které role
- Přístup složený z balíčků oprávnění
- Odkud se přepínač bere
- Týmy uvnitř firmy
- Passkeys na profilu
- Rozšířený příklad
- Co vlastní který balíček
- Změna dvoufázového ověření nebo passkeys si znovu vyžádá heslo
- Související
Dvě funkce, o které se každá administrace dřív nebo později poprosí — a ani jedna z nich se tady neimplementuje.
Dvoufázové ověření vlastní Laravel Fortify. Tajemství, TOTP okno, záložní kódy, výzva při přihlašování a rate limiting kolem ní jsou bezpečnostní plocha s udržovaným vlastníkem. Týmy vlastní permission vrstva — role omezené na tým, sloupec navíc v pivot tabulkách a klíč cache, který ho musí obsahovat.
Permission vrstvou je tady vždycky nyoncode/laravel-permission-extended. Není
to alternativa ke spatie/laravel-permission — vyžaduje ho a rozšiřuje o
wildcard matching, super-admin gate, Blade direktivy a události o změně
oprávnění — a přesně to obrazovky rolí v tomhle frameworku předpokládají. Model
na holém Spatie se záměrně nedetekuje, protože správcovské UI nad polovinou
autorizačního modelu je horší než žádné.
Co obojí záměrně vynechává, je obrazovka. Tenhle modul dodává obrazovku a volá jejich akce, takže panel funkci dostane, aniž by vedle té první stála druhá implementace.
composer require laravel/fortify # dvoufázové ověřenícomposer require nyoncode/laravel-permission-extended # role a týmy
Jak to funguje
Detekce, ne konfigurace. Obě funkce jsou ve výchozím stavu auto a obě
auto hledají tu věc samotnou:
| Funkce | Co auto kontroluje |
Kde |
|---|---|---|
| Dvoufázové ověření | existuje Fortify třída EnableTwoFactorAuthentication a Features::enabled(Features::twoFactorAuthentication()) |
Support\TwoFactor |
| Týmy | nyoncode/laravel-permission-extended je nainstalovaný, config('permission.teams') je true, třída registraru existuje a teams.model se rozřeší |
Support\Teams |
V obou případech dvě podmínky, ne jedna — a obě stály za kontrolu. Fortify bývá
závislostí několika věcí, které aplikace může mít nainstalované z jiných důvodů,
a jeho dvoufázová funkce je řádek v fortify.features, který se dá prostě
vynechat: nainstalované-ale-vypnuté musí číst jako vypnuté, jinak se karta objeví
a její tlačítka spadnou. Týmy se řídí permission.teams, ne vlastním přepínačem,
protože právě tohle nastavení dělá funkci skutečnou: přidává sloupec týmu do
pivot tabulek a do klíče permission cache. Panel, který by s ním nesouhlasil, by
ukazoval přepínač nad autorizací, která omezená není.
Id týmu se pořád nastavuje na Spatie PermissionRegistrar, a není to spor:
rozšiřující balíček přidává chování na model uživatele a registrar dědí beze
změny, takže zůstává tím jediným objektem, který ví, čím je čtení oprávnění
omezené.
Dvoufázové ověření má tři stavy, ne dva. Fortify zapíše tajemství ve chvíli, kdy se vygeneruje QR kód — takže člověk, který panel otevřel a zavřel záložku, ho má a nechrání ho nic:
| Stav | Znamená | Karta ukáže |
|---|---|---|
| vypnuto | žádné tajemství | jedno tlačítko: zapnout |
| nedokončeno | tajemství, nepotvrzené | QR kód, klíč k opsání, šest políček na kód — a cestu ven |
| zapnuto | two_factor_confirmed_at je vyplněné |
záložní kódy a cestu pryč |
Panel, který by tohle modeloval jako boolean, by nechal každé přerušené nastavení
uvízlé v prostředním stavu, zatímco odznak tvrdí, že jsou v bezpečí. Odznak tam
čte „Nedokončeno“ a Vypnout se nabízí i z něj, nejen ze stavu zapnuto —
polorozdělané nastavení musí jít opustit oběma směry, jinak je to past.
Tým omezuje všechno, takže se nastavuje dřív, než ho něco přečte. Se zapnutým
permission.teams je každé čtení role a oprávnění omezené tím id týmu, které
registrar naposledy dostal — takže request, který mu ho nikdy neřekne, vidí
předchozí, což ve workeru nebo v dlouho běžícím procesu znamená cizí. Modul
přidá SetCurrentTeam do middleware skupiny web, a do skupiny, ne za alias,
který si routa vyžádá: stránka, která by alias zapomněla, by autorizovala proti
špatnému týmu a vypadala by naprosto správně.
Přepnutí přesměruje. Tým omezuje každé čtení oprávnění, takže stránka
poskládaná před přepnutím — menu, akce, řádky, které pustila policy — byla
postavená pro tým, ze kterého jste právě odešli. Jediná poctivá odpověď je nový
request a TeamSwitcher ho vyvolá.
Členství se kontroluje při přepnutí, ne jen v seznamu. Přepínač vypíše týmy,
do kterých patříte, a Teams::switchTo() členství ověří znovu, než cokoli uloží.
<select> je markup a markup je to, co doletělo do prohlížeče.
Obrazovky se řídí týmem, nejen autorizace. Spatie omezuje, co člověk smí,
na aktuální tým; obrazovky uživatelů a rolí stejně omezují, co vidí — členy
aktuálního týmu, globální role a role týmu — pokud člověk nepracuje napříč týmy.
Co se smí rozdat a na čí účet se smí sáhnout, jsou dvě další pravidla nad tím,
každé s jedním vlastníkem: Support\RoleGrants a Support\AccountGuard. Sekce
níže je probírají jedno po druhém.
Aktuální tým spadne zpátky, místo aby selhal. Session jeden jmenuje; když jmenuje tým, ve kterém už nejste — nebo zatím nejmenuje nic — použije se první, do kterého patříte. Oba případy jsou běžné a ani jeden by neměl člověka poslat na stránku omezenou na nic.
Jak zapnout dvoufázové ověření
Nainstalujte Fortify, spusťte jeho migraci a zapněte funkci. Nic dalšího se v tomhle panelu nemění:
// config/fortify.phpuse Laravel\Fortify\Features; 'features' => [ Features::twoFactorAuthentication([ 'confirm' => true, 'confirmPassword' => true, ]), ],
// app/Models/User.phpuse Illuminate\Foundation\Auth\User as Authenticatable;use Laravel\Fortify\TwoFactorAuthenticatable; class User extends Authenticatable{ use TwoFactorAuthenticatable; }
Karta se na stránce profilu objeví ve chvíli, kdy platí obojí. 'confirm' => false
se taky respektuje — modul si to čte přímo z nastavení dané Fortify funkce, ne
z kopie toho nastavení — a tajemství pak je celé nastavení, takže stav „nedokončeno“
nenastane.
Kód se na obou stranách dveří zadává do stejného pole. Políčka na téhle kartě
jsou OtpInput z wire-forms — stejné, jaké kreslí přihlašovací výzva: samy
posouvají kurzor, rozeberou vložený kód a odešlou jednu hodnotu. Žádný druhý
názor na to, jak vypadá šest číslic.
Výzva patří té druhé polovině. Tenhle modul vlastní správcovskou kartu — QR kód, záložní kódy, přepínač. Obrazovka, která se ptá na kód během přihlašování, patří modulu přihlášení, vedle přihlašovacího formuláře, a mechanismus pod obojím je tentýž Fortify. To rozdělení je záměr, ne náhoda: tenhle modul si Fortify detekuje a funguje i bez něj, kdežto balíček, jehož jediná práce jsou obrazovky Fortify, ho vyžaduje. Viz modul přihlášení.
Odpovědět za sebe
// config/wire-module-users.php'two_factor' => false, // kartu neukazovat, ať je nainstalované cokoli
'profile' => [ 'two_factor' => false, // držet ji mimo profil a namountovat jinde ],
Po tom druhém sáhněte, když kartu chcete na vlastní stránce zabezpečení —
@livewire(\NyonCode\WireModuleUsers\Livewire\TwoFactorAuthentication::class)
nebere argumenty, protože účet, na kterém pracuje, je vždycky ten přihlášený.
Jak zapnout týmy
Týmy potřebují od vaší aplikace tři věci: nastavení, model týmu a relaci na uživateli, která se k němu dostane. Tenhle modul nedodává ani jednu, protože aplikace, která týmy má, má všechny tři.
Nastavení a pojmenování za vás udělá php artisan wire:install. Jeho krok
Teams se zeptá, zda jsou role vázané na týmy, a při „ano“ publikuje config
níže, pokud ještě není, nastaví teams na true — v souboru i v běžícím procesu
— zapíše WIRE_USERS_TEAM_MODEL a — pokud zadáte jinou — i relaci. Krok Roles &
permissions přijde až po něm a spustí permission-extended:install, který
publikuje migraci, spustí ji a přidá HasRoles na váš model uživatele. Právě o to
pořadí jde: Spatie migrace čte permission.teams ve chvíli, kdy běží, a instalátor
rolí ji spouští, takže zapnout týmy až potom znamená pivotní tabulky bez sloupce
týmu. Tabulky vytvořené bez sloupce se berou jako odpověď „ne“ a znovu se na ně
neptá; zapnout týmy později je migrace, kterou napíšete sami. Model týmu a relaci
na uživateli si pořád píšete sami.
// config/permission.php — vlastní config spatie/laravel-permission, publikovaný// přes závislost rozšiřujícího balíčku'teams' => true, 'team_foreign_key' => 'team_id',
// config/wire-module-users.php'teams' => [ 'enabled' => 'auto', // řídí se permission.teams 'model' => 'App\Models\Team', 'relation' => 'teams', 'label_attribute' => 'name', 'session_key' => 'wire.team',],
// app/Models/User.phpuse Illuminate\Database\Eloquent\Relations\BelongsToMany;use Illuminate\Foundation\Auth\User as Authenticatable;use NyonCode\PermissionExtended\Traits\HasRoles; // tenhle, nikdy Spatie class User extends Authenticatable{ use HasRoles; /** Relace, kterou jmenuje `wire-module-users.teams.relation`. */ public function teams(): BelongsToMany { return $this->belongsToMany(Team::class); } }
To je celé. V horní liště se objeví přepínač pro každého, kdo patří do víc než
jednoho týmu, middleware omezí každé čtení oprávnění v každém web requestu
a Gate::allows() začne odpovídat po týmech, aniž by se změnilo jediné volací
místo.
Správci týmů a administrátoři
Dvě role, které tento modul zná jménem a vytvoří je, když se poprvé přidělují — s
oprávněními obrazovek uživatelů a rolí (users.viewAny, users.view,
users.create, users.update a totéž pro roles, jak je nakonfigurováno),
přesnými jmény, ne users.*, protože přidělený wildcard je jméno a
can('users.viewAny') se ptá na jméno:
# Správce týmu: členové a role týmu 3 a žádného jiného.php artisan wire:assign-role mia@example.com --role=team-admin --team=3 # Administrátor všech týmů najednou: stejná oprávnění, držená globálně.php artisan wire:assign-role ada@example.com --role=admin --global
team-admin (teams.admin_role) je globální role přidělená v týmu, takže její
oprávnění platí jen v tom týmu. admin (admin_role) přidělený s --global
platí ve všech týmech — vidí členy a role všech týmů a obrazovka rolí ho bere
jako někoho, kdo pracuje napříč týmy. Pořád to není super-admin: může, co role
nese, a přijde o to, co se z role odebere. Výchozí oprávnění se zapíšou jen při
vytvoření role; když roli potom upravíte, úpravy zůstanou. --global nebere
--team a nikdy nedá super-admina, na to je --super-admin.
Role přidělovaná jménem se hledá mezi globálními rolemi a rolemi týmu — přednost má role týmu — a nikdy mezi rolemi jiného týmu.
Role v týmu a super-admin nad nimi
Role tu patří týmu: balíček oprávnění ukládá tým ke každému přiřazení a počítá jen ta z aktuálního týmu. Účtu, který v žádném týmu není, ji tedy dát nejde, a tým, jehož členem není, se odmítne, protože role by ležela tam, kde ji přepínač nikdy nenabídne:
php artisan wire:assign-role jana@example.com --role=editor # v jejím aktuálním týmuphp artisan wire:assign-role jana@example.com --role=editor --team=3 # v tomhle
Super-admin je výjimka a nikdy není rolí týmu. Může všechno, ve všech týmech, takže brána balíčku oprávnění uzná jen jeho globální přiřazení — přiřazení uvnitř týmu neobejde nic. Přiděluje se záměrně, z příkazové řádky, a nepotřebuje tým, a proto jím může být i první administrátor nové instalace:
php artisan wire:assign-role admin@example.com --super-admin
Výběr rolí ho nikdy nenabídne, --role=super-admin se odmítne a --super-admin
nebere --team.
Kdo koho vidí na obrazovce uživatelů
Se zapnutými týmy ukazuje obrazovka uživatelů členy aktuálního týmu — seznam, stránky detailu a úprav i každou akci na řádku. Účet jiného týmu otevřený přes URL vrátí 404, ne 403, takže odpověď neprozradí, že existuje, a podvržený klíč z jiného týmu nenajde nic ke smazání. Přepnutí týmu přepne seznam; kdo nepatří do žádného týmu, nevidí nikoho.
Členy všech týmů vidí dva druhy lidí: super-admin a administrátor, jehož
users.viewAny pochází z globální role — přiřazené přes assignGlobalRole(),
jejíž oprávnění platí ve všech týmech. Stejné oprávnění z role jednoho týmu
spravuje jen ten tým. Účet, který založí správce týmu, se do toho týmu přidá;
účet, který založí někdo, kdo pracuje napříč týmy, nepatří do žádného, protože
neřekl, do kterého.
Kdo vidí které role
Obrazovka rolí drží stejnou hranici. Správce týmu vidí globální role — sdílené šablony ke čtení, ne ke změně — a role svého týmu ke změně; role jiného týmu nejsou v seznamu, nejdou otevřít přes URL (404) ani podvrženou akcí na řádku. Roli, kterou správce týmu založí, patří jeho týmu; role, kterou založí někdo, kdo pracuje napříč týmy, je globální, a správce bez týmu nezaloží žádnou. Výběr rolí ve formuláři uživatele nabízí tytéž role. Kdo pracuje napříč týmy, vidí a mění všechny role — kromě dvou popsaných v Uživatelích.
Přístup složený z balíčků oprávnění
Účet může mít libovolný počet rolí a jeho oprávnění jsou jejich sjednocením. Tak se přístup skládá: udělejte malé role, z nichž každá nese jeden balíček oprávnění, a dejte člověku tolik, kolik jeho práce potřebuje. Pro balíčky se nejlépe hodí globální role — nadefinované jednou, použitelné v každém týmu a přidělované po týmech:
use Spatie\Permission\Models\Role; // Nadefinované jednou, globálně (bez týmu).Role::create(['name' => 'bundle-invoices-read'])->givePermissionTo('invoices.view');Role::create(['name' => 'bundle-invoices-write'])->givePermissionTo(['invoices.view', 'invoices.create']);Role::create(['name' => 'bundle-reports'])->givePermissionTo('reports.view');
# Přidělené v týmu — Olga v týmu 3 čte faktury a reporty a nic víc.php artisan wire:assign-role olga@example.com --role=bundle-invoices-read --role=bundle-reports --team=3
Výběr rolí ve formuláři uživatele jich bere víc najednou a pravidlo proti
eskalaci čte také sjednocení: správce týmu, který drží invoices.view přes jeden
balíček a reports.view přes druhý, smí dát balíček, který nese obojí. Balíček se
nikdy nevnořuje do jiné role — každá role nese vlastní oprávnění a člověk sbírá
role.
Odkud se přepínač bere
Není v layoutu shellu a tenhle modul do toho souboru nikdy nesahá. Registruje
view do registru chrome ve wire-core a shell vykreslí, co je zaregistrované —
tentýž šev, jaký používá media picker na druhém konci dokumentu:
// packages/module-users/src/WireModuleUsersServiceProvider.phpuse NyonCode\WireCore\Foundation\View\PageChrome; protected function bootTeams(): void{ if (! Teams::enabled()) { return; } $this->app->make(Kernel::class)->appendMiddlewareToGroup('web', SetCurrentTeam::class); $this->app->make(PageChrome::class)->add( 'wire-module-users::team-switcher', PageChrome::TOPBAR, ); }
PageChrome::TOPBAR je region pro věci, které musí být vidět — přepínač
týmu, výběr tenanta, odznak prostředí. PageChrome::BODY, výchozí, je pro věci,
které musí jen existovat: modal, který otevírá něco jiného, hostitel
potvrzení. Vaše vlastní aplikace se může registrovat do obou, a jakýkoli jiný
balíček taky:
// app/Providers/AppServiceProvider.phppublic function boot(): void{ app(PageChrome::class)->add('partials.environment-badge', PageChrome::TOPBAR); }
Týmy uvnitř firmy
S tenancy je tenant firma a tým jedno z jejích oddělení nebo projektů — člověk může patřit jen do firmy A a do tří z jejích pěti projektů. Udělejte model týmu vlastněný tenantem a zbytek plyne sám:
use NyonCode\WireCore\Core\Tenancy\Concerns\BelongsToTenant; class Team extends Model{ use BelongsToTenant; }
- Přepínač nabídne projekty aktuální firmy, do kterých člověk patří — tenant scope už každý dotaz na týmy zúží na firmu v URL.
- Aktuální projekt se pamatuje pro každou firmu zvlášť, pod
wire.team.{tenant}: přepnutí firmy vás nikdy nezanese do projektu té druhé a po návratu najdete projekt, ve kterém jste skončili. - Role následují firmu.
SetCurrentTeamběží ve skupiněweb, dřív než tenantová zóna do firmy vstoupí, takže modul poslouchá vstup do firmy (TenantEntered) a teprve tehdy nasměruje vrstvu oprávnění na aktuální projekt té firmy.
Roli, která má platit napříč všemi projekty jedné firmy — „admin firmy A“ —, zatím
přiřaďte v každém z nich; role vázané na tenanta jsou v plánu pro
laravel-permission-extended.
Passkeys na profilu
Třetí věc, kterou se dá účet zajistit, a nejnovější: passkey je vlastní
přihlašovací údaj platformy — Touch ID, Windows Hello, telefon, bezpečnostní
klíč. Laravel má celé, přesně jako dvoufázové ověření: laravel/passkeys za
Features::passkeys() ve Fortify přináší ceremonii WebAuthn, řádky s údaji i
klienta do prohlížeče. Tenhle modul přidává kartu.
// config/wire-module-users.php'profile' => [ 'passkeys' => true, // karta tam, kde je funkce zapnutá ], 'passkeys' => 'auto', // 'auto' hledá balíčky se zapnutou funkcí
Dva řádky na user modelu — a právě ty, jejichž chybění je tiché: všechny routy odpovídají a zaregistrovaný klíč nepatří nikomu, takže se karta na ně dívá a řekne, co chybí:
use Laravel\Passkeys\Contracts\PasskeyUser;use Laravel\Passkeys\PasskeyAuthenticatable; class User extends Authenticatable implements PasskeyUser { use PasskeyAuthenticatable; }
Karta vypíše, co účet má, vezme jméno pro nový klíč a otevře dialog platformy, a
odebírá vlastní akcí balíčku — takže se vyvolá PasskeyDeleted pro cokoli, co
naslouchá.
Tlačítko pro přihlášení patří té druhé polovině, vedle přihlašovacího
formuláře, ze stejného důvodu jako dvoufázová výzva. Viz modul
auth — včetně pasti při vývoji: choďte na localhost, nikdy na
127.0.0.1.
Rozšířený příklad
Všechno výše v jedné aplikaci: tabulka uživatelů s fotkou, týmy omezující autorizaci a dvoufázové ověření dostupné každému, kdo o něj stojí.
// app/Models/User.phpnamespace App\Models; use Illuminate\Database\Eloquent\Relations\BelongsToMany;use Illuminate\Foundation\Auth\User as Authenticatable;use Laravel\Fortify\TwoFactorAuthenticatable;use NyonCode\WireCore\Foundation\Contracts\HasAvatar;use NyonCode\PermissionExtended\Traits\HasRoles;use NyonCode\WireModuleUsers\Concerns\InteractsWithAvatar; class User extends Authenticatable implements HasAvatar{ use HasRoles; use InteractsWithAvatar; // fotka, z nakonfigurovaného sloupce use TwoFactorAuthenticatable; // tajemství a záložní kódy, Fortify protected $fillable = ['name', 'email', 'password', 'avatar_path']; protected $hidden = ['password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes']; /** Co vypíše přepínač v liště a proti čemu kontroluje Teams::switchTo(). */ public function teams(): BelongsToMany { return $this->belongsToMany(Team::class); } }
// config/wire-module-users.phpreturn [ 'model' => App\Models\User::class, 'avatar' => [ 'enabled' => 'auto', // rozhoduje sloupec níž 'column' => 'avatar_path', 'disk' => 'public', ], 'two_factor' => 'auto', // rozhoduje Fortify 'teams' => [ 'enabled' => 'auto', // rozhoduje permission.teams 'model' => App\Models\Team::class, 'relation' => 'teams', ], 'profile' => [ 'password' => true, 'two_factor' => true, 'delete_account' => false, ],];
// database/migrations/…_add_profile_columns_to_users_table.phpSchema::table('users', function (Blueprint $table): void { $table->string('avatar_path')->nullable(); });
Nic dalšího. Spusťte php artisan about a panel nahlásí, kterou ze čtyř
volitelných půlek tahle instalace opravdu dostala:
Wire Module Users ....................................................... Avatars ......................................................... enabled Roles ........................................................... enabled Teams ........................................................... enabled Two-factor ...................................................... enabled User model ..................................... App\Models\User
Co vlastní který balíček
| Oblast | Vlastník | Podíl tohohle modulu |
|---|---|---|
Hashování hesla, pravidlo current_password |
Laravel | karta, která se ptá, a udržení session přihlášené po změně |
| Dvoufázová tajemství, TOTP, záložní kódy, výzva při přihlášení | Fortify | třístavová karta, která volá Fortify akce |
| Role, oprávnění, omezení na tým, permission cache, globální role a brána super-admina | nyoncode/laravel-permission-extended (1.1+), nad spatie/laravel-permission, který vyžaduje | obrazovky rolí, to, ve kterém týmu je tenhle request, co obrazovka ukáže z jiných týmů, co smí člověk rozdat (RoleGrants) a na čí účet smí sáhnout (AccountGuard) |
| Přihlášení, registrace, reset hesla, ověření e-mailu | Fortify nebo Breeze | nic — viz admin shell |
| Týmy samotné: tabulka, model, členství | vaše aplikace | přepínač nad tím, co už máte |
Změna dvoufázového ověření nebo passkeys si znovu vyžádá heslo
Fortify dává password.confirm na každou route, která vypíná dvoufázové
ověření, čte záložní kód nebo odebírá passkey — a má to ve výchozím stavu
zapnuté. Tyto karty ovšem volají Fortify akce přes Livewire, čímž se k nim
dostanou mimo ten middleware — takže je ta pojistka zopakovaná přímo na
kartě, místo aby se ztratila. Kdo drží vypůjčenou session, nevypne druhý faktor,
nepřečte TOTP secret ani nesmaže passkey, aniž by znal heslo, které to všechno
chrání.
Čte se stejné okno, jaké zapisuje Laravelí vlastní RequirePassword —
auth.password_confirmed_at proti auth.password_timeout — takže potvrzení
hesla kdekoli v aplikaci vyhoví všem těmto místům a timeout se nastavuje na
jediném místě:
// config/auth.php'password_timeout' => 10800, // v sekundách; Laravel má ve výchozím stavu tři hodiny
Mimo toto okno tlačítka přesměrují na obrazovku pro potvrzení hesla a karta řekne proč, místo aby tiše zobrazila prázdný panel.
Související
- Modul uživatelů — stránka profilu, na které tyhle karty žijí
- Admin shell — auth rám a kam patří přihlašovací obrazovka
- Moduly — co je modul a jak ho balíček dodá