【105日目】【1日20分のRailsチュートリアル】【第9章】すべてのユーザーを表示する
今日は「9.3 すべてのユーザーを表示する」から。
9.3 すべてのユーザーを表示する
この節では、いよいよ最後から2番目のユーザーアクションであるindexアクションを追加しましょう。このアクションは、すべてのユーザーを一覧表示します。
すべてのユーザーはどのアカウントからも見れるアプリケーションなんだっけ。Twitterみたいなやつ作るんだよね、確か。
9.3.1 ユーザーインデックス
ユーザーのshowページについては、今後も (ログインしているかどうかに関わらず) サイトを訪れたすべてのユーザーから見えるようにしておきますが、
ユーザーのindexページはログインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限します。
ほぅほぅ。
indexページを不正なアクセスから守るために、まずはindexアクションが正しくリダイレクトするか検証するテストを書いてみます (リスト9.31)。
indexアクションに関するテストを追加。
test/controllers/users_controller_test.rb
test "should redirect index when not logged in" do get :index assert_redirected_to login_url end
次に、beforeフィルターのlogged_in_userにindexアクションを追加して、このアクションを保護します (リスト9.32)。
Userコントローラーにindexアクションを追加して、brforeフィルターにもindexアクションを追加。
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] : def index end :
ここでテスト実行すると通りました。
idenxアクションのリダイレクト処理書いてないのにー、と一瞬思ったけど、
logged_in_user
でリダイレクト処理入れてましたね…。そういうことか。
$ bundle exec rake test 36 tests, 85 assertions, 0 failures, 0 errors, 0 skips
途中だけど今日はここまで。
今日の学習時間は【16分】。
次は「9.3.1 ユーザーインデックス」のindexビューを追加するところから。
【104日目】【1日20分のRailsチュートリアル】【第9章】フレンドリーフォワーディング機能を実装する
今日は「9.2.3 フレンドリーフォワーディング」の実装するところから。
9.2.3 フレンドリーフォワーディング
失敗するテストが書けたので、ようやくフレンドリーフォワーディングを実装する準備ができました。
ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要があります。
Sessionsヘルパーにリクエスト時点のページを保存するメソッドと保存したページにリダイレクトさせるメソッドを追加。
app/helpers/sessions_helper.rb
module SessionsHelper : # 記憶したURL (もしくはデフォルト値) にリダイレクト def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end # アクセスしようとしたURLを覚えておく def store_location session[:forwarding_url] = request.url if request.get? end end
ただし、GETリクエストが送られたときだけ格納するようにしておきます。
稀なケースに対応するために必要らしい。
こういったケースに対処しておかないと、POSTや PATCH、DELETEリクエストを期待しているURLに対して (リダイレクトを通して) GETリクエストが送られてしまい、場合によってはエラーが発生します。
不整合が発生するので対応しておく感じなのかな。。。なんかよく分からないけど今はいいや。
先ほど定義したstore_locationメソッドを使って、早速beforeフィルターのlogged_in_userを修正してみます (リスト9.28)。
logged_in_user
メソッドにstore_location
を追加。
app/controllers/users_controller.rb
: # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? store_location flash[:danger] = "Please log in." redirect_to login_url end end :
フォワーディング自体を実装するには、redirect_back_orメソッドを使用します。
(中略)
デフォルトのURLは、Sessionコントローラのcreateアクションに追加し、サインイン成功後にリダイレクトします (リスト9.29)。
Sessionコントローラのcreateアクションにてredirect_back_or
メソッドを使うように変更。
app/controllers/sessions_controller.rb
def create @user = User.find_by(email: params[:session][:email].downcase) if @user && @user.authenticate(params[:session][:password]) log_in @user params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) redirect_back_or @user else : end end
サンプルソースはuser
だけど私のコード@user
使ってる。。。
なんかどっかで直した気がするのでとりあえず@user
のままで。問題はないはず。
session.delete(:forwarding_url) という式を通して転送用のURLを削除している点に注意してください。
これをやっておかないと、次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまいます。
redirect_back_orメソッドの中身の話。
確かにこれうっかり忘れそうだな。使ったら初期化しておく。
実装が終わったのでテスト実行して問題ないことを確認。
これで基本ユーザー認証機能とページ保護機能の実装は完了。
$ bundle exec rake test 35 tests, 84 assertions, 0 failures, 0 errors, 0 skips
今日の学習時間は【26分】。
次は「9.3 すべてのユーザーを表示する」から。
【103日目】【1日20分のRailsチュートリアル】【第9章】ユーザーフレンドリーな機能のテストを追加する
今日は「9.2.2 正しいユーザーを要求する」のリファクタリングから。
9.2.2 正しいユーザーを要求する
最後に、リファクタリングではありますが、一般的な慣習に倣ってcurrent_user?という論理値を返すメソッドを実装します。
あるユーザーが今ログインしているユーザーかどうかチェックするメソッドを実装する。
app/helpers/sessions_helper.rb
# 与えられたユーザーがログイン済みユーザーであればtrueを返す def current_user?(user) user == current_user end
@user == current_user
をcurrent_user?
で置き換えることでちょっと分かりやすいコードに。
app/controllers/users_controller.rb
# 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end
9.2.3 フレンドリーフォワーディング
(省略)後1つ小さなキズがあります。保護されたページにアクセスしようとすると、問答無用で自分のプロファイルページに移動させられてしまいます。
アクセスしようとしてたページに移動して欲しいよね。
実際のテストはまず編集ページにアクセスし、ログインした後に、(デフォルトのプロフィールページではなく) 編集ページにリダイレクトされているかどうかをチェックするといったテストです。
編集ページへのアクセスについてテストを下記のように修正。
test/integration/users_edit_test.rb
: 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), 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 end
(なお、リダイレクトによってedit用のテンプレートが描画されなくなったので、リスト9.26では該当するテストを削除しています)
テスト内のこのコードのことかな。
assert_template 'users/edit'
まだアクセスしようとしてたページにリダイレクトする処理は実装してないのでこの段階ではテストは失敗します。
$ bundle exec rake test 35 tests, 79 assertions, 1 failures, 0 errors, 0 skips
項の途中だけど今日はここまで。
今日の学習時間は【21分】。
次は「9.2.3 フレンドリーフォワーディング」の処理を実装するところから。
【102日目】【1日20分のRailsチュートリアル】【第9章】ユーザーが自分の情報だけを編集できるように実装を追加する
今日は「9.2.2 正しいユーザーを要求する」から。
9.2.2 正しいユーザーを要求する
当然のことですが、ログインを要求するだけでは十分ではありません。ユーザーが自分の情報だけを編集できるようにする必要があります。
勝手に自分の情報を編集されたら困るもんね。
そこで本項では、セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。
ほぅほぅ。
まずはユーザーの情報が互いに編集できないことを確認するために、サンプルユーザーをもう一人追加します。ユーザー用のfixtureファイルに2人目のユーザーを追加してみましょう (リスト9.20)。
2人目のユーザーを追加。
test/fixtures/users.yml
archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>
次に、 リスト8.50で定義したlog_in_asメソッドを使って、editアクションとupdateアクションをテストします (リスト9.21)。
setup
でもう一つのユーザーとしてさっき追加ユーザーを設定して、
edit
/update
アクションそれぞれでログインしているユーザーとは別のユーザーのプロフィールページを編集/更新をチェックするテストを追加。
test/controllers/users_controller_test.rb
: def setup @user = users(:michael) @other_user = users(:archer) end : test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get :edit, id: @user assert flash.empty? assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch :update, id: @user, user: { name: @user.name, email: @user.email } assert flash.empty? assert_redirected_to root_url end end
別のユーザーのプロフィールを編集しようとしたらリダイレクトさせたいので、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにします (リスト9.22)。
別のユーザーのプロフィールを編集しようとしたらルートURLにリダイレクトする処理を追加する。
app/controllers/users_controller.rb
: before_action :correct_user, only: [:edit, :update] : # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user end end
beforeフィルターのcorrect_userで@user変数を定義しているため、リスト9.22ではeditとupdateの各アクションから、@userへの代入文を削除している点にも注意してください。
@user変数の定義は二重にはいらないってことかな。。。
コメントアウトしとこう。
def edit # @user = User.find(params[:id]) end def update # @user = User.find(params[:id]) :
ここまで実装できたらさっき追加したテストが通るはずなので、テスト実行。
$ bundle exec rake test 35 tests, 83 assertions, 0 failures, 0 errors, 0 skips
問題なし。
途中だけどキリがいいので今日はここまで。
今日の学習時間は【20分】。
次は「9.2.2 正しいユーザーを要求する」のリファクタリングから。
【101日目】【1日20分のRailsチュートリアル】【第9章】ユーザーにログインを要求する処理のテストを作成する
今日は「9.2.1 ユーザーにログインを要求する」のテストを修正するところから。
9.2.1 ユーザーにログインを要求する
原因は、editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗するようになったためです
テストに失敗するようになってしまったのでテストを修正するところから。
このため、editアクションやupdateアクションをテストする前にログインしておく必要があります。
テストユーザーとしてログインする、log_in_asヘルパーを使うとのこと。
test/integration/users_edit_test.rb
: test "unsuccessful edit" do log_in_as(@user) : test "successful edit" do log_in_as(@user) :
テスト実行してテストが通るようになったことを確認。
$ bundle exec rake test 31 tests, 75 assertions, 0 failures, 0 errors, 0 skips
これでテストスイートがパスするようになりましたが、実はbeforeフィルターの実装はまだ終わっておりません。
え、そうなん。
セキュリティモデルに関する実装を取り外してもテストがGREENになってしまうかどうか、実際にコメントアウトして確かめてみましょう (リスト9.16)。
この前追加したコードをコメントアウトしてみる。
app/controllers/users_controller.rb
class UsersController < ApplicationController # before_action :logged_in_user, only: [:edit, :update] :
テスト実行。
$ bundle exec rake test 31 tests, 75 assertions, 0 failures, 0 errors, 0 skips
あら、通りますね。
beforeフィルターをコメントアウトして巨大なセキュリティーホールが作られたら、テストスイートでそれを検出できるべきです。
ふむふむ。
テスト追加しましょう。
beforeフィルターは基本的にアクションごとに適用していくので、Usersコントローラのテストもアクションごとに書いていきます。
setup
とedit/updateアクションを実行してフラッシュが表示されること、ログインページにリダイレクトすることを確認するテストを追加。
test/controllers/users_controller_test.rb
: def setup @user = users(:michael) end : test "should redirect edit when not logged in" do get :edit, id: @user assert_not flash.empty? assert_redirected_to login_url end test "should redirect update when not logged in" do patch :update, id: @user, user: { name: @user.name, email: @user.email } assert_not flash.empty? assert_redirected_to login_url end end
この時点では、(beforeフィルターが無効のままなので) テストスイートはREDになるはずです。beforeフィルターのコメントアウトを元に戻して、GREENになるかどうか確かめてみましょう (リスト9.18)。
コメントアウトしたままテスト実行してみると失敗する。
$ bundle exec rake test 33 tests, 79 assertions, 2 failures, 0 errors, 0 skips
beforeフィルターのコメントアウトを戻してテスト実行してみると成功した。
$ bundle exec rake test 33 tests, 79 assertions, 0 failures, 0 errors, 0 skips
これらのテストを実装したことによって、うっかり誰でも編集できてしまうバグがあっても、すぐに検知できるようになりました。
検知できる仕組みだいじ。
今日の学習時間は【20分】。
次は「9.2.2 正しいユーザーを要求する」から。
【100日目】【1日20分のRailsチュートリアル】【第9章】ユーザーにログインを要求する処理を追加
遂に100日目!
今日は「9.2 認可」から。
9.2 認可
9.1のeditアクションとupdateアクションはすでに完全に動作していますが、セキュリティ上の大穴が1つ空いています。
どのユーザーでもあらゆるアクションにアクセスでき、ログインさえしていれば他のユーザーの情報を編集できてしまいます。
URL直打ちすれば他のユーザーの編集ページにアクセスできるってことかな。
9.2.1では、ログインしていないユーザーが保護されたページにアクセスしようとした際のケースについて対処していきます。
こういったケースはアプリケーションを使っていると普通に起こることなので、ログインページに転送して、そのときに分かりやすいメッセージも表示するようにしましょう。
よくありますね。確かに今はその機能ないや。
9.2.1 ユーザーにログインを要求する
図9.6のように転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルターを使います。
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] : private : # beforeフィルター # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end end
デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:onlyオプションハッシュを渡すことによって:editと:updateアクションにのみこのフィルタが適用されるように制限をかけています。
フィルタかけられるのか。楽だなー。
beforeフィルターを使って実装した結果 (リスト9.12) は、一度ログアウトしてユーザー編集ページ (/users/1/edit) にアクセスしてみることで確認できます (図9.7)。
動作見てみよう。
サーバーを起動して
$ rails server -b $IP -p $PORT
アプリケーションからログアウトしていることを確認してユーザー編集ページにアクセス。
ログインページとログインを促すメッセージが表示されました。
ちょっと気になってるんだけど、セッションの永続化してるはずなのに日をまたぐ(パソコン再起動挟む)とログアウト状態になってるのはいいのかな。。
もしくは毎回新しいブラウザでアプリケーションにアクセスしているからか。
なんでだろう~。
リスト9.12のキャプションに記したように、今の段階ではテストは失敗します。
テスト失敗するようになってしまった。
editアクションやupdateアクションでログインを要求するようにしたため、らしい。
$ bundle exec rake test : FAIL["test_successful_edit", UsersEditTest, 2017-01-24 19:32:59 +0000] test_successful_edit#UsersEditTest (1485286379.47s) expecting <"users/edit"> but rendering with <[]> test/integration/users_edit_test.rb:21:in `block in <class:UsersEditTest>' FAIL["test_unsuccessful_edit", UsersEditTest, 2017-01-24 19:32:59 +0000] test_unsuccessful_edit#UsersEditTest (1485286379.48s) expecting <"users/edit"> but rendering with <[]> test/integration/users_edit_test.rb:11:in `block in <class:UsersEditTest>' 31 tests, 69 assertions, 2 failures, 0 errors, 0 skips
途中だけど今日はここまで。
今日の学習時間は【12分】。
次は「9.2.1 ユーザーにログインを要求する」のテストを修正するところから。
【99日目】【1日20分のRailsチュートリアル】【第9章】テスト駆動開発でプロフィール編集処理を実装する
今日は「9.1.4 TDDで編集を成功させる」から。
9.1.4 TDDで編集を成功させる
プロファイル画像の編集は、画像のアップロードをGravatarに任せてあるので、既に動作するようになっています。
Gravatarの方で画像の編集をする感じか。
今回はテスト駆動開発を使ってユーザーの編集機能を実装してみましょう。
TDDって「テスト駆動開発」のことか。そうか…覚えてなかったよ。。。
flashメッセージが空でないかどうかと、プロフィールページにリダイレクトされるかどうかをチェックします。
また、データベース内のユーザー情報が正しく変更されたかどうかも検証します。
データベース内のユーザー情報が正しく変更されたかについては、@user.reload
でデータベース内のユーザー情報を取得して情報を比較するそう。
test/integration/users_edit_test.rb
test "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), 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
テストにパスする必要のある、リスト9.8のupdateアクションは、リスト9.9に示したように、createアクション (リスト8.22) の最終的なフォームとほぼ同じです。
updateアクションにflashメッセージの表示とプロフィールページのリダイレクト処理を追加。
app/controllers/users_controller.rb
def update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else :
パスワードのバリデーションに対して、空だったときの例外処理を加える必要があります。
パスワードのバリデーションに空だったときの例外処理(allow_nil: true
)を追加。
app/models/user.rb
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
すべてのテストを走らせてみて、成功したかどうか確かめてみてください。
テストは通りました。
$ bundle exec rake test 31 tests, 75 assertions, 0 failures, 0 errors, 0 skips
一応ユーザーの編集も試してみよう。
サーバーを起動して
$ rails server -b $IP -p $PORT
プロフィール編集ページにアクセス。
名前とか適当に変えてみてボタンクリック。
フラッシュメッセージも表示されたしパスワード欄空でも問題なし。
ちゃんと動いてそう。
今日の学習時間は【25分】。
次は「9.2 認可」から。