19. ページ権限の作成

シナリオの記述

ページの権限管理ができていないので、この部分を実装する。以前は、ページごとに before_action で制御していたが、view の定義と一致しない問題が見受けられた。新システムでは権限を管理するメソッドを定義することにする。

まず、定義の前にシナリオを作成する。ここでは、一般ユーザが教務室のトップページを表示できないことを確認する。現在は、権限管理ができていないので、このテストはエラーとなる。

# language: ja
機能: 権限の確認
  シナリオ: 一般ユーザでログインして、教務室のページが見られないことを確認する
    前提 教員でログインする 
    もし "teacher/kyoumus"にアクセスする
    ならば 教務室トップページと表示されていないこと

コントローラにおけるユーザ権限の実装

権限の確認は、各 controller に privilege_check を before_action することで行う。各 controller には Privileges 定数を設定し、メソッドごとの権限を記述する。app/controllers/teacher/kyoumus_controller.rb は以下のようにした。Privileges 定数はアクションをキーとして、権限一覧を配列として保持する。

  before_action :privilege_check
  Privileges = { index: %w( is_kyoumu? ) }

privilege_check メソッドは app/controllers/application_controller.rb に記述する。権限の確認自体は、user または admin の has_privilegges? に丸投げする。

                                                          
  # @note ページの権限を確認して、権限がない場合にはリダイレクト
  # @return [Boolean] 権限がある場合には true を返す      
  def privilege_check                 
    privileges = self.class::Privileges[self.action_name.to_sym]
    redirect_to_root_path_with_message(current_user_or_admin.has_privileges?(each_session, privileges))
  end

  # @return [User,Admin] 現在のユーザまたは管理者を得る
  def current_user_or_admin
    current_user || current_admin
  end

モデルにおけるユーザ権限のテスト

User および Admin モデルに has_privileges? メソッドがないので記述する。モデルなので、実装の前に先に spec を記述する。これからテストが増えるので、ユーザごとの複数のテストを it 一つにまとめた(ユーザごとに共通の it を繰り返す)。

context "Each user of users" do
    user_keys = %i( user_hkob user_skyoumu )

    context "checking the privileges" do
      answers = {
        user_hkob: [ nil ],
        user_skyoumu: [ true ],
      }
      privileges = [
        %w( is_kyoumu? ),
        %w( is_kyoumu? is_kyougaku? ),
      ]
      has_privileges_answers = {
        user_hkob: [ nil, nil ],
        user_skyoumu: [ true, true ],
      }
      user_keys.each do |user_key|
        it "#{user_key} should have correct privileges" do
          user = FactoryGirl.build(user_key)           
          session = {}                                 
          user.set_privileges(session)                 
          expect(session.values_at(*User::Privileges)).to eq(answers[user_key])
          privileges.each_with_index do |privilege, i|
            ans = has_privileges_answers[user_key][i]
            expect(user.has_privileges?(session, privilege)).to eq(ans)
          end
        end
      end
    end
  end

モデルにおけるユーザ権限の実装(共通モジュール)

has_privileges? は user および admin の両方で実装するが、内容がほぼ同一なので module として、 app/models/concerns/privilege.rb に記載する。

module Privilege
  # @param [Array<Symbol>] session 確認するセッション
  # @param [Array<Symbol>] privileges 許容する権限のシンボル配列
  # @return [Boolean] 権限があれば true
  def has_privileges?(session, privileges)
    session.values_at(*privileges).compact.first
  end
end

この module を読み込むために、app/models/user.rb に include を記述する。

class User < ActiveRecord::Base
  (中略)
  include Privilege

同様に app/models/admin.rb にも追加する。

class Admin < ActiveRecord::Base
  (中略)
  include Privilege

コントローラのテスト失敗 → ヘルパーメソッドの修正

User および Admin に has_privileges? が生成されたが、welcome#index を経由しない Devise のテストでは session に権限が設定されないために正しくテストが通過しないことがわかった。そこで、spec/rails_spec.rb の login_user_as メソッドで controller.user_session に権限を設定するように記載した。

def login_user_as(fg_key, controller)
  user = FactoryGirl.create(fg_key)
  sign_in :user, user
  user.set_privileges(controller.user_session)
  user
end

spec/controllers/welcome_controller_spec.rb および spec/controllers/teacher/kyoumus_controller.rb の呼び出し側も controller を渡すように修正する。

        login_user_as :user_skyoumu, controller

テスト通過 → さらなるテストの追加

これでテストがすべて通過した。以下のようにシナリオを増やして各ページに権限設定を行う。

  シナリオ: 一般ユーザでログインして、年度変更のページが見られないことを確認する
    前提 教員でログインする
    もし "teacher/years"にアクセスする
    ならば 年度一覧と表示されていないこと

取り急ぎ、app/controllers/teacher/years_controller.rb に権限設定を追加した。

class Teacher::YearsController < ApplicationController
  before_action :privilege_check
  Privileges = { index: %w( is_kyoumu? ) }

テストの漏れの発見 → シナリオの追加

この結果、受入テストは通過したが、spec/teacher/years_controller_spec.rb が以下のようなエラーで動かなくなった。これは、spec でログイン処理の記述が抜けているためである。すなわちログインしていない場合に、このページを表示すると user が nil のためにエラーとなることがわかる。

  1) Teacher::YearsController GET index returns http success
     Failure/Error: get :index
     NoMethodError:
       undefined method `has_privileges?' for nil:NilClass

受入テストが足りていないことがわかったので、先にログインしていない場合のシナリオを追加する。

  シナリオ: ログインしない状態で、教務室のページが見られないことを確認する
    もし "teacher/kyoumus"にアクセスする
    ならば ログインと表示されていること

コントローラの修正

さきほどの spec の記述ミスと同じエラーがでることを確認できた。そこで、app/controllers/applicaton_controller.rb の privilege_check を修正する。

  def privilege_check
    if ua = current_user_or_admin
      privileges = self.class::Privileges[self.action_name.to_sym]
      redirect_to_root_path_with_message(ua.has_privileges?(each_session, privileges))
    else
      flash[:alert] = 'ログインしないとこのページは表示できません'
      redirect_to new_user_session_path
    end 

最後に、spec/controllers/teacher/years_controller.rb に login_user_as を追加する。これですべてのテストが通過した。

  describe "GET index" do
    it "returns http success" do
      login_user_as :user_skyoumu, controller
      get :index
      expect(response).to have_http_status(:success)
    end
  end

今日はここまで。