s平面の左側

左側なので安定してます(制御工学の話は出てきません)

Laravel 5.4 におけるユーザ権限管理の実装に関する考察(途中まで)

背景

業務において、同じチームのメンバーがプルリクエストを WIP で出し「こんな方法でいいか?」という旨の相談を投げかけてきた。

それについて議論になったことがあるので、それについて書いておく。

そのときの論点は大きく分けては 2 つあったのだが、今回はその 1 つについて書く。 もう 1 つは気が向いたときに書くかもしれない。

※以下はあくまで私個人の意見であり、「こうすべき」と言っているのではないことに留意。意見・ツッコミ等は歓迎。

実現したいこと

前提:Laravel 5.4 で開発している web システム

ユーザごとに「このデータは編集できる」「このデータは閲覧のみ可」といった権限管理をしたい。

登場するテーブルは以下の2つ。

(テーブル名は実際のものとは異なる。あくまでイメージ)

  • users
    • ユーザのテーブル。後述する groups テーブルの id を外部キーとして持つ。
  • groups
    • 権限グループのマスタテーブル。id と「閲覧のみ」「管理者」といった表示用の名前 name のみを格納。

プルリクの中身

いろいろ端折っており、あくまでイメージ。

実際にはもう少し細かい条件分岐などがある。

論点は「Model 内に Controller のアクションのリストが存在する」という点である。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Group extends Model
{
    protected $fillable = [
        'id',
        'name',
    ];

    /**
     * 権限リストを取得
     *
     * @return array
     */
    public function getPermissions()
    {
        //----------
        // 閲覧のみ
        //----------
        if ($this->id === 1) {
            $permissions = [
                'UserController@index'   => true,
                'UserController@update' => false,
                'UserController@delete'   => false,
            ];
        }

        // 以下略

        return $permissions;
    }

}

この Model を介して、各 Controller のアクションが実行できるのかを判定する。

私の意見

MVC 各層を次のような階層関係として捉える。

View
| ↓
|  Controller
↓ ↓
Model

※矢印は「呼び出し元→呼び出し先」という関係

このとき前述の「Model 内に Controller のアクションのリストが存在する」という状態はすなわち「呼び出される側が呼び出し元について知っている」ことになり Group モデル が「知りすぎている」状態になっている。

実装例

ユーザを弾くかどうかの処理は View → Controller までの間(Controller を含む)の責務であり、Laravel においては Middleware がそれにあたる。

権限グループごとにアクセスを弾く Middleware を作成し、routes にてアクションごとに必要な Middleware を設定してあげればよい。

<?php

namespace App\Http\Middleware;

class DenyReadonlyUser
{
    /**
     * 「閲覧のみ」ユーザがアクセスしたとき 403 エラーを返す
     */
    public function handle($request, \Closure $next)
    {
        if (\Auth::user()->group_id === 1) {
            abort(403);
        }

        return $next($request);
    }
}

(Http\Kernel.php の設定は省略)

routes/web.php

Route::get('/users', 'UserController@index');
Route::post('/users/{id}', 'UserController@update')->middleware('deny.readonly');
Route::delete('/users/{id}', 'UserController@delete')->middleware('deny.readonly');

※ただし、この方法ではブラックリスト方式になってしまうので安全ではないという懸念がある。

考察……はまた今度

本当はそれぞれのメリット・デメリットを比較しようと思ったのだが、このあと私用で出かけてしまいアドベントカレンダーの期限に間に合いそうになくなるので、いったんここまで。

残りの考察についてはまた別途書きたいと思う。