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

Rails-tutorialのまとめ11.3(アカウントの有効化 主に演習)

その11.2から続く

11.3 アカウントを有効化する

メールが生成できたら、今度はAccountActivationsコントローラのeditアクション
を書いていきましょう。また、アクションへのテストを書き、しっかりとテスト
できていることが確認できたら、AccountActivationsコントローラからUser
モデルにコードを移していく作業 (リファクタリング) にも取り掛かっていきます。

11.3.1 authenticated?メソッドの抽象化

有効化トークンとメールをそれぞれparams[:id]とparams[:email]で参照できることを思い出してみましょう。
パスワードのモデルと記憶トークンで学んだことを
元に、次のようなコードでユーザーを検索して認証することにします。

user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])

これから実装するauthenticated?メソッドでは、受け取ったパラメータに応じて呼び出すメソッドを切り替える手法を使います。

この一見不思議な手法は「メタプログラミング」と呼ばれています。
メタプログラミングを一言で言うと「プログラムでプログラムを作成する」ことです。メタプログラミングはRubyが有するきわめて強力な機能であり、
Railsの一見魔法のような機能 (「黒魔術」とも呼ばれます) の多くは、
Rubyのメタプログラミングによって実現されています。
ここで重要なのは、sendメソッドの強力きわまる機能です。
このメソッドは、渡されたオブジェクトに「メッセージを送る」ことによって、呼び出すメソッドを動的に決めることが
できます。
例を見てみましょう。Railsコンソールを開き、
Rubyのオブジェクトに対してsendメソッドを実行し、配列の長さを得るとします

rails console
>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send("length")
=> 3

sendを通して渡したシンボル:lengthや文字列"length"は、いずれもlengthメソッドと同じ結果になりました。つまり、どちらもオブジェクトにlengthメソッドを渡しているため、等価なのです。もう1つ例をお見せします。データベースの最初のユーザーが持つactivation_digest属性にアクセスする例です。

>> user = User.first
>> user.activation_digest
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtR"
>> user.send(:activation_digest)
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtR"
>> user.send("activation_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtR"
>> attribute = :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtR"

シンボル:activationと等しいattribute変数を定義し、文字列の式展開 (interpolation) を使って引数を正しく組み立ててから、sendに渡しています。文字列'activation'でも同じことができますが、Rubyではシンボルを使う方が一般的です。

"#{attribute}_digest"

シンボルと文字列どちらを使った場合でも、上のコードは次のように文字列に変換されます。

"activation_digest"

sendメソッドの動作原理がわかったので、この仕組みを利用してauthenticated?メソッドを書き換えてみましょう。

抽象化されたauthenticated?メソッド red
app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
red
rails test

テストが失敗する理由は、current_userメソッド とnilダイジェストのテストの両方で、authenticated?が古いままになっており、引数も2つではなくまだ1つのままだからです。これを解消するため、両者を更新して、新しい一般的なメソッドを使うようにします。

 current_user内の抽象化したauthenticated?メソッド red
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(:remember, cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end
Userテスト内の抽象化したauthenticated?メソッド green
test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?(:remember, '')
  end
end

上のような変更を加えると、テストは greenに変わります。

rails test

演習

1:コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか?
また、各トークンに対応するダイジェストの値はどうなっているでしょうか?

created_at: nil, updated_at: nil, password_digest:
"$2a$10$D80rMhemJkoM8JmOU5IGN8YwsOHhBb...",
remember_digest: nil, admin: false, activation_digest: nil,
activated: false, activated_at: nil

2:抽象化したauthenticated?メソッドを使って、先ほどの
各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。

動作確認するだけ

11.3.2 editアクションで有効化

editアクションを書く準備ができました。
このアクションは、paramsハッシュで
渡されたメールアドレスに対応するユーザーを認証します。
ユーザーが有効であることを確認する中核は、次の部分になります。

if user && !user.activated? && user.authenticated(:activation,params[:id])

既に有効になっているユーザーを誤って再度有効化しないために必要です

上の論理値に基いてユーザーを認証するには、ユーザーを認証してからactivated_atタイムスタンプを更新する必要があります。

user.update_attribute(:activated, true)
user.update_attribute(:activated_at, Time.zone.now)

アカウントを有効化するeditアクション
app/controllers/account_activations_controller.rb

class AccountActivationsController < ApplicationController

def edit
  user = User.find_by(email: params[:email])
  if user && !user.activated? && user.authenticated?(:activation, params[:id])
   user.update_attribute(:activated, true)
   user.update_attribute(:activated_at, Time.zone.now)
   log_in user
   flash[:success] = "Account activated!"
   redirect_to user
  else
   flash[:danger] = "Invalid activation link"
   redirect_to root_url
  end
 end
end

user.activated?がtrueの場合にのみログインを許可し、そうでない場合はルートURLにリダイレクトしてwarningで警告を表示します。

有効でないユーザーがログインすることのないようにする
app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

def new
end

def create
 user = User.find_by(email: params[:session][:email].downcase)
 if user && user.authenticate(params[:session][:password])
  if user.activated?
    log_in user
    params[:session][:remember_me] == '1' ? remember(user) : forget(user)
    redirect_back_or user
  else
    message = "Account not activated. " 
    message += "Check your email for the activation link."
    flash[:warning] = message
    redirect_to root_url
  end
else

def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

演習

1:コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?

Hi testerer,

Welcome to the Sample App! Click on the link below to activate your account:

https://ap-northeast-1.amazonaws.com/account_activations/O0VRZve0xN9l27LNySAnVQ/edit?email=exxample%40gmal.com

----==_mimepart_5e8d6fcbafbc4_11b625d352085248
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Email styles need to be inline */
</style>
</head>

<body>
<h1>Sample App</h1>

<p>Hi tt,</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

<a href="https://ap-northeast-1.amazonaws.com/account_activations/xliAdIUvihhp2b1S3vD2CA/edit?email=rena%40gma.com">Activate</a>

</body>
</html>

O0VRZve0xN9l27LNySAnVQが有効化トークン

2:先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。
また、有効化ステータスがtrueになっていることを確認してみてください。

rails c で確認するだけ

11.3.3 有効化のテストとリファクタリング

ユーザー登録のテストにアカウント有効化を追加する green
test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  def setup
    ActionMailer::Base.deliveries.clear
  end

  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'
  end

  test "valid signup information with account activation" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    assert_equal 1, ActionMailer::Base.deliveries.size
    user = assigns(:user)
    assert_not user.activated?
    # 有効化していない状態でログインしてみる
    log_in_as(user)
    assert_not is_logged_in?
    # 有効化トークンが不正な場合
    get edit_account_activation_path("invalid token", email: user.email)
    assert_not is_logged_in?
    # トークンは正しいがメールアドレスが無効な場合
    get edit_account_activation_path(user.activation_token, email: 'wrong')
    assert_not is_logged_in?
    # 有効化トークンが正しい場合
    get edit_account_activation_path(user.activation_token, email: user.email)
    assert user.reload.activated?
    follow_redirect!
    assert_template 'users/show'
    assert is_logged_in?
  end
end

本当に重要な部分は次の1行です。

assert_equal 1, ActionMailer::Base.deliveries.size

上のコードは、配信されたメッセージがきっかり1つであるかどうかを確認します。

assignsメソッドを使うと対応するアクション内のインスタンス変数にアクセスできるようになります。
例えば、Usersコントローラのcreateアクションでは@user
というインスタンス変数が定義されていますが、
テストでassigns(:user)と
書くとこのインスタンス変数にアクセスできるようになる、といった具合です。

green
rails test
activateメソッドを作成してユーザーの有効化属性を更新し、send_activation_emailメソッドを作成して有効化メールを送信します。
Userモデルにユーザー有効化メソッドを追加するapp/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # アカウントを有効にする
  def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
  end

  # 有効化用のメールを送信する
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

  private
    .
    .
    .
end
ユーザーモデルオブジェクトからメールを送信するapp/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      @user.send_activation_email
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end
  .
  .
  .
end
ユーザーモデルオブジェクト経由でアカウントを有効化するapp/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end
green
rails test

演習

1:activateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。
テンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。

user.rb

class User < ApplicationRecord
(中略)
  # アカウントを有効にする
  def activate
    update_columns(activated: true, activated_at: Time.zone.now)
  end
(後略)

2:現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、テンプレートを使って、この動作を変更してみましょう 

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
  
  def index
    @users = User.where(activated: true).paginate(page: params[:page])
  end

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless @user.activated?
  end

(後略)

3:ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。

update_columnsメソッドは、コールバックとバリデーションを実行せずにスキップしますので、コールバックやバリデーションをかける必要がある場合は注意が必要です。

update_columnsを使用するテンプレート
app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest
  .
  .
  .
  # アカウントを有効にする
  def activate
    update_columns(activated: FILL_IN, activated_at: FILL_IN)
  end

  # 有効化用のメールを送信する
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

  private

    # メールアドレスをすべて小文字にする
    def downcase_email
      self.email = email.downcase
    end

    # 有効化トークンとダイジェストを作成および代入する
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end
有効なユーザーだけを表示するコードのテンプレートapp/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def index
    @users = User.where(activated: FILL_IN).paginate(page: params[:page])
  end

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless FILL_IN
  end
  .
  .
  .
end

11.4 本番環境でのメール送信

本番環境からメール送信するために、「Mailgun」というHerokuアドオンを利用してアカウントを検証します (このアドオンを利用するためにはHerokuアカウントにクレジットカードを設定する必要がありますが、アカウント検証では料金は発生しません)。

Railsのproduction環境でMailgunを使う設定config/environments/production.rb
Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '<あなたのHerokuサブドメイン名>.herokuapp.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :port           => ENV['MAILGUN_SMTP_PORT'],
    :address        => ENV['MAILGUN_SMTP_SERVER'],
    :user_name      => ENV['MAILGUN_SMTP_LOGIN'],
    :password       => ENV['MAILGUN_SMTP_PASSWORD'],
    :domain         => host,
    :authentication => :plain,
  }
  .
  .
  .
end
rails test
git add -A
git commit -m "Add account activation"
git checkout master
git merge account-activation

続いてリモートリポジトリにプッシュし、Herokuにデプロイします。

rails test
git push
git push heroku
heroku run rails db:migrate

MailgunのHerokuアドオンを追加するために、次のコマンドを実行します。

heroku addons:create mailgun:starter

: herokuコマンドのバージョンが古いとここで失敗するかもしれません。その場合は、Heroku Toolbeltを使って最新版に更新するか、次の古い文法のコマンドを試してみてください。

heroku addons:add mailgun:starter

Herokuの環境変数を表示したい場合は、次のコマンドを実行します。

heroku config:get MAILGUN_SMTP_LOGIN heroku config:get MAILGUN_SMTP_PASSWORD
#受信メールの認証を行います
heroku addons:open mailgun

演習

1:実際に本番環境でユーザー登録をしてみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?

動作確認する。

メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。
また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。
ターミナルからheroku logsコマンドを実行してみましょう。

<a href="https://&lt;your heroku app&gt;.herokuapp.com/account_activations/uiXkg8zRNKlluX3pX7-vwg/edit?email=mochikichi%40live.jp">Activate</a>

11章のまとめ

1:アカウント有効化は Active Recordオブジェクトではないが、セッションの場合と同様に、リソースでモデル化できる

2:Railsは、メール送信で扱うAction Mailerのアクションとビューを生成することができる

3:Action MailerではテキストメールとHTMLメールの両方を利用できる

4:メイラーアクションで定義したインスタンス変数は、他のアクションやビューと同様、メイラーのビューから参照できる

5:アカウントを有効化させるために、生成したトークンを使って一意のURLを作る

6:より安全なアカウント有効化のために、ハッシュ化したトークン (ダイジェスト) を使う

7:メイラーのテストと統合テストは、どちらもUserメイラーの振舞いを確認するのに有用

8:SendGridを使うと、production環境からメールを送信できる

その12に続く

Rails-tutorialのまとめ11.2(アカウント有効化のメール送信 主に演習)

その11から続く

11.2 アカウント有効化のメール送信
11.2.1 送信メールのテンプレート

メイラーは、モデルやコントローラと同様にrails generateで生成できます。

rails generate mailer UserMailer account_activation password_reset

今回必要となるaccount_activationメソッドと、
第12章で必要となるpassword_resetメソッドが生成されました。

生成したメイラーごとに、ビューのテンプレートが2つずつ生成されます。
1つはテキストメール用のテンプレート、
1つはHTMLメール用のテンプレートです。

アカウント有効化メイラーのテキストビュー (自動生成)
app/views/user_mailer/account_activation.text.erb

UserMailer#account_activation
<%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb

アカウント有効化メイラーのHTMLビュー (自動生成)
app/views/user_mailer/account_activation.html.erb

<h1>UserMailer#account_activation</h1>
  <p>
    <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb
  </p>

生成されるHTMLメイラーのレイアウトやテキストメイラーのレイアウトはapp/views/layoutsで確認できます。
生成されたコードにはインスタンス変数@greetingも含まれています。
続きを読む

Rails tutorialのまとめ(第11章アカウントの有効化 主に演習)

その10から続く

第11章アカウントの有効化

アカウントを有効化する段取りは、ユーザーログイン、
特にユーザーの記憶と似ています。基本的な手順は次のようになります。

1:ユーザーの初期状態は「有効化されていない」(unactivated) にしておく。

2:ユーザー登録が行われたときに、有効化トークンと、
それに対応する有効化ダイジェストを生成する。

3:有効化ダイジェストはデータベースに保存しておき、有効化トークンは
メールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく

4:ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレス
をキーにしてユーザーを探し、データベース内に保存しておいた
有効化ダイジェストと比較することでトークンを認証する。

5:ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から
「有効化済み」(activated) に変更する。

今回実装するアカウント有効化やパスワード再設定の仕組みと、
以前に実装したパスワードや記憶トークンの仕組みにはよく似た点が多いので、
多くのアイデアを使い回すことができます。
(具体的にはUser.digestやUser.new_token、改造版のuser.authenticated?メソッドなど)。
それぞれの仕組みの似ている点をまとめてみました

検索キー  string              digest         authentication
email   password         password_digest   authenticate(password)
id      remember_token   remember_digest   authenticated?(:remember, token)
email   activation_token activation_digest authenticated?(:activation, token)
email   reset_token      reset_digest      authenticated?(:reset, token)

11.1 AccountActivationsリソース

ユーザーからのGETリクエストを受けるために、(本来であればupdateのところを)editアクションに変更して使っていきます。

11.1.1 AccountActivationsコントローラ

AccountActivationsリソースを作るために、
まずはAccountActivationsコントローラを生成してみましょう。

rails generate controller AccountActivations

有効化のメールには次のURLを含めることになります。

edit_account_activation_url(activation_token, ...)

editアクションへの名前付きルートが必要になるということです。そこでまずは、名前付きルートを扱えるようにするため、ルーティングにアカウント有効化用のresources行を追加します。

アカウント有効化に使うリソース (editアクション) を追加する
config/routes.rb

Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
end
HTTPリクエスト Action      名前付きルート                 URL 
GET           edit    edit_account_activation_url(token) /account_activation/<token>/edit

続きを読む

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に続く

Rails-tutorialのまとめ10.3(すべてのユーザーを表示する)

その10.2から続きます

10.3 すべてのユーザーを表示する

indexアクションを追加しましょう。このアクションは、すべてのユーザーを一覧表示します。
その際、データベースにサンプルデータを追加する方法や、
将来ユーザー数が膨大になってもindexページを問題なく表示できるようにするためのユーザー出力のページネーション (pagination=ページ分割) の方法を学びます。

10.3.1 ユーザーの一覧ページ

ユーザーの一覧ページを実装するために、まずはセキュリティモデルについて考えてみましょう。
ユーザーのshowページについては、今後も(ログインしているか
どうかに関わらず) サイトを訪れたすべてのユーザーから見えるようにしておきますが
ユーザーのindexページはログインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限します。

indexアクションのリダイレクトをテストする red
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 index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
  .
  .
  .
end

beforeフィルターのlogged_in_userにindexアクションを追加して、
このアクションを保護します

indexアクションにはログインを要求する green
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  before_action :correct_user,   only: [:edit, :update]

  def index
  end

  def show
    @user = User.find(params[:id])
  end
  .
  .
  .
end

今度はすべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装します。
User.allを使ってデータベース上の全ユーザーを取得し、
ビューで使えるインスタンス変数@usersに代入させます。

ユーザーのindexアクション

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

実際のindexページを作成するには、ユーザーを列挙してユーザーごとにliタグで囲むビューを作成する必要があります。

ここではeachメソッドを使って作成します。
それぞれの行をリストタグulで囲いながら、
各ユーザーのGravatarと名前を表示します。

続きを読む

Rails-tutorialのまとめ10.2(第10章 ユーザーの更新の続き 主に演習)

その10から続く

10.2.1 ユーザーにログインを要求する

ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御してみましょう。
(こういったセキュリティ上の制御機構をセキュリティモデルと呼びます)。

許可されていないページに対してアクセスするログイン済みのユーザーがいたら(例えば他人のユーザー編集ページにアクセスしようとしたら)、
ルートURLにリダイレクトさせるようにします。

転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルター
を使います。beforeフィルターは、before_actionメソッドを使って何らかの処理
が実行される直前に特定のメソッドを実行する仕組みです。

今回はユーザーにログインを要求するために、logged_in_userメソッドを
定義してbefore_action :logged_in_userという形式で使います

beforeフィルターにlogged_in_userを追加する red
app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション
    # ログイン済みユーザーかどうか確認
    def logged_in_user
unless logged_in?flash[:danger] = "Please log in."redirect_to login_urlend    end
end
デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに
適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、
:editと:updateアクションだけにこのフィルタが適用されるように制限をかけています
今の段階ではテストは redになります。
rails test
テストユーザーでログインする green
test/integration/users_edit_test.rb

続きを読む

Rails-tutorialのまとめ(第10章 ユーザーの更新 主に演習)

その9から続く

10.1.1 編集フォーム

ユーザーのeditアクションapp/controllers/users_controller.rb
class UsersController < ApplicationController
 #追加
  def edit
   @user = User.find(params[:id])  end    
  end

beforeフィルター (before filter) を使ってこのアクセス制御を実現できます。

1: ユーザーのeditビュー
app/views/users/edit.html.erb

<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
<div class="col-md-6 col-md-offset-3">
  <%= form_for(@user) do |f| %>
    <%= render 'shared/error_messages' %>
    <%= f.label :name %>
    <%= f.text_field :name, class: 'form-control' %>
    <%= f.label :email %>
    <%= f.email_field :email, class: 'form-control' %>
    <%= f.label :password %>
    <%= f.password_field :password, class: 'form-control' %>
    <%= f.label :password_confirmation, "Confirmation" %>
    <%= f.password_field :password_confirmation, class: 'form-control' %>
    <%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
  <%= gravatar_for @user %>
   <a href="http://gravatar.com/emails" target="_blank">change</a>
   </div>
  </div>
</div>

gravatarへのリンクでtarget=”_blank”が使われていますが、
これを使うとリンク先を新しいタブ(またはウィンドウ)で開くようになるので、
別のWebサイトへリンクするときなどに便利です。

Railsはどうやって新規のPostと既存のPatchを区別している?
Railsは、form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOSTを、falseのときにはPATCHを使います。

レイアウトの “Settings” リンクを更新するapp/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", '#' %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
               <li>
                <%= link_to "Settings", edit_user_path(current_user) %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

演習

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

rel= “noopener”を追加するだけ

続きを読む

Rails-tutorial自分用まとめ(第9章 発展的なログイン機構 主に演習)

その8から続く

9.1 Remember me 機能

ユーザーのログイン状態をブラウザを閉じた後でも有効にする [remember me] 機能永続クッキー (permanent cookies) を使ってこの機能を実現していきます

9.1.1 記憶トークンと暗号化

1:記憶トークンにはランダムな文字列を生成して用いる。
2:ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。
3:トークンはハッシュ値に変換してからデータベースに保存する。
4:ブラウザのcookiesに保存するユーザーIDは暗号化しておく。
5:永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、
記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。
最初に、必要となるremember_digest属性をUserモデルに追加

rails generate migration add_remember_digest_to_users remember_digest:string
rails db:migrate
トークンとしてRuby標準ライブラリのSecureRandomモジュールに
あるurlsafe_base64メソッドを使う
トークン生成用メソッドを追加するapp/models/user.rb
class User < ApplicationRecord

  # ランダムなトークンを返す。追加
  def User.new_token
SecureRandom.urlsafe_base64
  end
end

実装計画としては、user.rememberメソッドを作成することで、
このメソッドは記憶トークンをユーザーと関連付け、トークンに対応する記憶
ダイジェストをデータベースに保存します。

Userモデルには既にremember_digest属性が追加されていますが
remember_token属性はまだ追加されていません。
このためuser.remember_tokenメソッドを使ってトークンにアクセスできるようにし、トークンをデータベースに保存せずに実装する必要があリます。

仮想のpassword属性はhas_secure_passwordメソッドで自動的に作成されましたが、今回はremember_tokenのコードを自分で書く必要があります。
attr_accessorを使って「仮想の」属性を作成します。

class User < ApplicationRecord
 attr_accessor :remember_token
 .
 .
 .
 def remember
   self.remember_token = ...update_attribute(:remember_digest, ...)
 end
end

selfキーワードを与えると、この代入によってユーザーのremember_token属性が期待どおりに設定されます。

rememberメソッドの2行目では、update_attributeメソッドを使って記憶ダイジェストを更新しています。

ユーザーを記憶するには、記憶トークンを作成して、そのトークンをダイジェストに変換したものをデータベースに保存します。

新しいトークンを作成するためのnew_tokenメソッドを作成

rememberメソッドをUserモデルに追加する green

app/models/user.rb
class User < ApplicationRecord
  
  attr_accessor :remember_token  中略 ↑追加↓
  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_tokenupdate_attribute(:remember_digest, User.digest(remember_token))  end
  end

続きを読む

Rails-tutorial自分用まとめ(第8章 ログイン機構 主に演習とその回答)

その7から続く

8.1 セッション

HTTPはステートレス (Stateless) なプロトコルです。文字通り「状態 (state)」が「ない (less)」ので、HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われます。

ユーザーログインの必要なWebアプリケーションでは、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定します。

Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。

8.1.1 Sessionsコントローラ

Sessionsコントローラを生成する

rails generate controller Sessions new
リソースを追加して標準的なRESTfulアクションをgetできるようにする red config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
end
#セッションルールによって提供されるルーティング
HTTPリクエスト URL     名前付きルート アクション名      用途
GET          /login  login_path    new       新しいセッションのページ (ログイン)
POST         /login  login_path    create    新しいセッションの作成 (ログイン)
DELETE       /logout logout_path   destroy   セッションの削除 (ログアウト)

演習

1:GET login_pathとPOST login_pathとの違いを説明できますか?
少し考えてみましょう。

get    '/login',   to: 'sessions#new'
post   '/login',   to: 'sessions#create'

GET login_path: “/login”へアクセスされた際に”sessions#new”アクションを実行

POST login_path: “sessions#create”アクションの情報を”/login”へ送信。

Getは/loginのリクエストが来た際にsessionsコントローラのnewアクションをするという意味でPostはsessionsコントローラのcreateアクションを/loginに送信する

2:ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。
同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。
現在、いくつのSessionsリソースがあるでしょうか?

rails routes | grep users#
rails routes | grep sessions# 3つ

form_for(@user)

Railsでは上のように書くだけで、
「フォームのactionは/usersというURLへのPOSTである」と自動的に判定しますが、
セッションの場合はリソースの名前とそれに対応するURLを具体的に指定する必要があります。

form_for(:session, url: login_path)

8.1.2 ログインフォーム

演習

リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか?
ヒント:表 8.1とリスト 8.5の1行目に注目してください。

action=”/login” method=”post”から/loginにPostする場合
sessionsコントローラのcreateアクションが実行されるため

8.1.3 ユーザーの検索と認証

 ユーザーをデータベースから見つけて検証するapp/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new'
    end
  end

  def destroy
  end
end

User                   Password                                                a && b
存在しない        何でもよい (nil && [オブジェクト]) == false
有効なユーザー 誤ったパスワード (true && false) == false
有効なユーザー 正しいパスワード (true && true) == true

演習

続きを読む

Rails-tutorial自分用まとめ(第7章ユーザー登録 主に演習とその回答)

その6から続きます

7.1.1 デバッグとRails環境

サイトのレイアウトにデバッグ情報を追加するapp/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  .
  .
  .
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
  </body>
 デバッグ表示を整形するための追加と、Sassのミックスイン.
app/assets/stylesheets/custom.scss
@import "bootstrap-sprockets";
@import "bootstrap";

/* mixins, variables, etc. */

$gray-medium-light: #eaeaea;

@mixin box_sizing {-moz-box-sizing:    border-box;-webkit-box-sizing: border-box;box-sizing:         border-box;}.
.
.
/* miscellaneous */
.debug_dump {clear: both;float: left;width: 100%;margin-top: 45px; -moz-box-sizing:    border-box;
  -webkit-box-sizing: border-box;
  box-sizing:         border-box;
}

演習

1:ブラウザから /about にアクセスし、デバッグ情報が表示されていることを確認してください。

このページを表示するとき、どのコントローラとアクションが使われ
ていたでしょうか? paramsの内容から確認してみましょう。

controller: static_pages
action: about

2:Railsコンソールを開き、データベースから最初のユーザー情報を取得し、変数userに格納してください。

その後、puts user.attributes.to_yamlを実行すると何が表示されますか?
ここで表示された結果と、yメソッドを使ったy user.attributesの実行結果を比較してみましょう。

user = User.first

表示は同じ

HTTP   リクエストURL  アクション 名前付きルート        用途
GET    /users       index   users_path     すべてのユーザーを一覧するページ
GET    /users/1     show    user_path(user) 特定のユーザーを表示するページ
GET    /users/new   new     new_user_path  ユーザーを新規作成するページ (ユーザー登録)
POST   /users       create  users_path     ユーザーを作成するアクション
GET    /users/1/    edit    edit edit_user_path(user) id=1のユーザーを編集するページ
PATCH  /users/1     update  user_path(user) ユーザーを更新するアクション
DELETE /users/1     destroy user_path(user) ユーザーを削除するアクション
Usersリソースが提供するRESTfulなルート

演習

埋め込みRubyを使って、マジックカラム (created_atとupdated_at)
の値をshowページに表示してみましょう埋め込みRubyを使って、
Time.nowの結果をshowページに表示してみましょう。
ページを更新すると、その結果はどう変わっていますか? 確認してみてください。

<%= @user.name %>, <%= @user.email %>,
<%= @user.updated_at %>, <%= @user.created_at %>,
<%= Time.now %>

続きを読む