laravel-permissionの使い方

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

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

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

Laravelでは[laravel-permission](https://github.com/spatie/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が使用できる方
レポジトリをクローンしてもらうだけで大丈夫です。

```bash
# サンプルアプリケーションをクローンする
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とDocker-Compose等をインストールします。

```bash
# クローンしてインスタンスを起動する
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ユーザーで作業してます。

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

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

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

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


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

## アプリケーションを起動する
```bash
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](https://qiita-image-store.s3.amazonaws.com/0/115726/dd69b810-e152-0ea0-9604-fb5e27bdd043.png)

次に社員情報をクリックします。
すると社員情報の一覧が表示されます。
![2017-12-16_16h26_53.png](https://qiita-image-store.s3.amazonaws.com/0/115726/41085d72-0a0e-98b6-8876-d383d606c539.png)
社員情報の一覧の変更ボタンですがユーザーに`write users`パーミッションがある場合表示しており`laravel-permission`の機能を使って制御しています。

一旦ログアウトして社員ロールでログインします。
次はE-Mail Addressに`staff@test`、Passwordに`staff`と入力しログインします。
同じくメニューから社員情報をクリックすると社員情報の一覧が表示されますがAction欄に変更ボタンが表示されていないと思います。
![2017-12-16_16h30_24.png](https://qiita-image-store.s3.amazonaws.com/0/115726/8ba1e6ba-0856-65ba-9ecc-1c365bdef057.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](https://qiita-image-store.s3.amazonaws.com/0/115726/5cb85762-8530-af50-4fb4-3fa76f08663e.png)


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

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

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

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

**ロールの設定**

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

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

```php: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を使って作成しています。

```php: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に以下のように書きます。

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

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

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

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

```php:app\Http\Kernel.php
    protected $routeMiddleware = [
        // 省略
        'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
        'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
    ];
```
これでRouteでパーミッションとロールの制御が行えるようになりました。
例えば、特定のパスに対して`read users`を持つユーザーだけアクセスできるようにしたいという場合は以下のように書きます。

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

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

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

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

```php: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トレイトに書いてあります。

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

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

```php
$user->getRoleNames();
```

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

```php
$user->syncRoles('owner');
```

## ユーザーに設定されているPermissionを取得したい
```php
// ユーザーに設定されている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に書いてあるマルチガードの対応例です。

```php
// 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']);
```