Laravel-adminで「GETのみ許可したユーザなのに削除ができてしまう」ので削除ボタンの非表示化で対応した話
Laravel-adminというLaravelの管理画面ライブラリがあります。このLaravel Adminを使うと管理画面が簡単に作れるのですが、「特定のユーザには閲覧のみを許可して、編集や作成をさせない」ようにしたら削除はできしまうという問題が発生しました。タイトルのとおり、結論としては削除ボタンを表示しないようにすることで対応しました。経緯などを書いていたら長くなってしまいましたが、この記事はその記録です。
Laravel Adminについて
まず前提となるLaravel Adminについて紹介します。
Laravel Adminの導入は公式ドキュメントを参照してください。3つのコマンドを実行するだけでインストール可能です。余談ですが、公式ドキュメントのサイトは証明書の有効期限切れで警告が出ることがたまにあります。
Laravel Adminは導入するだけで、ログインを含めた管理画面機能が実装してくれます。初期状態では、ユーザ名「admin」パスワード「admin」でログインできます。
また、アプリケーションで扱うモデルの削除や編集する管理画面も簡単に実装できます。例として、Flightモデルを操作する管理画面を作ってみます。Flightは作成・更新・論理削除日時の他にはnameプロパティだけを持つ簡単なモデルです。Laravel Adminでこのモデルを操作するフォームを作るには次のコマンドを実行します。
$ php artisan admin:make FlightController --model=App\\Models\\Flight
こうするとapp/Admin/Controllers/FlightController.phpが作成されます。また、コマンド実行時に app/Admin/routes.php に下記コードを追記するようメッセージがあるので、それに従います。
$router->resource('flights', FlightController::class);
このコントローラの自動生成とroutes.phpへの追記だけで完成です。http://localhost/admin/flights にアクセスすると、このようにDBに保存されているデータを一覧できます。各データについてCRUD(新規作成・詳細表示・編集・削除)に対応する操作も可能です。
閲覧のみ可能な管理ユーザを作る
そろそろ本題です。もともとLaravel Adminにはユーザごとに権限を管理する機能があります。最初からログインできるadminユーザはAdministoraterロールが割り当てられていて全ての操作が可能なようですが、管理画面を見ることしかできないユーザを新しく作ります。
まず”read-only”というpermissionを作成します。このパーミッションは次のように、全てのパスに対してGETのみ可能なように指定します。
続いて、PermissionだけでなくRoleも作っておきます。同じread-onlyという名前のロールを作り、”read-only”パーミッションを割り当てます。
最後に、Usersで”test”というユーザを作り、できたてほやほやの“read-only”ロールを割りあてます。これでtestはGETしかできない、閲覧のみ可能なアカウントになったはずです。
ここまで作ったら、実際にFlightの一覧から「新規」ボタンで作成しようとすると、フォームの入力まではできますがエラーになります。
同じ編集画面にある削除ボタンを押してみても、ローディングダイアログがずっと残り続けて削除はできません。
ただしここである問題がありました。この一覧ページの「操作」で出るコンテキストメニューの「削除」ボタンを押すと、
削除できてしまいます。閲覧だけできるはずのアカウントで削除できてはまずいです。
そこで、適切な権限であれば削除するカスタムアクションを実装し、デフォルトの削除ボタンの代わりにそのボタンを表示するよう変更してみました。ここではコードを省略しますが、参考にしたのは検索で引っかかったQiitaの記事「【laravel-admin】deleteアクションをカスタマイズする」です。
実際にやってみると、カスタムアクションで実装した削除ボタンでは「adminは削除できるけど、testは権限がないため削除しようとするとエラー」になります。しかし、これでめでたし、ではありませんでした。
自分でPOSTすれば削除できる
もともとあったデフォルトの削除ボタンが何をしていたのか、Chromeで確認してみます。非表示にしたデフォルトの削除ボタンを元に戻し、実際に押してみたときに送信したリクエストが次の画像です。なにやら /admin/_handle_action_
にフォームデータをPOSTしています。新規作成のPOSTは弾かれるのですが、この_handle_action_へのPOSTは権限チェックをスルーする仕様のようです。
デフォルトの削除ボタンを非表示にしたとき、このPOSTを送信してみるとどうなるでしょう。testユーザでログインして、次のようなJavaScriptをChromeのコンソールで実行してみます。
const token = document.querySelector("meta[name='csrf-token']").content; const formData = new FormData(); Object.entries({ _key: "20", // 削除するモデルのID _model: "App_Models_Flight", _token: token, _action: "Encore_Admin_Grid_Actions_Delete", _input: "true", }).map(([k, v]) => formData.append(k, v)); await fetch("http://localhost/admin/_handle_action_", { body: formData, method: "POST", });
すると、HTTP200 OKが返ってきます。リロードしてみるとID: 20に該当するデータが消えていました。
つまり、デフォルトの削除ボタンを権限チェック付きのカスタムのものに置き換えても、元の削除ボタンのAPIは残っていて、それを使えば削除可能ということです。
これではわざわざカスタムアクションで削除を実装する意味がありません。もともとあったデフォルトの削除ボタンを非表示にするのと実質同じことをしただけで、それなら権限がないユーザには削除ボタンを表示しないようにする方が簡単だからです。
結局は削除ボタンの非表示化で対応
ということでタイトルの結論に至ります。”read-only”ロールに該当するユーザには削除ボタンを表示しないようにするという実装で対応することにしました。一応理由として、管理画面は不特定多数が使うわけではない点、他の強引な実装では影響範囲が広い危険性、不正なリクエストで削除してもログに残る点(実はLaravel AdminのOperation Logも同じような方法で削除できてしまったのですが、システムのログファイルには残るはずです)が理由です。
この実装はこのようになります。app/Admin/Controllers/FlightController.php の該当部分をこのようにします。
<?php namespace App\Admin\Controllers; use App\Models\Flight; use Encore\Admin\Controllers\AdminController; use Encore\Admin\Form; use Encore\Admin\Grid; use Encore\Admin\Show; class FlightController extends AdminController { /** * Title for current resource. * * @var string */ protected $title = 'Flight'; /** * Make a grid builder. * * @return Grid */ protected function grid() { $grid = new Grid(new Flight()); $grid->column('id', __('Id')); $grid->column('name', __('Name')); $grid->column('created_at', __('Created at')); $grid->column('updated_at', __('Updated at')); $grid->column('deleted_at', __('Deleted at')); $grid->actions(function ($actions) { // read-onlyロールのユーザには、削除ボタンを非表示 if (\Admin::user()->inRoles(['read-only'])) { $actions->disableDelete(); } }); $grid->batchActions(function ($actions) { if (\Admin::user()->inRoles(['read-only'])) { $actions->disableDelete(); } }); return $grid; } // ... (以下省略)
1つ目の $grid->action
は表中の各行について操作できるUIの設定です。削除ボタン以外にも編集ボタンや詳細ボタンも非表示化できます(参考:公式ドキュメント)。2つ目の$grid->batchActions
では、各行のチェックボックス押下時の一括削除ボタンを非表示化しています。
これは公式ドキュメントにもある平凡なコードですが、強いて言えば、複数回関数を呼び出すと後勝ちで設定されてしまう点に注意が必要です。Gridクラスではaction
とbatchActions
のどちらの関数もコールバック関数を単に保持するだけの実装のようです。
権限に応じて削除させないようにする方法としては例えば、/admin/_handle_action_
に対するリクエストをルーティングでブロックする方法や、モデル側で削除をブロックする方法もあるかもしれません。ただ、Laravel Adminの実装自体がこの操作に関してパーミッションを意識していないようなので、今回はそこまで深入りしませんでした。そもそも詳細なカスタマイズをしたいのであればLaravel Adminが適さないのかもしれません。
以上、いろいろ回り道しましたが、結局のところ「Laravel Adminで特定のユーザには削除ボタンを表示させない」実装をするだけの記事でした。