紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【10章】Ruby on Railsチュートリアル演習まとめ&解答例【10.2 認可】

はじめに

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

未ログイン状態であっても、ユーザ情報編集画面にアクセスすることができてしまう。

未ログイン状態でサイトを表示する。 image

URLに、https://xxxx/users/1/edit  を打ち込んでアクセスすると、IDが1番のユーザ情報変更画面が表示されてしまう。

image

現在の実装の課題2

ログイン状態であれば、他のユーザーの情報が見えてしまう。 また、他のユーザーの情報を更新することも可能。

IDが1番のユーザでログインする。 image

URLのhttps://xxxx/users/1/edit を、https://xxxx/users/2/edit に 書き換えてアクセスすると、IDが2番のユーザ情報変更画面が表示されてしまう。

image

課題の原因

  • 要ログイン画面に対して、ログインの制御をかけていないため
  • 自分以外のユーザ情報に対して制限をかけていないため

原因への対策

  • 未ログインのユーザが、要ログイン画面にアクセスした場合、ログイン画面に転送する
  • ログイン済みのユーザが、許可されていないページにアクセスした場合、ルート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

動作確認

未ログイン状態でサイトにアクセスする。 image

URLに、https://xxxx/users/1/edit を打ち込んでアクセスすると、IDが1番のユーザ情報変更画面ではなく、ログイン画面が表示されるようになる。 image

テストコードの修正

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」ボタンをクリックする。

image.png

ユーザー登録画面ではなく、ログイン画面が表示されてしまう。

image.png

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 にアクセスする
  • ログイン画面が表示される image.png
  • ログイン後、無条件にユーザープロフィールページが表示される image.png

テストコードの仕様

テスト駆動開発のため、テストコードを先に書く。

  • 編集ページにアクセスする
  • @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.comedit_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で表示できるようにするとか対策を考えたい・・・。