laravel
LaravelDay 20

laravel-permissionの使い方

Qiitaで書くのは初めてなので不届きなどありましたらお許し下さい。

目的

ウェブアプリケーションを作っていると色々な制限を実装しなければならないことが多いと思います。
その中でログインしているユーザーができることを許可/拒否する権限を実装することがあると思います。
例えば特定のユーザーはこの機能を使うことができるけど、それ以外のユーザーは使えないようにするとか、ユーザーごとに設定するのは管理が大変だからロールに設定したいとかです。

自分は10年以上Javaでウェブアプリケーションを作っておりSpringではSpringSecurityを使って同様な機能の実装ができます。

Laravelではlaravel-permissionというパッケージを使うことで比較的簡単に実装することができます。
実際に簡単なアプリケーションを作って見たいと思います。

動作(開発)環境

  • PHP 7.1
  • Laravel 5.5
  • PostgreSQL 10.0

サンプルアプリケーションの概要

サンプルアプリケーションでは、ユーザーはロールに属し、ロール毎にパーミッションを設定し利用できるコンテンツを制限してみます。

ロールは以下の通りです。

  • 社長(owner)
  • 店長(manager)
  • 店員(staff)

コンテンツは以下の通りです。

  • 社員の情報(users)
  • 社員の評価(scores)

権限マトリックスは以下の通りです。

コンテンツ 店員 店長 社長
社員情報 R R/W R/W
社員の評価 R R/W

R = 読み取り、W = 書き込み

ユーザー一覧は以下の通りです。
構成をシンプルにするためユーザー名 = ロール名にしました。

ユーザー ログインID パスワード
社長 owner@test owner
店長 manager@test manager
店員 staff@test staff

アプリケーションを実行する準備

一から作るのは時間がかかるのでサンプルアプリケーションが動作するDocker環境をDocker-Composeで準備しました。

Docker-Composeが使用できる方

レポジトリをクローンしてもらうだけで大丈夫です。

# サンプルアプリケーションをクローンする
git clone https://github.com/beeete2/qiita-laravel-permission.git

# イメージをpullする
cd qiita-laravel-permission
docker-compose pull

Vagrantを使用できる方

ゲストOSのIPは192.168.33.51になりますのでIPが衝突する場合、その他カスタマイズが必要な場合はVagrantfileファイルを修正してください。
AnsibleでDockerとDcoker-Compose等をインストールします。

# クローンしてインスタンスを起動する
git clone https://github.com/beeete2/qiita-laravel-permission.git
cd qiita-laravel-permission
vagrant up

# sshでゲストOSにログインする
ssh 192.168.33.51
cd /vagrant

# イメージをpullする
docker-compose pull

CentOS7を使用できる方

Ansibleなどをインストールしますので、使い捨ての環境を想定しています。
またすべてrootユーザーで作業してます。

# 必要なパッケージをインストールする
yum install epel-release
yum install git ansible

# サンプルアプリケーションをクローンする
git clone https://github.com/beeete2/qiita-laravel-permission.git
cd qiita-laravel-permission

# AnsibleでDcoker、Docker-Composeをインストールする
ansible-playbook -i localhost, -c local playbook.yml --extra-vars "skip_user_in_group=Yes"

# コンテナをpullする
docker-compose pull

アプリケーションを見てみる

laravel-permissionの説明をする前に、アプリケーションを見てみたいと思います。
各イメージをビルドしてレポジトリディレクトリにいることを想定しています。

アプリケーションを起動する

docker-compose run web composer update
cat src/.env.example > src/.env
docker-compose run web php artisan key:generate

# docker-composeでdbコンテナとwebコンテナを起動する
docker-compose up -d

# マイグレーションする
docker-compose exec web php artisan migrate --seed

# オプション(実行する必要はない)
# scmemaspyでデータベースのスキーマー情報を出力する(build/schemaspy以下に出力される)
chmod 777 build/schemaspy # ザルですがお許しください。
docker-compose run scmemaspy

ブラウザで見てみる。

ブラウザで見てみます。
http://Docker-ComposeホストのIP/

Laravelのホーム画面が表示されると思いますので社長ロールでログインするため右上のLoginをクリックします。
するとログイン画面が表示されると思いますのでE-Mail Addressにowner@test、Passwordにownerと入力しログインします。
するとメニューが表示されます。
2017-12-16_16h25_33.png

次に社員情報をクリックします。
すると社員情報の一覧が表示されます。
2017-12-16_16h26_53.png
社員情報の一覧の変更ボタンですがユーザーにwrite usersパーミッションがある場合表示しておりlaravel-permissionの機能を使って制御しています。

一旦ログアウトして社員ロールでログインします。
次はE-Mail Addressにstaff@test、Passwordにstaffと入力しログインします。
同じくメニューから社員情報をクリックすると社員情報の一覧が表示されますがAction欄に変更ボタンが表示されていないと思います。
2017-12-16_16h30_24.png

laravel-permissionではViewだけではなくルートでも制限を行えます。
社員ロールでログインしたまま
http:/Docker-ComposeホストのIP/users/3
にアクセスします。するとエラーページ(ステータスコード403)が表示されたと思います。
また社員ロールには社員の評価の権限がないのでメニューの社員の評価をクリックしてもエラーページが表示されます。

サンプルアプリケーションの説明

サンプルアプリケーションを見ながらlaravel-permissionを簡単に説明していきたいと思います。

使用するテーブルの概要

laravel-permissionは主に以下のテーブルを使います。

テーブル名 説明
users ユーザーのアカウント
roles ロールを管理するテーブル。今回の例だと社長など。
permissions パーミッションを管理するテーブル。今回の例だと社員の情報を読めるなど。
role_has_permissions ロールに設定されたパーミッション
model_has_permissions ユーザーに設定されたパーミッション
model_has_roles ユーザーに設定されたロール

リレーションは以下のようになります。
2017-12-16_23h47_59.png

ちなみにテーブル名はconfig/permission.phpに記載されています。
プロジェクトにテーブル名の命名規則等ありあわせる必要が有る場合は、このファイルを修正することで変更可能です。

ロールとパーミッションの設定

ロールとパーミッションの設定を行います。
サンプルアプリケーションではSeederを使って作成しています。
以下は抜粋です。

パーミッションの設定
パーミッションは文字列になります。
サンプルではread usersのようにアクション + スペース + コンテンツというルールでパーミッションを設定しました。

PermissionTableSeeder.php
    public function run()
    {
        $permissions = [
            'read users',  // 社員情報を読める
            'write users', // 社員情報を変更できる
            'read scores',  // 社員の評価を読める
            'write scores', // 社員の評価を変更できる
        ];
        foreach ($permissions as $permission) {
            Permission::create(['name' => $permission]);
        }
    }

ロールの設定

RoleTableSeeder.php
    public function run()
    {
        $roles = [
            'owner',   // 社長
            'manager', // 店長
            'staff',   // 店員
        ];
        foreach ($roles as $role) {
            Role::create(['name' => $role]);
        }
    }

ロールにパーミッションを割り当てる

次にロールにパーミッションを割り当ててみます。
これもSeederを使って作成しています。

RoleHasPermissionTableSeeder.php
    public function run()
    {
        // ownerロールのパーミッション設定
        $permissions = [
            'read users',  // 社員情報を読める
            'write users', // 社員情報を変更できる
            'read scores',  // 社員の評価を読める
            'write scores', // 社員の評価を変更できる
        ];
        $role = Role::findByName('owner');
        $role->givePermissionTo($permissions);

        // managerロールのパーミッション設定
        $permissions = [
            'read users',  // 社員情報を読める
            'write users', // 社員情報を変更できる
            'read scores',  // 社員の評価を読める
        ];
        $role = Role::findByName('manager');
        $role->givePermissionTo($permissions);

        // staffロールのパーミッション設定
        $permissions = [
            'read users',  // 社員情報を読める
        ];
        $role = Role::findByName('staff');
        $role->givePermissionTo($permissions);
    }

ユーザーを作成しロールを割り当てる

最後にユーザーを作成しロールを割り当てます。
これもSeederを使って作成しています。

UsersTableSeeder.php
    public function run()
    {
        // 社長(owner)ユーザーを作成する
        $user = User::create([
            'name' => '社長',
            'email' => 'owner@localhost',
            'password' => Hash::make('owner'),
        ]);
        $user->assignRole('owner');

        // 店長(manager)ユーザーを作成する
        $user = User::create([
            'name' => '店長',
            'email' => 'manager@localhost',
            'password' => Hash::make('manager'),
        ]);
        $user->assignRole('manager');

        // 店員(staff)ユーザーを作成する
        $user = User::create([
            'name' => '店員',
            'email' => 'staff@localhost',
            'password' => Hash::make('staff'),
        ]);
        $user->assignRole('staff');
    }

パーミッションに従ったコンテンツの制御

基本的にlaravel-permissionのREADMEに詳しく書いてありますが、ロールとパーミッションに従ったコンテンツの制御は以下のように行います。

Bladeの制御

Bladeでパーミッションがある場合は特定のメニューを表示したいということがあると思います。
その場合はblade.phpに以下のように書きます。

@can('read users')
  // read usersパーミッションが割り当てられる場合実行される
@endcan

ロールの場合は以下のように書きます。

@role('owner')
    // オーナーロールの場合実行される
@endrole

ミドルウェアの制御

この手のパーミッション制御を行うと、例えばあるパス(Route)には特定のパーミッションを持っているときにアクセスできるということをよく実装すると思います。これをミドルウェアを使って制御することができます。
最初にlaravel-permissionの制御ができるようにミドルウェアを追加します。

app\Http\Kernel.php
    protected $routeMiddleware = [
        // 省略
        'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
        'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
    ];

これでRouteでパーミッションとロールの制御が行えるようになりました。
例えば、特定のパスに対してread usersを持つユーザーだけアクセスできるようにしたいという場合は以下のように書きます。

Route::group(['middleware' => ['permission:read users']], function () {
    // 処理を記載する
});

ロールで制御することも可能です。
以下のように書くとownerロールの場合だけアクセスすることができます。

Route::group(['middleware' => ['role:owner']], function () {
    // 処理を記載する
});

今回のサンプル・アプリケーションでは社員情報の場合usersにはread usersパーミッション、ユーザーを変更するためのusers/1にはwrite usersが必要になります。
サンプル・アプリケーションでは以下のように実装しました。

web.php
Route::prefix('/users')->middleware(['permission:read users'])->group(function () {
    Route::get('index', 'UsersController@index')->name('users.index');
    Route::middleware(['permission:write users'])->group(function () {
        Route::get('{user}', 'UsersController@update')->name('users.update');
    });
});

結びに

こんな感じで実装できます。もちろん自分で実装することもできると思いますがシンプルな権限制御だったらlaravel-permissionである程度実装できるのではないかと思います。

その他

Userモデルでlaravel-permissionの機能を使うためにHasRolesトレイトを使用しています。
大体の処理はHasRolesトレイトに書いてあります。

Models\User
class User extends Authenticatable
{
    use Notifiable;
    use HasRoles; // laravel-permission
}

Role名を取得したい

ユーザーのRole名は以下のように取得することができます。

$user->getRoleNames();

ロールを再設定したい

現在設定されているロールを削除して、設定し直したい場合は以下のようにします。

$user->syncRoles('owner');

ユーザーに設定されているPermissionを取得したい

// ユーザーに設定されているPermission(Direct Permission)
$user->getDirectPermissions();

// ユーザーが所属するロールに設定されているPermission
$user->getPermissionsViaRoles();

// ユーザーに与えられているパーミッション(Direct PermissionとRole Permissionをマージ)
$user->getAllPermissions();

Guardごとにロールとパーミッションを分けたい

管理画面を作るときに例えば管理者とユーザーでログインするページが別々になっており、それぞれにロールとパーミッションの制御が必要になるということがよくあると思います。
laravel-permissionはデフォルトでconfig('auth.defaults.guard')に指定されているGuardを参照しますがマルチガードに対応しております。
rolesテーブルもpermissionsテーブルもguardカラムを持っているので、それぞれのロール、パーミッションを作成するときにどのガードか指定すればよいです。

以下はlaravel-permissionのREADMEに書いてあるマルチガードの対応例です。

// Create a superadmin role for the admin users
$role = Role::create(['guard_name' => 'admin', 'name' => 'superadmin']);

// Define a `publish articles` permission for the admin users belonging to the admin guard
$permission = Permission::create(['guard_name' => 'admin', 'name' => 'publish articles']);

// Define a *different* `publish articles` permission for the regular users belonging to the web guard
$permission = Permission::create(['guard_name' => 'web', 'name' => 'publish articles']);