spatie/laravel-permissionを使って雑に権限管理する初歩の初歩
例えばlaravel breezeを使うと認証画面とその後のDashboardまでは高速開発可能であるが、多くの認証型webアプリケーションの場合このユーザーを管理する必要が出てくる。通常ここはスーパーユーザーのみアクセス可能な領域となっているべきである。
そこで、ここでは素のlaravel11をセットアップし、spatie/laravel-permission
を雑に使ってここへのアクセスを管理者に限定するユーザー管理のアクセス制限を行ってみよう。いずれにせよ、この手のライブラリーは事前にプロトタイプを作って要件に適しているかどうかをチェックする必要が多分にあるはずだ。
laravel.buildからさっとbreezeを展開する
curl -s "https://laravel.build/spatie-permission?with=mysql" | bash
./vendor/bin/sail artisan migrate:fresh --seed
./vendor/bin/sail composer require laravel/breeze --dev
./vendor/bin/sail artisan breeze:install blade
ここではblade
を展開している
いつものログイン画面、しかしlaravel12のスターターキットが出るとこれともお別れかもしれないが
ユーザーseederの変更
現在
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
}
}
このようになっている。test@example.com
でログインできるのであるが、これだけだとイマイチピンとこないだろうから、admin@example.com
に変更し、これを管理者アカウントとしよう。さらに
// User::factory(10)->create();
のコメントを外し10人ユーザーを作成する
public function run(): void
{
User::factory(10)->create();
User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@example.com',
]);
}
再度DBを作り直す
./vendor/bin/sail artisan migrate:fresh --seed
ユーザーを確認する
とか。admin@example.comとその他10人ほど作成された。
spatie/laravel-permissionのインストール
まあ普通にcomposerから
composer require spatie/laravel-permission
ライブラリーをいれるのだが
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
して、
- config/permission.php
- database/migrations/xxxx_create_permission_tables.php
を呼びこんでくる。このmigrationはかなり膨大なので読みといてもいいけど基本的には内容を理解しなくても使えるようになっているし、configは大抵の場合カスタムしなければdefaultで動作するので、ここは最初から深く掘る必要は無いと思われる。
使っていく
このライブラリーは細かい権限設定も可能なのであるが、ここでの例は、たとえばbreezeで作成されたユーザーを管理したい場合などで
- http://server/users/{index,create,edit}
などしてみてもいいのだが
- http://server/admin/users/{index,create,edit}
のようにadmin的なprefixを付けてそれ以下をadminロールのみアクセス可能とするという単純なものをまず考えてみよう。
使い出す準備 (User モデルに HasRoles トレイトを追加)
このライブラリーを使うにあたっては、これを必ず行う必要がある。
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
+use Spatie\Permission\Traits\HasRoles;
+
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
+ use HasRoles;
+
/**
* The attributes that are mass assignable.
*
このように追加しておこう、これで基本的には準備ok
admin@example.comにadminロールを当てる
これはseederでまずやってしまう
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
User::factory(10)->create();
$adminUser = User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@example.com',
]);
$adminRole = Role::firstOrCreate(['name' => 'admin']);
$adminUser->assignRole($adminRole);
}
}
このようにしてseedしな直した後でtinker
このように、$user
オブジェクトにhasRole()
というメソッドが増えている。これにより単純にadminかadminじゃないかが理解できるようになっているわけだ。
ユーザーリソースフルコントローラーを作る
php artisan make:controller UserController -m User -r
としたあとで
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return view('users.index', [
'users' => User::all()
]);
}
などとし、
<?php
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
Route::middleware(['auth', 'verified'])->group(function () {
Route::resource('users', UserController::class);
});
require __DIR__.'/auth.php';
などとする。さらに
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Users') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th class="px-6 py-3 bg-gray-50"></th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach ($users as $user)
<tr>
<td class="px-6 py-4 whitespace-no-wrap">
{{ $user->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap">
{{ $user->email }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-right text-sm leading-5 font-medium">
<a href="{{ route('users.show', $user->id) }}" class="text-indigo-600 hover:text-indigo-900">View</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</x-app-layout>
としてユーザー管理的なviewをでっち上げる。これでhttp://your-server/users にアクセスしてみると
こんな感じなるだろう。これに関しては今回作りこまないので、ここから先はどうでもいい
middlewareで制御する
このユーザー一覧的なインターフェースにアクセス可能なのはここまでの流れだと当然role=admin
に限定したいわけだが、今はどのようなユーザーでもアクセスできる。
どのようなユーザーでもアクセス可能
/admin プレフィックスを付けてrole:adminのみ許可する
Route::middleware(['auth', 'verified', 'role:admin'])->prefix('admin')->group(function () {
Route::resource('users', UserController::class);
});
このようにすると、http://your-server/usersはhttp:/your-server/admin/usersに移動するのだが
middlewareを呼びこんでいないのでエラーとなる。これはlaravel11かそれ以前かで異なるのであるが、laravel11であれば
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
のようにbootstrap/app.phpに追加。それ以前はapp/Http/Kernel.php。これでアクセス可能になるだろう
別のユーザーでアクセスしてみる
確認用のユーザーを作ってもいいんだけど、面倒なのでfactoryされたユーザー、どれでもいいので
> User::all('email')
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#2310
all: [
App\Models\User {#6239
email: "admin@example.com",
},
App\Models\User {#6240
email: "crawford90@example.com",
},
App\Models\User {#6241
email: "dach.osborne@example.net",
},
App\Models\User {#6242
email: "felipa42@example.net",
},
App\Models\User {#6243
email: "fzieme@example.org",
},
App\Models\User {#6244
email: "heller.kyle@example.net",
},
App\Models\User {#6245
email: "lubowitz.thalia@example.net",
},
App\Models\User {#6246
email: "nschaefer@example.net",
},
App\Models\User {#6247
email: "olga58@example.org",
},
App\Models\User {#6248
email: "pamela.gulgowski@example.net",
},
App\Models\User {#6249
email: "waters.marquise@example.net",
},
],
}
ランダムに作られているから適当にチョイスする。ここではwaters.marquise@example.net
でログインして /admin/users にアクセスしてみると
403になる
Dashboardにリンクを作る
再びadmin@example.comでログインしよう
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
+ <x-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')">
+ {{ __('Users') }}
+ </x-nav-link>
</div>
</div>
@@ -70,6 +73,9 @@
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
+ <x-responsive-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')">
+ {{ __('Users') }}
+ </x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
こんな感じでレイアウトにメニューを増やすと...
このように「Users」へのリンクが生える。しかしこれに関していえば、これももちろんadminロールのユーザーのみ表示したいわけで一般ユーザーからは消しさりたいわけだ。
これはbladeを使っている場合はとんでもなく簡単で@roleというディレクティブが生えている
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
+ @role('admin')
+ <x-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')">
+ {{ __('Users') }}
+ </x-nav-link>
+ @endrole
</div>
</div>
@@ -70,6 +75,11 @@
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
+ @role('admin')
+ <x-responsive-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')">
+ {{ __('Users') }}
+ </x-responsive-nav-link>
+ @endrole
</div>
<!-- Responsive Settings Options -->
これで以下のように一般ユーザーでログインするとリンクが消滅するはずだ
Usersへのリンクが消滅した
これはbladeの機能なのでInertia.js
を用いる場合は多少工夫が必要である。とくにlayoutなどの部分に関連する機能はapp/Http/Middleware/HandleInertiaRequests.phpあたりで渡してあげないといけないだろう。
以上、簡単な解説
このようにspatie/laravel-permission
に関しては「文字ベース」でロールや権限を定義するため自由自在である反面自由すぎる所もあるのでしっかりと設計する必要がある。また今回はロールしか定義していないが、「記事を編集できる/できない」のようなパーミッションベースでの指定もできるから、興味があればオフィシャルドキュメントをあたっていただきたい
おまけ、複雑な例
public function run(): void
{
// ロールを3つ作成
$roles = [
'admin' => Role::firstOrCreate(['name' => 'admin']),
'editor' => Role::firstOrCreate(['name' => 'editor']),
'user' => Role::firstOrCreate(['name' => 'user']),
];
// `admin` ユーザーを作成し、adminロールを付与
$adminUser = User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@example.com',
]);
$adminUser->assignRole($roles['admin']);
// `special` ユーザーを作成し、すべてのロールを付与
$specialUser = User::factory()->create([
'name' => 'Special User',
'email' => 'special@example.com',
]);
$specialUser->syncRoles($roles); // 全ロールを適用
// 10人の一般ユーザーを作成し、ランダムなロールを割り当てる
User::factory(10)->create()->each(function ($user) use ($roles) {
// ランダムに1つのロールを割り当てる
$randomRole = collect($roles)->random();
$user->assignRole($randomRole);
});
}
このように
- admin
- editor
- user
という3つのロールを当て、ランダムに割り当てる。
User::with('roles')->get()->map(function ($user) {
return [
'email' => $user->email,
'role' => $user->roles->pluck('name')->implode(', '),
];
});
このようにすると
こうなる。
specialユーザーに見られるように、ロールを複数持つ事も可能なので、この辺こそが設計力を試される所だろう。
また、どういうロールがシステムに登録されているかは
Spatie\Permission\Models\Role::all()->pluck('name');
とかでわかる。
この辺りの複雑な取得は以下を参照されたい
Discussion