エンジニア志望のブログ

Railsチュートリアル第10章

はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境  Ruby: 2.7.2 , Rails: 6.1.4

メモ

パスワードが空のままでもユーザー情報が編集できるようにする

パスワードのvalidatesにallow_nil: trueを追加する。

ユーザーの新規作成時にはhas_secure_passwordがpasswordとpassword_confirmationの存在性を検証するようになっているため、ユーザーの新規作成時に空のパスワードが有効になることはない。

認可と認証

認証(authentication):サイトのユーザーを識別すること
認可(authorization) :ユーザーが実行可能な操作を管理すること

セキュリティモデル

ログインしたユーザーのみがユーザの情報を変更できるようにする、ログインしたユーザーのみが見ることができるページを設定するような仕組み

Railsではbeforeフィルターを用いて実現する。

フレンドリーフォワーディング

ログイン成功時に元々行きたかったページに転送させる機能

ログインをせずに保護されたページを開いた場合ログイン画面に移動させられ、
移動させられたログイン画面でユーザーがログインすると保護された開きたかったページに転送する仕組み

実装の流れ

ログインしていないユーザーが保護されたページにGETリクエストを送った場合
session変数を利用して開きたかったページの情報を保存しておく

session[:forwarding_url] = request.original_url if request.get?

実際にログインした後にリダイレクトさせるには

 redirect_to session[:forwarding_url]

リダイレクトした後に保存しておいた情報を削除することを忘れずに
削除しないと次回ログインした時に保存されたページに転送されてしまう。

session.delete(:forwarding_url)

ページネーション

ひとつのページに決められたユーザー数だけを表示するような仕組み

実装の流れ

gemファイルをインストール
 will_paginate gem
 bootstrap-will_paginate gem
表示させたいオブジェトをページネーションが理解できるオブジェクトに置き換える

    @users = User.paginate(page: params[:page])

ビューにwill_paginateメソッドを追加する

サンプルユーザーの生成

データベースにサンプルデータを生成する
db/seeds.rbファイルに記述。

# メインのサンプルユーザーを1人作成する
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

# 追加のユーザーをまとめて生成する
99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

create!はcreateアクションと同じ動きをするがcreate!では無効なデータを生成する場合例外を発生させる。

サンプルデータを生成

$ rails db:seed

role

あるユーザーだけにある処理を実行できる権限を与えること

DELETEリクエストの偽造

ブラウザはネイティブでDELETEリクエストを送信できない
RailsではJavaScriptを使ってDELETEリクエストを偽造する
よって、JavaScriptがオフになっているとDELETEのリンクが無効になる
このような場合フォームとPOSTリクエストを使ってDELETEリクエストを偽造することができる

演習

10.1.1

演習1

先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング(Phising)サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。

    <div class="gravatar_edit">
      <%= gravatar_for(@user) %>
      <a href="https://gravatar.com/emails" target="_blank", rel="noopener">change</a>
    </div>

10.1.3

演習1
  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } }
    assert_template 'users/edit'
    assert_select "div.alert-danger", "The form contains 4 errors"
  end

10.2.2

演習1

何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。

自分以外のユーザーの情報を悪意のあるユーザーに見られる、あるいは書き換えられる危険性があるから。

演習2

上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?

editアクション
URLのID部の値を書き換えるだけで他のユーザーの編集画面を開くことができるから。

10.2.3

演習1
    test "successful edit with friendry forwarding" do
    get edit_user_path(@user)
    assert_equal session[:forwarding_url], edit_user_url(@user) ###
    log_in_as(@user)
    assert_redirected_to edit_user_url(@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_url @user
    @user.reload
    assert_equal name, @user.name
    assert_equal email, @user.email
  end

10.3.1

演習1
レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。

  test "layout links when logged in user" do
    log_in_as(@user)
    get root_path
    assert_template "static_pages/home"
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", login_path, count: 0
  end

10.3.4

演習2
先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが green のままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。

  test "index including pagination" do
    log_in_as(@user)
    get users_path
    assert_template 'users/index'
    assert_select "div.pagination", count: 2
    User.paginate(page: 1).each do |user|
      assert_select "a[href=?]", user_path(user), text: user.name
    end
  end

10.4.1.2

演習1

Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL(/users/:id)に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は red になるはずです。最後の行では、更新済みのユーザー情報をデータベースから読み込めることを確認します( 6.1.5)。

  test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@user), params: { user: { password: "password",
      password_digest: "password", admin: true } }
    assert_not @other_user.reload.admin?
  end

おわりに

この章は前章に比べて理解しやすかったです。
9章が鬼門なのかな、、