はじめに
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 10章 10.2 認可の演習まとめ&解答例です。
個人の解答例なので、誤りがあればご指摘ください。
動作環境
- cloud9
- ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
- Rails 5.0.0.1
10.2 認可
本章での学び
認証と認可
- 認証 (authentication) はサイトのユーザーを識別すること
- 認可 (authorization) はそのユーザーが実行可能な操作を管理すること
現在の実装の課題1
未ログイン状態であっても、ユーザ情報編集画面にアクセスすることができてしまう。
未ログイン状態でサイトを表示する。
URLに、https://xxxx/users/1/edit を打ち込んでアクセスすると、IDが1番のユーザ情報変更画面が表示されてしまう。
現在の実装の課題2
ログイン状態であれば、他のユーザーの情報が見えてしまう。 また、他のユーザーの情報を更新することも可能。
IDが1番のユーザでログインする。
URLのhttps://xxxx/users/1/edit を、https://xxxx/users/2/edit に 書き換えてアクセスすると、IDが2番のユーザ情報変更画面が表示されてしまう。
課題の原因
- 要ログイン画面に対して、ログインの制御をかけていないため
- 自分以外のユーザ情報に対して制限をかけていないため
原因への対策
- 未ログインのユーザが、要ログイン画面にアクセスした場合、ログイン画面に転送する
- ログイン済みのユーザが、許可されていないページにアクセスした場合、ルートURLにリダイレクトさせる
10.2.1 ユーザーにログインを要求する
本章での学び
【controller】beforeフィルター
before_action
メソッドを使って、Usersコントローラ内の処理が実行される前に、ログイン画面に転送する仕組みを実現する。
class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] ・・・省略・・・ # beforeアクション def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end
動作確認
未ログイン状態でサイトにアクセスする。
URLに、https://xxxx/users/1/edit を打ち込んでアクセスすると、IDが1番のユーザ情報変更画面ではなく、ログイン画面が表示されるようになる。
テストコードの修正
before_action
にて、editアクションや、updateアクション実行前にログインが必要になったため、テストコードを修正する。
log_in_as
ヘルパーを使用して、テストコード内でログイン状態にする。
test "unsuccessful edit" do log_in_as(@user) ・・・省略・・・ end test "successful edit" do log_in_as(@user) ・・・省略・・・ end
【controller】テストコードの追加修正
before_action :logged_in_user, only: [:edit, :update]
をコメントアウトしてもしなくても、
テストを実行するとgreenになってしまう。
class UsersController < ApplicationController # before_action :logged_in_user, only: [:edit, :update]
テスト結果がgreenになってしまう。
yokoyan:~/workspace/sample_app (updating-users) $ rails test Running via Spring preloader in process 2387 Started with run options --seed 18734 30/30: [========================================================================================================================================================================================================] 100% Time: 00:00:04, Time: 00:00:04 Finished in 4.19952s 30 tests, 83 assertions, 0 failures, 0 errors, 0 skips
原因は、UsersControllerTest
の中で、editアクションとupdateアクションを呼び出していないため。
(before_actionをテストしていない状態になっている)
class UsersControllerTest < ActionDispatch::IntegrationTest test "should get new" do get signup_path assert_response :success end end
上記のテストコードに、editアクションとupdateアクションの処理を追加する。
■テストケース1
- ユーザ編集画面に、GETリクエストを送信する
- フラッシュメッセージが空ではないこと(エラーになること)を確認する
- ログイン画面にリダイレクトする
■テストケース2
- ユーザ編集画面に、ユーザ情報とPATCHリクエストを送信する
- フラッシュメッセージが空ではないこと(エラーになること)を確認する
- ログイン画面にリダイレクトする
test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_url end test "should redirect update when not logged in" do patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert_not flash.empty? assert_redirected_to login_url end
【controller】テストコードの追加確認
再度、before_action
を有効にしてテストコードを実行する。
class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update]
テスト結果がgreenになることを確認。
yokoyan:~/workspace/sample_app (updating-users) $ rails test Running via Spring preloader in process 1921 Started with run options --seed 5738 32/32: [======================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.27503s 32 tests, 87 assertions, 0 failures, 0 errors, 0 skips
演習1
デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです (結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか (テストが失敗するかどうか) 確かめてみましょう。
only:オプション
をコメントアウトする。
class UsersController < ApplicationController # before_action :logged_in_user, only: [:edit, :update] before_action :logged_in_user
テスト結果がredになることを確認。
yokoyan:~/workspace/sample_app (updating-users) $ rails test Running via Spring preloader in process 1260 Started with run options --seed 865 FAIL["test_layout_links", SiteLayoutTest, 1.4123519230633974] test_layout_links#SiteLayoutTest (1.41s) Expected at least 1 element matching "title", found 0.. Expected 0 to be >= 1. test/integration/site_layout_test.rb:18:in `block in <class:SiteLayoutTest>' FAIL["test_should_get_new", UsersControllerTest, 1.9974089090246707] test_should_get_new#UsersControllerTest (2.00s) Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/login> test/controllers/users_controller_test.rb:11:in `block in <class:UsersControllerTest>' FAIL["test_invalid_signup_information", UsersSignupTest, 2.0474980850704014] test_invalid_signup_information#UsersSignupTest (2.05s) expecting <"users/new"> but rendering with <[]> test/integration/users_signup_test.rb:16:in `block in <class:UsersSignupTest>' FAIL["test_valid_signup_information", UsersSignupTest, 2.0680019629653543] test_valid_signup_information#UsersSignupTest (2.07s) "User.count" didn't change by 1. Expected: 2 Actual: 1 test/integration/users_signup_test.rb:30:in `block in <class:UsersSignupTest>' 32/32: [========================================================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.07602s 32 tests, 76 assertions, 4 failures, 0 errors, 0 skips
なお、すべてのアクションの実行前に必ずログイン要求を行うようになる。 「sign up」ボタンをクリックする。
ユーザー登録画面ではなく、ログイン画面が表示されてしまう。
10.2.2 正しいユーザーを要求する
本章での学び
テスト駆動による開発
以下の仕様でテストコードを作成する。
- fixtureに2人目のユーザー「archer」を追加する
- テストコードの修正
setup
メソッドにて、2人目のユーザーをインスタンス変数@other_user
に代入log_in_as(@other_user)
として、2人目のユーザー「archer」でログインしておく- その後、1人目のユーザー「michael」のユーザ情報変更画面や、ユーザ情報更新を行う
flash
が空であることを確認するroot_url
にリダイレクトさせる
【controller】beforeフィルターの修正
beforeフィルターから呼び出される、correct_user
メソッドを追加する。
- paramsのidを取得し、Databaseからユーザ情報を取得した結果を、インスタンス変数
@user
に代入 - 取得した
@user
と、現在のログイン中のユーザ情報を比較する(sessions_helper.rb
で定義したcurrent_user
を呼び出す) - 一致していない場合は、root_urlにリダイレクトさせる。
before_action :correct_user, only: [:edit, :update] ・・・省略・・・ def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user end
【helper】と【controller】のリファクタリング
Sessionsヘルパー内に、current_user?
という論理値を返すメソッドを作成する。
# 渡されたユーザーがログイン済みであればtrueを返す def current_user?(user) user == current_user end
コントローラの以下の部分を、
unless @user == current_user
以下のようにリファクタリングする。
unless current_user?(@user)
備忘録
リファクタリング中に、なぜか全角スペースが含まれてしまい、 2日ほど大ハマリしました。。。 以下に投稿しました。
改修後にrailsアプリで急にActionController::RoutingErrorが出るようになった時の対処法
演習1
何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
どちらもユーザー単位で実行されるアクションであるため。 showアクションとdestroyアクションも保護したほうが良い気がする。
yokoyan:~/workspace/sample_app (updating-users) $ rails routes Prefix Verb URI Pattern Controller#Action edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy
演習2
上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?
URLのGETパラメータのIDを書き換えるだけで良いため、editアクションのほうが簡単。
10.2.3 フレンドリーフォワーディング
本章での学び
フレンドリーフォワーディングとは
ユーザーが開こうとしていたページにリダイレクトしてあげること。
例)ログインしていないユーザーが、編集ページにアクセスしようとしていたなら、ログイン後も編集ページにリダイレクトする。
今の時点のアプリケーションは、無条件でユーザープロフィールページに遷移させているため、 フレンドリー(親切)ではない。
- 未ログイン状態で、https://xxx/edit/user/1 にアクセスする
- ログイン画面が表示される
- ログイン後、無条件にユーザープロフィールページが表示される
テストコードの仕様
テスト駆動開発のため、テストコードを先に書く。
- 編集ページにアクセスする
@user
でログインする- 編集ページにリダイレクトされること
test "successful edit with friendly forwarding" do get edit_user_path(@user) log_in_as(@user) assert_redirected_to edit_user_path(@user) name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end
【helper】フレンドリーフォワーディングの処理を実装する
Sessionsヘルパーに以下2つのヘルパーメソッドを追加。
- 記憶したURLにリダイレクトする
default
のURLは、Sessionコントローラのcreateアクションに追加する- 指定しない場合では、ユーザープロフィールページとなる模様。
- 値が
nil
でなければ、session[:forwarding_url]
を評価する - 次回ログインした時に備えて、転送用URLを削除する
def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end
- アクセスしようとしたURLをsessionに記憶しておく
- GETリクエストの場合のみ記憶する
def store_location session[:forwarding_url] = request.original_url if request.get? end
【filter】before_actionの修正
ログインしていなければ、前述のstore_location
を呼び出す
def logged_in_user unless logged_in? store_location ・・・略・・・ end end
【controller】sessionコントローラからリダイレクト処理を呼び出す
sessions_controller
にて、redirect_to
の代わりにredirect_back_or
メソッドを呼び出す
def create @user = User.find_by(email: params[:session][:email].downcase) if @user && @user.authenticate(params[:session][:password]) ・・・略・・・ # redirect_to @user redirect_back_or @user ・・・略・・・ end end
テスト実行
テスト駆動で作成したテストコードを再実行。 テストがgreenになることを確認。
yokoyan:~/workspace/sample_app (updating-users) $ rails test Running via Spring preloader in process 2540 Started with run options --seed 21765 35/35: [============================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.81708s 35 tests, 98 assertions, 0 failures, 0 errors, 0 skips
演習1
フレンドリーフォワーディングで、最初に渡されたURLにのみ確実に転送されていることを確認するテストを作成してみましょう。続けて、ログインを行った後、転送先のURLはデフォルト (プロフィール画面) に戻る必要もありますので、これもテストで確認してみてください。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうかを確認するテストを追加してみましょう。
users_edit_test.rb
にテストを追加する。
1. puts session[:forwarding_url]
の値が、ユーザー編集画面のURLと等しいことを確認する
puts session[:forwarding_url] http://www.example.com/users/762146111/edit
puts edit_user_path(@user) /users/762146111/edit
ということで、http://www.example.com
をedit_user_path(@user)
に付与して比較する。
2. ログイン後はデフォルト(プロフィール画面になる)ことを確認する
ログイン後は、forwarding_url
はnilになる。
3. 実装&確認
test "successful edit with friendly forwarding" do get edit_user_path(@user) assert_equal session[:forwarding_url], "http://www.example.com" + edit_user_path(@user) log_in_as(@user) assert_redirected_to edit_user_path(@user) assert_nil session[:forwarding_url] name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end
テスト結果がgreenになることを確認。
yokoyan:~/workspace/sample_app (updating-users) $ rails test Running via Spring preloader in process 4007 Started with run options --seed 8923 35/35: [============================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.57232s 35 tests, 100 assertions, 0 failures, 0 errors, 0 skips
演習2
7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください (デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう (デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって (コラム 1.1)、落ち着いて対処してみましょう)。
debuggerメソッドを追加する。
class SessionsController < ApplicationController def new debugger end
ブラウザを開いて、ログアウトしてから、/users/1/editにアクセスする。
byebugを使って、session[:forwarding_url]
の値と、request.get?
の値を確認する。
[1, 10] in /home/ubuntu/workspace/sample_app/app/controllers/sessions_controller.rb 1: class SessionsController < ApplicationController 2: def new 3: debugger => 4: end 5: 6: def create 7: @user = User.find_by(email: params[:session][:email].downcase) 8: if @user && @user.authenticate(params[:session][:password]) 9: #ユーザログイン後にユーザ情報のページヘリダイレクトする 10: log_in @user (byebug) session[:forwarding_url] "https://rails-tutorial-yokoyan.c9users.io/users/1/edit" (byebug) request.get? true (byebug)
参考記事
Railsチュートリアルにはbyebugの使い方がなく、なんじゃこりゃあ!?となってしまうので、 byebugの使い方の記事をあわせて読むと良いと思います。
printデバッグにさようなら!Ruby初心者のためのByebugチュートリアル
おわりに
Railsのデバッガを使うようになり、開発がやりやすくなってきました。 今回、全角スペースが混じってしまい、2〜3日はまってしまったので、IDEで表示できるようにするとか対策を考えたい・・・。