10.4.1 管理ユーザー
特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?メソッド
(論理値を返す) も使えるようになりますので、
これを使って管理ユーザーの状態をテストできます。
rails generate migration add_admin_to_users admin:boolean
default: falseという引数をadd_columnに追加しています。これは、
デフォルトでは管理者になれないということを示すためです。
(default:false引数を与えない場合、 adminの値はデフォルトでnilになりますが、これはfalseと同じ意味ですので、必ずしもこの引数を与える必要はありません。
ただし、このように明示的に引数を与えておけば、
コードの意図をRailsと開発者に明確に示すことができます)。
admin
属性をUserに追加するマイグレーション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?メソッドも利用できるようになっています。
rails console --sandbox >> user = User.first >> user.admin? => false >> user.toggle!(:admin) => true >> user.admin? => true
ここではtoggle!メソッドを使って admin属性の状態をfalseからtrueに反転しています。
db/seeds.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
次に、データベースをリセットして、サンプルデータを再度生成します。
rails db:migrate:reset
rails db:seed
攻撃者は次のようなPATCHリクエストを送信してくるかもしれません。
patch /users/17?admin=1
このリクエストは、17番目のユーザーを管理者に変えてしまいます。
次のようにparamsハッシュに対してrequireとpermitを呼び出します。
def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end
演習
1:Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、PATCHを直接ユーザーのURL (/users/:id) に送信するテストを作成してみてください。
テストが正しい振る舞いをしているかどうか確信を得るために、
まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。
最初のテストの結果は redになるはずです。
admin属性の変更が禁止されていることをテストする
test/controllers/users_controller_test.rb
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(@other_user), params: { user: { password: FILL_IN, password_confirmation: FILL_IN, admin: FILL_IN } } assert_not @other_user.FILL_IN.admin? end
users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest (中略) 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(@other_user), params: { user: { password: @other_user.password, password_confirmation: @other_user.password, admin: true } } assert_not @other_user.reload.admin? end end
10.4.2 destroy
アクション
Usersリソースの最後の仕上げとして、destroyアクションへのリンクを追加しましょう。
まず、ユーザーindexページの各ユーザーに削除用のリンクを追加し、
続いて管理ユーザーへのアクセスを制限します。
これによって、現在のユーザーが
管理者のときに限り[delete]リンクが表示されるようになります。
app/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>
ブラウザはネイティブではDELETEリクエストを送信できないため、
RailsではJavaScriptを使って偽造します。
つまり、JavaScriptがオフになっているとユーザー削除のリンクも無効になるということです。
JavaScriptをサポートしないブラウザをサポートする必要がある場合は、フォームとPOSTリクエストを使ってDELETEリクエストを偽造することもできます。
こちらはJavaScriptがなくても動作します
destroyアクションを追加する必要があります。
このアクションでは、該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除し、最後にユーザーindexに移動します
ユーザーを削除するためにはログインしていなくてはならないので、
:destroyアクションもlogged_in_userフィルターに追加しています。
実際に動作するdestroyアクションを追加する
app/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
ある程度の腕前を持つ攻撃者なら、コマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除してしまうことができるでしょう。
サイトを正しく防衛するには、destroyアクションにもアクセス制御を行う必要があります。
beforeフィルターでdestroyアクションを管理者だけに限定する
app/controllers/users_controller.rb
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
演習
1:管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。
ユーザーを削除すると、Railsサーバーのログにはどのような情報が
表示されるでしょうか?
DELETE FROM "users" WHERE "users"."id" = ? [["id", 28]] (9.0ms) commit transaction Redirected to https://・・・/users
10.4.3 ユーザー削除のテスト
ユーザーを削除するといった重要な操作については、期待された通りに動作するか確かめるテストを書くべきです。そこで、まずはユーザー用fixtureファイルを修正し、今いるサンプルユーザーの一人を管理者にしてみましょう。
test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
削除をテストするために、DELETEリクエストを発行してdestroyアクションを
直接動作させます。このとき2つのケースをチェックします。1つは、
ログインしていないユーザーであれば、ログイン画面にリダイレクトされることです。
もう1つは、ログイン済みではあっても管理者でなければ、
ホーム画面にリダイレクトされることです。
管理者権限の制御をアクションレベルでテストする green
test/controllers/users_controller_test.rb
require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest 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 end
assert_no_differenceメソッドを使って、ユーザー数が変化しないことを確認している点に注目してください。
assert_differenceメソッドを使ってユーザーが作成されたことを確認しましたが、
今回は同じメソッドを使ってユーザーが削除されたことを確認しています。
具体的には、DELETEリクエストを適切なURLに向けて発行し、User.countを使ってユーザー数が 1 減ったかどうかを確認しています。
削除リンクとユーザー削除に対する統合テスト green
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
各ユーザーの削除リンクをテストするときに、ユーザーが管理者であればスキップしている点にも注目してください。
演習
試しにapp/controllers/users_controller.rb
にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果が redに変わることを確認してみましょう。
before_action :admin_userをコメントアウトして確認したらOKです