日別アーカイブ: 2021年11月27日

Rails-tutorialのまとめ10.4(管理ユーザー 主に演習)

10.4から続く

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と開発者に明確に示すことができます)。

boolean型の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に反転しています。

サンプルデータ生成タスクに管理者を1人追加する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ファイルを修正し、今いるサンプルユーザーの一人を管理者にしてみましょう。

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です

10.5.1 本章のまとめ

1:ユーザーは、編集フォームからPATCHリクエストをupdateアクションに対して送信し、情報を更新する

2:Strong Parametersを使うことで、安全にWeb上から更新させることができる

3:beforeフィルターを使うと、特定のアクションが実行される直前にメソッドを呼び出すことができる

4:beforeフィルターを使って、認可 (アクセス制御) を実現した

5:認可に対するテストでは、特定のHTTPリクエストを直接送信する低級なテストと、ブラウザの操作をシミュレーションする高級なテスト (統合テスト) の2つを利用した

6:フレンドリーフォワーディングとは、ログイン成功時に元々行きたかったページに転送させる機能である

7:ユーザー一覧ページでは、すべてのユーザーをページ毎に分割して表示する

8:rails db:seedコマンドは、db/seeds.rbにあるサンプルデータをデータベースに流し込む

9:render @usersを実行すると、自動的に_user.html.erbパーシャルを参照し、各ユーザーをコレクションとして表示する

10:boolean型のadmin属性をUserモデルに追加すると、admin?という論理オブジェクトを返すメソッドが自動的に追加される
11:管理者が削除リンクをクリックすると、DELETEリクエストがdestroyアクションに向けて送信され、該当するユーザーが削除される
12:fixtureファイル内で埋め込みRubyを使うと、多量のテストユーザーを作成することができる

その11に続く