10.4 ユーザーの削除
ユーザーの一覧ページは完了した。残るはdestroy
だけ。
これを実装することで、RESTに準拠した正統なアプリとなる。
ここでは、ユーザーを削除するためのリンクを追加する。また、削除を行うのに必要なdestroy
アクションも実装する。
まず、削除を実行できる権限をもつ管理(admin)ユーザーのクラスを作成。
10.4.1 管理ユーザー
特権を持つ管理ユーザーを識別するために、論理値をとるadmin
属性をUserモデルに追加する。こうすると自動的にadmin?
メソッド(論理値を返す)も使えるようになるので、これを使って管理ユーザーの状態をテストできる。
変更後のデータモデルは次の通り
id | integer |
name | string |
string | |
created_at | datetime |
updated_at | datetime |
password_digest | string |
remember_digest | string |
admin | boolean |
アドミン属性をUserモデルに追加
ここでいつものように、マイグレーションを実行してadmin
属性を追加する。この属性の型はboolean
とする。
rails g migration add_admin_to_users admin:boolean
マイグレーションを実行するとadmin
カラムがusers
テーブルに追加される。
下の①ではdefault: false
という引数をadd_column
に追加している。
これは、デフォルトでは管理者になれないと示すため(default: false
引数を与えない場合、admin
の値はnil
になるが、これはfalse
と同じ意味になるので、必ずしもこの引数を与える必要はない。ただし、このように明示的に引数を与えておけば、てコードの意図をRailsと開発者に明確に示しことができる)。
db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.1] def change add_column :users, :admin, :boolean, default: false ←① end end
マイグレーションを実行
$ rails db:migrate
Railsコンソールで動作を確認すると、期待どおりadmin
属性が追加されて論理値をとり、さらに疑問符の付いたadmin?
メソッドも利用できるようになっている。
>> user = User.first >> user.admin? => false >> user.toggle!(:admin) ←① => true >> user.admin? => true
①:toggle!
メソッドでadmin
属性をfalseからtrueに反転している。。
仕上げに、最初のユーザーだけをデフォルトで管理者にするようサンプルデータを更新する(下の①)。
db/seed.rb
User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true) ←① 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
次に、DBをリセットして、サンプルデータを再度生成する。
rails db:migrate:reset rails db:seed
Strong Parameters、再び
上のseeds.rb
では、初期化ハッシュにadmin: true
を設定することでユーザーを管理者にしている。ここでは、荒れ狂うWeb世界にオブジェクトを晒すことの危険性を改めて強調している。
もし、任意のWebリクエストの初期化ハッシュをオブジェクトに渡せるとなると、攻撃者は次のようなPATCHリクエストを送信してくるかもしれない。
patch /users/17?admin=1
このリクエストは、17番目のユーザーを管理者に変えてしまう。ユーザーのこの行為は、少なくとも重大なセキュリティ違反となる可能性があるし、実際にはそれだけでは済まない。
このような危険があるからこそ、編集してもよい安全な属性だけを更新することが重要になる。7.3.2で説明した通りStrong Parametersを使って対策をする。具体的には、次のようにparams
ハッシュに対してrequire
とpermit
を呼び出す。
def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation)
上のコードでは、許可された属性リストにadmin
が含まれていないことに注目。これにより、任意のユーザーが自分自身にアプリの管理者権限を与えることを防止できる。この問題は重大であるため、編集可能になってはならない属性に対するテストを作成してみる。
10.4.2 destroyアクション
Usersリソースの最後の仕上げとして、destroy
アクションへのリンクを追加する。まず、ユーザーindexページの各ユーザーに削除用のリンクを追加し、続いて管理ユーザーへのアクセスを制限する。これによって、現在のユーザーが管理者のときに限り[delete
]リンクが表示されるようになる。
views/users/_user.html.erb
<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %>←① | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %> <% end %> </li>
①:current_user
が管理者かつ(削除対象の)ユーザがcurrent_user
でない。ここでいうユーザーはパーシャルの呼び出し元のindex.html.erb
でeach
文で取り出したuser
変数のこと。管理者が自分自身を削除できないようになっている。
必要なDELETE
リクエストを発行するリンクの生成は、method: :deleteによって行われている点に注意!また、各リンクをif文で囲い、管理者にだけ削除リンクが表示されるようにしている。ブラウザはネイティブではDELETE
リクエストを送信できないため、RailsではJavaScriptを使って偽造する。つまり、JavaScriptがオフになっているとユーザー削除のリンクも無効になるとういうこと!JavaScriptをサポートしないブラウザをサポートする必要がある場合は、フォームとPOSTリクエストを使ってDELETEリクエストを偽造することもできる。こちらはJavaScriptがなくても動作する。
destroy
アクションの実装
この削除リンクが動作するためには、destroy
アクションを追加する必要がある。このアクションでは、該当するユーザーを見つけてActive Recordのdestroy
メソッドを使って削除し、最後にユーザーindexに移動する。ユーザーを削除するためにはログインしていなければならないので、destroy
アクションもlogged_in_user
フィルターに追加する。
controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] ←追加 before_action :correct_user, only: [:edit, :update] : def destroy User.find(params[:id]).destroy ←①追加 flash[:success] = "User deleted" ←追加 redirect_to users_url ←追加 end
①で:find
メソッドとdestroy
メソッドを1行で書くために2つのメソッドを連結している点に注目。結果として、管理者だけがユーザーを削除できるようになる。(より具体的には、削除リンクが見えているユーザーのみ削除できる)。
しかし、実はまだ大きなセキュリティホールがある。ここでコマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除してしまうことができてしまう。サイトを正しく防衛するには、destroy
アクションにもアクセス制御を行う必要がある。これを実装してようやく、管理者だけがユーザーを削除できるようにする。
destroy
アクションへのアクセス制御
今回はbefore
フィルターを使ってdestroy
アクションへのアクセスを制御する。実装するadmin_user
フィルターを次に示す。
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] before_action :admin_user, only: :destroy ←追加 : private : #管理者かどうか確認 def admin_user redirect_to(root_url) unless current_user.admin? ←① end end
①:現在のユーザーが管理者でなければroot_urlへリダイレクト
10.4.3 ユーザー削除のテスト(重要)
ユーザーを削除するといった重要な操作については、期待された通りに動作するか確かめるテストを書くべき。そこで、まずはユーザー用fixtureファイルを修正し、今いるサンプルユーザーの1人を管理者にしてみる。
test/fixtures/users.yml
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true ←追加 archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.com password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>
次に、Usersコントローラをテストするために、アクション単位でアクセス制御をテストする。
ログアウトのテストと同様に、削除をテストするために、DELETEリクエストを発行してdestroy
アクションを直接動作させる。
このとき2つのケースをチェックする。
1.ログインしていないユーザーであれば、ログイン画面にリダイレクトされる
2.ログイン済みであっても管理者でなければ、ホーム画面にリダイレクトさせれる
作成したコードが次
test/controllers/users_controller.rb
def setup @user = users(:michael) @other_user = users(:archer) end : test "should redirect destroy when not logged in" do assert_no_difference 'User.count' do ←① delete user_path(@user) end assert_redirected_to login_url end test "should redirect destroy when logged in as a non-admin" do log_in_as(@other_user) assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to root_url end
①:assert_no_differenceメソッドを使って、ユーザー数が、ブロックで渡されたものを呼び出す前と呼び出した後で違いがなければtrue
上のテストでは、管理者ではないユーザーの振る舞いについて検証しているが、管理者ユーザーの振る舞いと一緒に確認できるとよい。そこで、管理者であればユーザー一覧画面に削除リンクが表示される仕様を利用して、今回のテストを追加していくことにする。これにより、後ほど追加する管理者の振る舞いについても簡単にテストが書けそう。
さて、今回のテストで唯一の手の込んだ箇所は、管理者が削除リンクをクリックしたときに、ユーザーが削除されたことを確認する部分。今回は次のようなテストで実現した。
assert_difference 'User.count', -1 do delete user_path(@other_user) end
7章ではassert_difference
メソッドを使ってユーザーが作成されたことを確認したが、今回は同じメソッドを使ってユーザーが削除されたことを確認している。
具体的には、DELETEリクエストを適切なURLに向けて発行し、User.count
を使ってユーザー数が1減ったかどうかを確認している。したがって、管理者や一般ユーザーのテスト、そしてページネーションや削除リンクのテストを全てまとめると、次のようになる。
test/integration/users_index_test.rb
require 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @admin = users(:michael) @non_admin = users(:archer) end test "index as admin including pagination and delete links" do log_in_as(@admin) get users_path assert_template 'users/index' assert_select 'div.pagination' first_page_of_users = User.paginate(page: 1) first_page_of_users.each do |user| assert_select 'a[href=?]', user_path(user), text: user.name unless user == @admin assert_select 'a[href=?]', user_path(user), text: 'delete' end end assert_difference 'User.count', -1 do delete user_path(@non_admin) end end test "index as non-admin" do log_in_as(@non_admin) get users_path assert_select 'a', text: 'delete', count: 0 end end
各ユーザーの削除リンクをテストする時に、ユーザーが管理者であればスキップしている点にも注目。これは、管理者であれば削除リンクが表示されないから。