load_and_authorize_resourceで、コントローラー名と異なるリソースを扱う際にうまく権限判定されない件について
要約
-
load_and_authorize_resourceをコントローラー名とは異なるリソースに適用する際に、権限判定をしようとしているリソースが、現在のコントローラーで扱っている「メインのリソース」ではなく「親のリソース」と判断されてしまい、望んだアクションではなく、showに対する権限の判定しか行ってくれない - よって、readの権限さえ付与されていればeditの権限を与えてないにも関わらずeditやupdateを実行することができてしまう
- オプションで
parent:falseを指定することで意図したactionに対して権限判定を行ってくれる
なぜ親リソースか判断しアクションをshowに書き換えるのか
resources :projects do
resources :tasks
end
このように親子関係のあるリソースにおいて、子供側のコントローラーからproject(親リソース)を経由してtaskを取得/権限判定したいという処理について考える。TasksControllerでは以下のようにしてリソースを取得/権限判定する
class TasksController < ApplicationController
load_and_authorize_resource :project
load_and_authorize_resource :task, through: :project
end
プロジェクト1が持つタスク5について編集をする場合を考える
/projects/1/tasks/5というルーティングにアクセスがくる
最初のload_and_authorize_resource :projectによって、プロジェクト1が取得される
今回はタスク5について編集を行いたいので走っているアクションはeditである
ここで、親リソースに対するアクションの権限判定について何も考慮しないと、この親リソース(プロジェクト1)についてもeditの権限判定をしてしまう!!
あくまで今回権限判定をしたいのは、タスク5に対するeditである。
親のリソースが見れないのに、子リソースに編集の権限があることは考えにくいため
親リソースについては最低限の閲覧権限(show)さえあれば良いという設計思想のもと、親リソースを取得する際には、どんなアクションに対してもデフォルトではshowに対する権限判定に上書きされる
このような設計の思想にそぐわない構造でload_and_authorize_resourceを利用すると、デフォルトのままでは意図しない挙動が起きてしまう、それが今回私が詰まってしまった原因である。本記事の最後に具体例で示す
問題の部分のコードを確認してみる
CanCanCanのgithubを見てみる
ここからは、コントローラー名とは異なる名前のリソースを取得する際にどこで不具合が起こるかについて考えながら読み解いていく
load_and_authorize_resourceは以下のように二つのメソッドを呼んでいる
def load_and_authorize_resource
load_resource
authorize_resource
end
-
load_resourceでリソースを取得 -
authorize_resourceで権限の判定
問題なのは、後者のauthorize_resourceである
def authorize_resource
return if skip?(:authorize)
@controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
end
-
return if skip?(:authorize)については、exceptオプションによってアクションごとに権限判定をスキップするかどうかをチェックしているので今回は関係ない。 -
@controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)が実際に権限判定を実行している部分であり、以下のようになっている- 第一引数:今回問題である権限判定を行いたいactionの取得
- 第二引数:権限判定の対象の取得
authorization_actionについて
def authorization_action
parent? ? parent_authorization_action : @params[:action].to_sym
end
parent?によって権限判定の対象が何かの親リソースであるかチェックし、それに応じて権限を判定するactionの取得方法が変わる
- true:
parent_authorization_actionによってactionを取得(こっちに入ってしまう...) - false:@paramsから現在実行されているactionを取得(本来はこっちに入ってきて欲しい)
この部分で、後者に入れば希望通り現在走っているactionに対して権限を判定してくれる
しかし、コントローラー名と異なるリソースを取得しようとするとparent?がtrueとなってしまい親リソースに対する権限の判定処理にシフトしてしまう
parent?について
def parent?
@options.key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
end
以下の状況において、このメソッドはtrueを返す
- オプションで取得したいリソースが親であると指定されている場合(
parent:true) - コントローラー名と異なるリソースを取得する場合
- 第一引数で指定した、モデル名をシンボル化したもの(例
:project)がコントローラー名から推測されるモデル名をシンボル化した値(TasksControllerなら:task)と一致しない場合
- 第一引数で指定した、モデル名をシンボル化したもの(例
つまり、親リソースであると指定していなくてもコントローラー名が取得したいリソースと一致していない場合に後者の判断で親リソース扱いされてしまう
(後で紹介するようなコントローラーの使い方をする場合には、この処理が意図しない挙動を生む)
parent_authorization_actionについて
def parent_authorization_action
@options[:parent_action] || :show
end
- オプションで
:parent_actionを指定している場合はそのアクションを返す- 上の例で紹介したように子リソースの取得のために、親のリソースを取得する際に親に対して持っていてほしい権限をオプション(
:parent_action)で指定できる。 - つまり、Task(子リソース)の編集をしたい場合にはProject(親リソース)に対して
:manageの権限を持っていないと不可。というような設定が可能になる
- 上の例で紹介したように子リソースの取得のために、親のリソースを取得する際に親に対して持っていてほしい権限をオプション(
- そうでない場合は、強制的に
:showアクションを返す
以上の部分で今回の問題が起こる部分のコードを見つけることができた
対処法
load_and_authorize_resourceでは、親子関係がなくただ単にコントローラー名と異なるリソースの取得をしたい場合
parent:falseの記述をオプションで指定することで、親リソースと判定されてどんなアクションに対しても、代わりにshowに対する権限判定に上書きされてしまう現象を防ぐことができる
具体的に詰まった状況を再現してみた
ここからは、具体的な例を用いて今回自分が悩まされた意図しない挙動が起こりうる状況を見てみる
まず、商品一覧の表示と各商品の編集ができるページがあるアプリケーションについて考える
商品ごとに設定されている各項目を編集できる機能があり、加えて各商品ごとの項目の並び順を変更する機能も備わっている。ただし、この並び順の変更は、別の専用ページで一括で設定する必要があるとする(これを別のコントローラーに切り出したことによってややこしくなっていた)
この状況を踏まえ、並び替えページにおけるリソースの取得方法と権限判定について考える
ページ構成
今回の説明に使う3ページについてイメージを持つために、軽く説明する
一覧ページ(A)
登録されている商品名が表示されている
「詳細」ボタンを押すと詳細ページ(B)へ遷移する
| 商品名 | 操作 |
|---|---|
| スマートフォン | 詳細 編集 削除 |
| ノートパソコン | 詳細 編集 削除 |
| ワイヤレスイヤホン | 詳細 編集 削除 |
| スマートウォッチ | 詳細 編集 削除 |
商品の詳細ページ(B)
商品の詳細情報を見ることができる
さらに、「並び順の編集」ボタンを押すと各項目の並び順を更新できるページ(C)に遷移する
| 項目 | 内容 |
|---|---|
| 商品名 | スマートフォン X |
| 価格 | ¥79,800 |
| 在庫数 | 25 個 |
| カテゴリー | 家電 |
| 発売日 | 2024年5月15日 |
| 説明 | 高性能カメラ搭載の最新スマホ |
並び順の編集ページ(C)
商品詳細ページ(B)で表示される際の各項目の並び順を更新することができる
| 項目 | 並び順 |
|---|---|
| 商品名 | 1 |
| 価格 | 2 |
| 在庫数 | 3 |
| カテゴリー | 4 |
| 発売日 | 5 |
| 説明 | 6 |
実装内容について
以下のような2つのコントローラーを作成する
-
ProductsController:商品の一覧ページの表示や登録、編集などを行うコントローラー -
ProductShowOrdersController:各商品における価格/在庫数/カテゴリなどの表示順序の変更を行うコントローラー- edit:詳細ページ(B)から並び順の編集ページ(C)へ遷移する
- update:並び順を更新する
ProductShowOrdersControllerでは以下の記述で編集対象のリソースを@productに代入する
class ProductShowOrdersController < ApplicationController
load_and_authorize_resource :product, only: %i[edit update]
before_actionでeditやupdateの前にload_and_authorize_resourceが呼び出され、以下の流れでリソースの取得と権限判定を行う
並び順を編集したいリソース(product)の取得→そのリソースに対してedit/updateに対する権限の判定
edit/updateに対する権限がないユーザーはここで、authorize_resourceの判定によって弾かれて実行できないはずである。
しかし!この記述ではread権限さえあればedit/updateが実行できてしまう...
なぜedit/update権限がないのに通ってしまうのか
今回、ProductとProductShowOrderに親子関係は存在しない
責務明確化のためにProductの単純なCRUD操作ではない「並び順の編集」を別のコントローラーに切り出したけど、扱うリソースについては同じ。という場合についても、CanCanCanはProductはProductShowOrderの親リソースだと勘違いしてしまう
よって、ここまでで解説したようにProductShowOrdersController内でproductというコントローラー名と異なるリソースに対してload_and_authorize_resourceを適用すると、どんなアクションに対しても親リソースに対する操作だと勘違いされてしまい、showに対する権限判定のみ行われてしまっていた
これによって、readの権限さえあればeditの権限がないのにeditアクションを実行することができてしまうという状況は生まれた
まとめ
今回の問題の解決を通して、「OSSのコードをじっくり読んで処理を追ってみる」という初めての経験ができてとても力になったと感じた。また、記事を書く前に調べていた段階で十分理解できていたと思っていたが、実際に記事を書く中で曖昧な理解が多くあったことに気づきより深く知ることができた。
Railsはただでさえ裏でやってくれることが多いので、表面上の理解だけでなくて時間がある時には実際にどんな処理が走っているかを追ってみることで得られるものがいっぱいありそうだと感じた。
Discussion