らくだ🐫にもできるRailsチュートリアル|10.2
10.2 認可
ウェブアプリケーションの文脈では、認証 (authentication) はサイトのユーザーを識別することであり、認可 (authorization) はそのユーザーが実行可能な操作を管理することです。
現状ではログインしていないユーザーでもどのユーザー情報でも編集できてしまうため
ユーザーにログインを要求し、自分以外のユーザー情報を変更できないように制御する
→セキュリティ上の制御機構をセキュリティモデルと呼ぶ
やっていくこと
ログインしていないユーザーが保護されたページにアクセスしようとした場合ログインページに転送してメッセージを表示させる
さらに、許可されていないページにアクセス済のユーザーがいたらルートURLにリダイレクトさせる
10.2.1 ユーザーにログインを要求する
Usersコントローラの中でbeforeフィルターを使ってユーザーにログインを要求する
beforeフィルターとはなんぞ
before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みです
class UsersController < ApplicationController # 直前にlogged_in_userメソッドを実行 edit,updateアクションにのみ適用 before_action :logged_in_user, only: [:edit, :update] ・ ・ ・ # 外部に公開されないメソッド private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user # logged_in?メソッドがfalseの場合 unless logged_in? # flashsでエラーメッセージを表示 flash[:danger] = "Please log in." # login_urlにリダイレクト redirect_to login_url end end end
unless文
if文が条件式の評価がtrueの場合に処理を実行するのに対して
unless文は条件式の評価がfalseの場合に処理を実行する
上記の場合
ヘルパーに定義したlogged_in?メソッドがfalse(current_userがnil)の場合に処理(エラーメッセージの表示→login_urlにリダイレクト)が行われる
↓SessionsHelperに定義されているlogged_in?メソッドとlogged_in?メソッドで呼んでるcurrent_userメソッドはこちら
module SessionsHelper ・ ・ ・ # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user && user.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end ・ ・ ・
現状のテストはRED
原因は、editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗するようになったためです。
→editアクションやupdateアクションをテストする前にログインしておく必要がある!
→log_in_asヘルパー (test_helper.rbに定義) を使う
・ ・ ・ test "unsuccessful edit" do # test userとしてログイン log_in_as(@user) # edit_user_path(@user)にgetのリクエスト get edit_user_path(@user) ・ ・ ・ test "successful edit" do # test userとしてログイン log_in_as(@user) # edit_user_path(@user)にgetのリクエスト ・ ・ ・
(リスト 10.17のsetupメソッド内でログイン処理をまとめてしまうことも可能です。しかし、10.2.3で片方のテストをログインする前に編集ページにアクセスするように変更したいので、ここでまとめてしまっても結局は元に戻すことになってしまいます。)
セキュリティモデルに関するテスト
現状ではセキュリティモデルに関する実装を取り外しても(beforeフィルターをコメントアウト)テストが greenになってしまう!
これでは危険が危ないので検知できるようにテストを修正してく
→beforeフィルターは基本的にアクションごとに適用していく。よってUsersコントローラのテストもアクションごとに書いていく。
- 正しい種類のHTTPリクエストでeditアクション(getのリクエスト)とupdateアクション(patchのリクエスト)をそれぞれ実行
- flashにメッセージが代入されたか
- ログイン画面にリダイレクトされたか
require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest ・ ・ ・ test "should get new" do get signup_path assert_response :success end test "should redirect edit when not logged in" do # edit_user_path(@user)にgetのリクエスト get edit_user_path(@user) # flashがemptyではない(flashが表示されている→エラー表示) assert_not flash.empty? # login_urlにリダイレクトされる assert_redirected_to login_url end test "should redirect update when not logged in" do # paramsハッシュに以下のデータを持たせてuser_path(@user)にpatchのリクエスト patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } # flashがemptyではない(flashが表示されている→エラー表示) assert_not flash.empty? # login_urlにリダイレクトされる assert_redirected_to login_url end end
テストが意図の通りに作動するか確認
class UsersController < ApplicationController #before_action~をコメントアウトするとテストはREDに #コメントアウトを外すとテストはGREENになる before_action :logged_in_user, only: [:edit, :update] . . . end
演習
デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです (結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか (テストが失敗するかどうか) 確かめてみましょう。
FAIL["test_invalid_signup_information", UsersSignupTest, 0.8014602010002818] test_invalid_signup_information#UsersSignupTest (0.80s) expecting <"users/new"> but rendering with <[]> test/integration/users_signup_test.rb:13:in `block in <class:UsersSignupTest>' FAIL["test_valid_signup_information", UsersSignupTest, 0.8080838440000662] test_valid_signup_information#UsersSignupTest (0.81s) "User.count" didn't change by 1. Expected: 2 Actual: 1 test/integration/users_signup_test.rb:21:in `block in <class:UsersSignupTest>' FAIL["test_should_get_new", UsersControllerTest, 0.9231980270001259] test_should_get_new#UsersControllerTest (0.92s) Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/login> Response body: <html><body>You are being <a href="http://www.example.com/login">redirected</a>.</body></html> test/controllers/users_controller_test.rb:12:in `block in <class:UsersControllerTest>'
これちょっと悩んで、
本文にはログインページやユーザー登録ページにも制限の範囲が及んでしまうはずってあるんだけど
ユーザー登録ページ関連にはエラーも出てるし実際動作もおかしくなる(ログインページに飛ばされるループになる)けど
ログインページではエラーも異常も出ないんだよね。
本文通りならログインページ関連にもエラーが出ないとおかしいのでは?どこか間違ってるのでは?ってなったんだけど
ログインページ関連はSessionsControllerなのでUsersControllerのbeforeフィルターの制限が及ぶわけないのよね。
と、納得した🐫
10.2.2 正しいユーザーを要求する
ログインを要求するだけではなく、ログインしたユーザーが自分だけの情報を編集できるようにする。
→現状ではログインさえしていれば他ユーザーの情報も更新できてしまう
ログインしているからと言って、他のユーザーの情報まで編集できてしまうのは🙅♀️
これをテスト駆動開発でやっていく!
他ユーザーの情報が編集できないかを確認するテスト
テスト用のデータに2人目のユーザーを作る
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>
log_in_asメソッドを使って、editアクションとupdateアクションをテストする
このとき、既にログイン済みのユーザーを対象としているため、ログインページではなくルートURLにリダイレクトしている点に注意してください。
require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest # テストユーザーを設定 def setup @user = users(:michael) # 追加したユーザーを@other_userに代入 @other_user = users(:archer) end ・ ・ ・ test "should redirect edit when logged in as wrong user" do # @other_userとしてログインする log_in_as(@other_user) # @userのユーザー情報編集ページにgetのリクエスト get edit_user_path(@user) # フラッシュがemptyである assert flash.empty? # root_urlにリダイレクトされる assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do # @other_userとしてログインする log_in_as(@other_user) # @userのupdateアクションにparamsハッシュのデータを持たせてPATCHのリクエスト patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } # フラッシュがemptyである assert flash.empty? # root_urlにリダイレクトされる assert_redirected_to root_url end end
別のユーザーのプロフィールを編集しようとしたらリダイレクトさせる
beforeフィルターで管理
→correct_userというメソッドを作成し、ログインユーザーが別のユーザーのプロフィールを編集しようとしたらリダイレクトさせる
beforeフィルターのcorrect_userで@user変数を定義しているため、リスト 10.25ではeditとupdateの各アクションから、@userへの代入文を削除している点にも注意してください。
class UsersController < ApplicationController # 直前にlogged_in_userメソッドを実行 edit,updateアクションにのみ適用 before_action :logged_in_user, only: [:edit, :update] # 直前にcorrect_userメソッドを実行 edit,updateアクションにのみ適用 before_action :correct_user, only: [:edit, :update] ・ ・ ・ #ユーザーのeditアクション def edit # 直前に実行されるcorrect_userメソッドで定義されているため下記代入文は削除 # @user = User.find(params[:id]) end def update # 直前に実行されるcorrect_userメソッドで定義されているため下記代入文は削除 # @user = User.find(params[:id]) #指定された属性の検証がすべて成功した場合@userの更新と保存を続けて同時に行う if @user.update_attributes(user_params) # 更新成功のフラッシュメッセージ flash[:success] = t('.profile_updated') # @user(プロフィールページ)へリダイレクト redirect_to @user else render 'edit' end end # 外部に公開されないメソッド private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user # logged_in?がfalseの場合 unless logged_in? # flashsでエラーメッセージを表示 flash[:danger] = t('users.please_log_in') # login_urlにリダイレクト redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) # root_urlにリダイレクト 以下の式がfalseの場合 @userとcurrent_userが等しい redirect_to(root_url) unless @user == current_user end end
リファクタリング
一般的な慣習に倣ってcurrent_user?という論理値を返すメソッドを実装します
unless @user == current_user #↑これ が ↓こうなって コードが少しわかりやすくなるとの事 unless current_user?(@user)
correct_userの中で使えるようにする為にSessionsヘルパーの中に追加する
module SessionsHelper # 渡されたユーザーでcookieを使ってログインする def log_in(user) session[:user_id] = user.id end # ユーザーのセッションを永続的にする def remember(user) user.remember cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token end # 渡されたユーザーがログイン済みユーザーであればtrueを返す def current_user?(user) user == current_user end ・ ・ ・
Usersコントローラーに反映させる
# beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user # logged_in?がfalseの場合 unless logged_in? # flashsでエラーメッセージを表示 flash[:danger] = t('users.please_log_in') # login_urlにリダイレクト redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) # root_urlにリダイレクト current_user?メソッドがfalseの場合 redirect_to(root_url) unless current_user?(@user) end end
演習
- 何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
- 上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?
- 他のユーザーのユーザー情報編集ページを表示させない為にeditアクション、データを更新させないためにupdateアクションをそれぞれ保護する必要がある
- viewが定義されているeditアクション
10.2.3 フレンドリーフォワーディング
各種リダイレクト先はユーザーが開こうとしていたページに設定するのが親切というもの!
ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがログインした後にはその編集ページにリダイレクトされるようにするのが望ましい動作です。
フレンドリーフォワーディングのテスト
ログインのテスト(ログインした後に編集ページへアクセスするという順序)を逆にする
- 編集ページにアクセス
- ログイン
- プロフィールページではなく編集ページにリダイレクト
・ ・ ・ # successful editのテストを編集していく test "successful edit with friendly forwarding" do # edit_user_path(@user)にgetのリクエスト get edit_user_path(@user) # @userとしてログイン log_in_as(@user) # edit用のテンプレートはリダイレクトで描画されるので下記一文は削除 # assert_template 'users/edit' # @userのユーザー編集ページにリダイレクトされる assert_redirected_to edit_user_url(@user) # nameに"Foo Bar"を代入 name = "Foo Bar" # emailに"foo@bar.com"を代入 email = "foo@bar.com" # 有効なparams:を持ったuser_path(@user)でpatch(更新)のリクエスト patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } # falseである→ flashが空っぽであるか assert_not flash.empty? # リダイレクトされている →@user(プロフィールページ) assert_redirected_to @user # @user(プロフィールページ)を再読み込み @user.reload # name(入力値)と@user.name(DBの値)が等しい assert_equal name, @user.name # email(入力値)と@user.email(DBの値)が等しい assert_equal email, @user.email end end
ん?
successful editのテストを編集するのででいいんだよね?(?
フレンドリーフォワーディングの実装
ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要があります。
Sessionヘルパーにstore_location(リクエスト時点のページを保存)とredirect_back_or(ユーザーを希望のページに転送)の二つのメソッドを定義する
2つのヘルパーメソッドの実装
store_locationとredirect_back_orの2つのメソッドを定義していく
・ ・ ・ # 記憶したURL (もしくはデフォルト値) にリダイレクト def redirect_back_or(default) # リダイレクト(session[:forwarding_url]の値、値がnilデフォルト値へ) redirect_to(session[:forwarding_url] || default) # session変数の:forwarding_urlキーの値をdelete session.delete(:forwarding_url) end # アクセスしようとしたURLを覚えておく def store_location # session変数の:forwarding_urlキーに格納 request.original_urlで取得したリクエスト先urlにGETリクエストが送られたときのみ session[:forwarding_url] = request.original_url if request.get? end end
requestオブジェクト
ブラウザから返されるリクエストに関する情報が多数含まれている
request.original_url → 現在のリクエストURLをStringとして返す
request.get? → HTTPメソッドがGETの時trueを返す
beforeフィルターを修正
store_locationメソッドを使って、早速beforeフィルターのlogged_in_userメソッドを修正
・ ・ ・ # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user # logged_in?がfalseの場合 unless logged_in? # SessionsHelperのstore_locationメソッドを呼び出す store_location # flashsでエラーメッセージを表示 flash[:danger] = t('users.please_log_in') # login_urlにリダイレクト redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) # root_urlにリダイレクト current_user?メソッドがfalseの場合 redirect_to(root_url) unless current_user?(@user) end end
createアクションを修正
createアクションにredirect_back_orメソッドを組み込んでリダイレクト先を呼び出す
・ ・ ・ def create @user = User.find_by(email: params[:session][:email].downcase) if @user && @user.authenticate(params[:session][:password]) #session[:user_id] = @user と言う事 log_in @user #params[:session][:remember_me]が1の時@userを記憶 そうでなければuserを忘れる params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) #SessionsHelperで定義したredirect_back_orメソッドを呼び出してリダイレクト先を定義 # 本文ではデフォルト値が「user」になってるけど「@user」が正解 redirect_back_or @user else flash.now[:danger] = t('.login_error') render 'new' end end ・ ・ ・
演習
- フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト (プロフィール画面) に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。
- 7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください (デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう (デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって (コラム 1.1)、落ち着いて対処してみましょう)。
1.
・ ・ ・ test "successful edit with friendly forwarding" do # edit_user_path(@user)にgetのリクエスト get edit_user_path(@user) # session[:forwarding_url]とedit_user_url(@user)が等しい時にtrue assert_equal session[:forwarding_url], edit_user_url(@user) # @userとしてログイン log_in_as(@user) # edit用のテンプレートはリダイレクトで描画されるので下記一文は削除 # assert_template 'users/edit' # session[:forwarding_url]がnilの時true assert_nil session[:forwarding_url] # @userのユーザー編集ページにリダイレクトされる assert_redirected_to edit_user_url(@user) # nameに"Foo Bar"を代入 name = "Foo Bar" # emailに"foo@bar.com"を代入 email = "foo@bar.com" # 有効なparams:を持ったuser_path(@user)でpatch(更新)のリクエスト patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } ・ ・ ・
2.
session[:forwarding_url]の値 → ~/users/1/editとなるはず
(byebug) session[:forwarding_url] "https:/*******amazonaws.com/users/1/edit"
request.get?の値 → trueとなるはず
(byebug) request.get? true
まとめとか感想
本文にちょこちょこ引っかかりまくって思いのほか時間がかかったりだけど、その分しっかり調べたから!と、思う🐫
フレンドリーフォワーディングたいせつ
らくだ🐫にもできるRailsチュートリアルとは
「ド」が付く素人のらくだ🐫が勉強するRailsチュートリアルの学習記録です。
自分用に記録していますが、お役に立つことがあれば幸いです。
調べたとはいえらくだ🐫なりの解釈や説明が含まれます。間違っている部分もあるかと思います。そんな所は教えて頂けますと幸いなのですが、このブログにはコメント機能がありません💧お手数おかけしますがTwitterなどでご連絡いただければ幸いです