こんにちは、 新しいポケモンを発売当初に買わず、完全に乗り遅れてしまった、 広告技術部のサンドバーグです。
この記事は Gunosy Advent Calendar 2022の13日目の記事です。昨日の記事は hamashita さんの『突撃隣の作業環境2022』でした。去年も同じ企画での記事がありましたが、毎年みんなの環境がレベルアップしている感じがあり、見応えがあります!
本記事は弊社でもよく作ることのあるRails管理画面のAuthentication/Authorizationのパターン紹介になります。結構シンプルな内容ではあるので、ご了承ください!
前提
- 今回主な実装はDeviseでされている想定で書いています
- そもそもユーザーロール管理が必要のないシステムであれば、この話はないです!
まず初めに
今回紹介するパターンは一部広告技術部で取り入れてはいますが、完全一致で採用しているわけではないので、個人的な見解なども含みます。それも踏まえて、弊社ではいくつか管理画面を作る上で、度々課題として上がってくるのがユーザの機能制限とコードの共通化の範囲です。
Authentication自体は基本DeviseやDoorkeeperといったユーザセッション管理、ユーザモデル生成のgemを使う限り、失敗することはないと思います。
Authorizationも複雑な処理は特に必要なく、gemを使う場合はpundit、使わない場合ではControllerなどでのbefore_actionでのロールチェックするのが普通かと。
ただ、管理画面においてAuthorizationを曖昧に設計してしまうと発生しがちなのが、ControllerとViewの複雑化です。
具体的な課題ケース
管理画面において、複数のロールが共通して使いたくなる機能が存在します。 今回はロールが三種類(admin/client/medium)あるとして、仮に「スレッド」を立てられると想定してControllerを書くと、以下のようになるかと思います。
class ThreadsController < ApplicationController before_action :deny_medium_user def create @thread = Thread.new(create_params) if @thread.save redirect_to threads_path else flash.now[:error] = @thread.errors.full_messages render :new, status: :unprocessable_entity end end private def create_params strong_params = params.require(:thread).permit(:title, :content, :index) strong_params.except!(:index) if current_user.client? end end
admin/clientはスレッドを立てられますが、mediumは使えないため、before_actionで操作をdenyします。 その上、スレッド作成時に管理者にしか設定ができないパラメータがある場合、Strong Parameterからは条件付きではじく必要があります。
上記の問題としては、それぞれのロールにおいてThreadが誰が作れる物であって、どこまでの設定で作れるのかが明確になっていないという物です。パラメータ・ロールの追加されるたび、コードがより複雑になるだけではなく、ちょっとした修正ミスにより、公開されるべきでない機能・画面が容易に出てしまう可能性があります。
言わずともViewにおいても、パラメータの違いを考慮してcurrent_userから表示・非表示の制御を追加する必要が出てくるため、html上での分岐が増えてしまいます。
解決に向けて
まず、弊社で行っているのがControllerとViewのロールごとでの完全分離です。 同じような画面・機能であっても、Routingをロールのnamespaceで切り分けていて、Controller/Viewもそれに合わせてディレクトリ/ファイルを完全に分けています。
├── controllers │ ├── admin │ │ └── dashboards_controller.rb │ ├── application_controller.rb │ ├── client │ │ └── dashboards_controller.rb │ ├── concerns │ └── medium │ └── dashboards_controller.rb └── views ├── admin │ └── dashboards │ └── show.html.erb ├── client │ └── dashboards │ └── show.html.erb └── medium └── dashboards └── show.html.erb
ThreadsControllerの例で行くと、以下のような書き方に変わります。
module Admin class ThreadsController < ApplicationController before_action :only_admin def create @thread = Thread.new(create_params) if @thread.save redirect_to threads_path else flash.now[:error] = @thread.errors.full_messages render :new, status: :unprocessable_entity end end private def create_params strong_params = params.require(:thread).permit(:title, :content, :index) end end end
素朴ですが、パラメータ管理の際に分岐を増やす必要もなくなる上、before_actionの権限保持者も明確になります。バグが含まれてしまった場合でも、特定のロールしか影響を受けないので、比較的安全です。
用意するファイル数は増えてしまいますが、各ファイルで書くコード量と複雑さは減らせるので、デメリットに対してはメリットが多いかと思います。
さらに
Adminしかアクセスを期待しないため、都度before_actionを指定せずとも、Application Controllerから仮にAdminControllerを用意すれば、毎度before_actionを設定しなくとも、一つのControllerに権限管理を任せることができます。
ただ、権限管理をしている単位がそもそもnamespaceで分かれてくるため、自然と行きつく先がroutes.rb自体でのAuthorizationとAuthenticationをまとめた管理だと思います。
上記のレポジトリーに動くコードを用意しましたが、肝になるところは以下のroutes.rbのコードになります。
Rails.application.routes.draw do devise_for :users authenticated :user, ->(u) { u.admin? } do root 'admin/dashboards#show', as: :admin_root namespace :admin do resource :dashboard, only: :show end end authenticated :user, ->(u) { u.client? } do root 'client/dashboards#show', as: :client_root namespace :client do resource :dashboard, only: :show end end authenticated :user, ->(u) { u.medium? } do root 'medium/dashboards#show', as: :medium_root namespace :medium do resource :dashboard, only: :show end end devise_scope :user do root 'devise/sessions#new', as: :unauthenticated_root end end
Deviseの認証で使うauthenticatedメソッドは、認証用のmodelを渡すだけではなく、lambdaでそのモデルオブジェクトに対してもメソッド実行ができるので、そのまま対象のロールかどうかを判定できます。
devise/routes.rb at 6d32d2447cc0f3739d9732246b5a5bde98d9e032 · heartcombo/devise · GitHub
routes.rbで管理することで、Controller/Viewを用意した時点でnamespaceによりロールのアクセス権限も制限されるため、Controllerでの権限考慮は一切考える必要がなくなります。 moduleも、明確にロールが定義されているため、見えずらい・わかりづらいも特にないかとは思います。
最後に
Deviseの機能もそうですが、概念的にも普通に昔からある物だとは思うので、今更な話かもしれません。ただ、改めてシステムの初期設計の段階でここの整理をおろそかにすることで後々大変な目に合うこともあると思うので、参考になればと思います。
本記事は、Deviseを用いたRailsでのAuthenticationとAuthorizationの一パターンのまとめになります。Railsでの管理画面開発自体かなりニッチではあると思うので、どれだけの人がこれで悩んでいるかわからないですが、これを見てふと考えるきっかけになれば幸いです。
次回は johnmanjiro さんの 『Go でサクッと GitHub CLI の拡張機能を作る』です!