社内システムの管理者権限をspatie/laravel-permissionで設計したときのメモです。権限を1つずつ手で書き並べるのではなく、リソースと操作の掛け算で生成しています。
権限を手で書き並べると、増えたときに辛い
管理対象のリソース(顧客、契約、料金プラン、オプションなど)が9種類、操作(閲覧・作成・更新・削除)が4種類あるので、単純に組み合わせると36個の権限があります。これを1つずつ
Permission::create(['name' => 'customers.view']);
Permission::create(['name' => 'customers.create']);
// ... 34行続く
と書くと、リソースを1つ増やすたびに4行足す作業が発生し、書き漏れにも気づきにくくなります。
対処: 二重ループで生成する
// database/seeders/RolePermissionSeeder.php
public function run(): void
{
$resources = [
'customers', 'contracts', 'features', 'customer-features',
'site-settings', 'plans', 'options', 'support-hour-packages', 'payments',
];
$actions = ['view', 'create', 'update', 'delete'];
foreach ($resources as $resource) {
foreach ($actions as $action) {
Permission::firstOrCreate(['name' => "{$resource}.{$action}"]);
}
}
$superAdmin = Role::firstOrCreate(['name' => 'SuperAdmin']);
$superAdmin->syncPermissions(Permission::all());
// Staffは削除操作のみ権限を持たない(誤操作による顧客データ消失を防ぐ)
$staff = Role::firstOrCreate(['name' => 'Staff']);
$staffPermissions = Permission::where('name', 'not like', '%.delete')->get();
$staff->syncPermissions($staffPermissions);
}
リソースを1つ足したいときは、$resources配列に文字列を1行追加するだけで、そのリソースの4つの権限(view/create/update/delete)が自動的に生成されます。権限の命名規則(リソース名.操作名)を統一しておくことで、ループでの生成が可能になっています。
ロールは「全部から引く」形で定義する
SuperAdminにはPermission::all()で全権限を渡し、Staffには**「削除以外」という条件**で絞り込んだものを渡しています。
$staffPermissions = Permission::where('name', 'not like', '%.delete')->get();
ここがこの設計の要点です。「Staffに何を許可するか」を列挙するのではなく、**「全権限から、許可しないものを引く」**という発想にしています。理由は、1人体制に近い小規模な組織では、権限の差は「削除操作ができるかどうか」の1点に集約されることが多いからです。
もし将来、リソースが増えても、not like '%.delete'という条件はそのまま機能します。新しいリソースの権限が生成されても、.deleteで終わる部分だけが自動的にStaffの権限から除外されるので、ロール定義側のコードは一切変更不要です。
逆に「リソースごとに列挙する」設計を選んでいたら、リソースを1つ増やすたびに、権限の生成コードとロールへの割り当てコードの両方を直す必要が出てきます。
firstOrCreateとsyncPermissionsで冪等にする
このシーダーは、何度実行しても同じ結果になるように書かれています。
-
Permission::firstOrCreate(...): 既に存在する権限は再作成しない -
Role::firstOrCreate(...): 既に存在するロールは再作成しない -
syncPermissions(...): ロールの権限を「渡した集合に完全に一致させる」(既存の余分な権限があれば外し、足りない権限があれば足す)
syncPermissionsが冪等な同期処理になっているおかげで、権限の定義($resourcesや$actions)を後から変更してシーダーを再実行するだけで、DBの状態を最新の定義に追従させられます。手動での付け外しが不要です。
この設計が向いているケース
- ロールの種類が少なく(今回は2つ)、権限の差が単純なルール(「削除だけ禁止」等)で表現できる
- リソースの追加・削除が今後も発生する見込みがある
逆に、ロールごとに全く異なる権限体系が必要な場合(例えば「経理担当は支払い関連だけ、営業担当は顧客関連だけ」のような縦割り)は、この「全部から引く」方式は合わず、リソースごとに許可リストを持たせる方が素直です。業務の実態に合わせてどちらの形にするかを選ぶ必要があります。
まとめ
- 権限が「リソース×操作」の掛け算になるなら、手で列挙せず二重ループで生成する
- ロールの権限差が単純なルールで表現できるなら、「列挙」ではなく「全部から条件で引く」形にすると、リソース追加時の変更箇所が減る
- シーダーは
firstOrCreate+syncPermissionsで冪等にし、定義変更のたびに再実行するだけで済むようにする
この記事はZennにも投稿しています。