ポリシー
Lase が提供するポリシークラスの一覧と、実装の約束事です。すべて CodebaseJp\Lase\Policies 名前空間にあります。
Laravel の規約による自動解決が効くため、LaseServiceProvider への登録は不要です(CodebaseJp\Lase\Models\TenantUser → CodebaseJp\Lase\Policies\TenantUserPolicy)。
権限の確認は2層
| 対象 | 担当 |
|---|---|
| レコード単位の判定が必要なもの | ポリシー($this->authorize())。ルート側の tenant.can は粗い下限として重ねる |
| 判定対象のレコードがないもの(Senter連携など) | tenant.can ミドルウェア |
ポリシーはレコードに紐づく判定を持ちます。一覧や登録のようにレコードを取らない操作は、権限の確認をミドルウェアに任せます(既存のポリシーに viewAny / create が定義されている場合は、定義と実装が乖離しないよう呼び出します)。
実装の約束事
戻り値
- 拒否理由が「権限がない」の一つだけなら
bool - 拒否理由が複数あるなら
Illuminate\Auth\Access\Responseを返し、Response::deny('理由')でメッセージを添える
テナント境界
テナントに属するレコード(tenant_id を持つモデル)を扱うメソッドは、必ず操作者のテナントと一致することを確認します。
一次防御は BelongsToTenant のグローバルスコープで、テナントのコンテキスト配下ではルートモデルバインディングが他テナントのレコードを解決しません。ポリシー側の確認は二次防御です。スコープを外した経路(withoutGlobalScope('tenant')、明示的な forTenant()、リレーション経由の取得、アプリ側がトレイトを付けていないモデル)でも境界が守られるようにします。
境界を跨いだ場合は Response::denyAsNotFound() を返します。403 を返すと、存在しない ID(ルートモデルバインディングで 404)と区別がついてしまい、他テナントのレコードの存在が分かるためです。
public function delete(User $user, McpServiceToken $mcpServiceToken): Response
{
if (! $user->isTenantUser($mcpServiceToken->tenant_id)) {
return Response::denyAsNotFound('トークンが見つかりません。');
}
if (! $user->hasTenantPermission(TenantPermission::MCP_SERVICE_TOKEN_MANAGE)) {
return Response::deny('MCP Service Token の管理権限が必要です。');
}
return Response::allow();
}isTenantUser() で境界を確認した後は、hasTenantPermission() の第2引数(リソースのテナントID)は同じ比較の繰り返しになるため渡しません。
TenantUserPolicy
| メソッド | 戻り値 | 必要な権限 | 追加の条件 |
|---|---|---|---|
viewAny | bool | user:read | - |
view | Response | user:read | テナント境界 |
create | bool | user:write | - |
update | Response | user:write | テナント境界/自分自身は不可/オーナーはオーナーのみ |
changeRoles | Response | user:write | update と同じ |
delete | Response | user:write | update と同じ |
- 自分自身には行えません。 権限を失って設定を戻せなくなるのを防ぐためです。画面側では自分の行の編集・削除を出さないようにしてください。
- オーナー(
*を持つ役割が割り当てられたユーザー)に対して行えるのはオーナーだけです。 ステータスのSUSPENDEDは実質の利用停止なので、降格・削除と同じ保護をかけています。 update(ステータス)とchangeRoles(役割)は現在同じ条件ですが、アプリ側が別々の条件に上書きできるようメソッドを分けています。
TenantRolePolicy
tenant_roles はテナントを持たない共通テーブルのため、テナント境界の判定はありません。
| メソッド | 戻り値 | 必要な権限 | 追加の条件 |
|---|---|---|---|
viewAny / view | bool | user:read | - |
create / update / delete | bool | user:write | - |
assign | Response | user:write | オーナーの役割を付与できるのはオーナーのみ |
assign は「その役割をユーザーに付与してよいか」を判定します。招待時と役割変更時の両方で呼んでください。
TenantUserInvitationPolicy
| メソッド | 戻り値 | 必要な権限 | 追加の条件 |
|---|---|---|---|
viewAny | bool | user:read | - |
view | Response | user:read | テナント境界 |
create | bool | user:write | - |
update / delete | Response | user:write | テナント境界 |
SubscriptionPolicy
| メソッド | 戻り値 | 必要な権限 | 追加の条件 |
|---|---|---|---|
update | Response | contract:write | テナント境界 |
一覧・登録は現在のテナントに対してのみ行われるため、レコード単位の判定を持ちません。
McpServiceTokenPolicy
| メソッド | 戻り値 | 必要な権限 | 追加の条件 |
|---|---|---|---|
update | Response | mcp.service-tokens:manage | テナント境界/無効化済みは対象外(404) |
delete | Response | mcp.service-tokens:manage | テナント境界 |
update はトークンのローテートに対応します。一覧・発行はレコードを取らないため、権限の確認は tenant.can:mcp.service-tokens:manage が行います。
アプリ側での拡張
ポリシーを継承して条件を追加できます。Gate::policy() で上書き登録してください。
class MyTenantUserPolicy extends TenantUserPolicy
{
public function changeRoles(User $user, TenantUser $tenantUser): Response
{
if (! $user->hasTenantPermission(MyTenantPermission::ROLE_ASSIGN)) {
return Response::deny('役割の割り当て権限が必要です。');
}
return parent::changeRoles($user, $tenantUser);
}
}// AuthServiceProvider
Gate::policy(TenantUser::class, MyTenantUserPolicy::class);