⌘K

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.

Týmy a dvoufázové ověření preview
Na této stránce

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.php
use Laravel\Fortify\Features;
 
'features' => [
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
]),
],
// app/Models/User.php
use 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.php
use 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ýmu
php 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.php
use 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.php
public 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. SetCurrentTeam běží 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.php
namespace 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.php
return [
'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.php
Schema::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á