Skip to content

Tutorial (japanese)

hakozaru edited this page Sep 30, 2017 · 4 revisions

本チュートリアルはBankenを利用した権限制御の実装例を紹介していきます。

要件

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  def index
    @posts = Post.all
  end

  def show
  end

  def new
    @post = Post.new
  end

  def edit
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to @post, notice: 'Post was successfully created.'
    else
      render :new
    end
  end

  def update
    if @post.update(post_params)
      redirect_to @post, notice: 'Post was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @post.destroy
    redirect_to posts_url, notice: 'Post was successfully destroyed.'
  end

  private
    def set_post
      @post = Post.find(params[:id])
    end

    def post_params
      params.require(:post).permit(:title, :body, :published_at)
    end
end

今回は上記のposts_controller.rbに対し、以下の要件に満たす記事の更新処理(update)を実装していきます。

  • 未公開記事の場合は誰でも更新(update)できる。
  • 公開記事は管理者しか更新(update)できない。

権限の設計

Bankenには、権限制御を行う上での制約や規約が用意されているわけではありません。権限情報の持たせ方や権限判定のロジックは利用者の設計に委ねられます。今回は管理者か一般ユーザかの2種類を区別できればよいのでusersテーブルにadminカラムを追加し、管理者の場合はtrue、一般ユーザの場合はfalseを設定することにします。

> User.find(1).admin?
true
> User.find(2).admin?
false

本チュートリアルでは紹介しませんが、権限が3種類以上存在する場合はRails4.1の新機能であるEnumを使用したり、1ユーザに複数種類の権限を持たせたい場合はrolesテーブルを作成したりなど要件によって権限の設計は様々だと思います。何れの設計であってもBankenは柔軟かつ忠実に権限制御を行うことが可能です。

Bankenを飼う

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Banken
  protect_from_forgery
end
> rails g banken:install
      create  app/loyalties/application_loyalty.rb

準備はとても簡単で2ステップです。

  1. ApplicationControllerBankenモジュールをincludeする
  2. rails g banken:installapp/loyalties/application_loyalty.rbを作成する
#app/loyalties/application_loyalty.rb
class ApplicationLoyalty
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end
end

これであなたのアプリでBankenを飼う準備が整いました。

Loyaltyクラスの作成

Bankenではcontrollerに一対一で紐づく同名のloyaltyクラスを作成していきます。 今回はPostsControllerに対して制御を行うのでPostsLoyaltyクラスを作成する必要があります。

> rails g banken:loyalty posts
      create  app/loyalties/posts_loyalty.rb

PostsLoyaltyクラスを作成するにはBankenが用意しているgenerateタスクを実行するのが手軽で良いでしょう。

#app/loyalties/posts_loyalty.rb
class PostsLoyalty < ApplicationLoyalty
end

さきほど作成されたApplicationLoyaltyクラスを継承するPostsLoyaltyクラスがapp/loyalties/配下に作成されました。

アクションの制御

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  
  # 他の処理は省略
  
  def update
    authorize! @post

    if @post.update(post_params)
      redirect_to @post, notice: 'Post was successfully updated.'
    else
      render :edit
    end
  end
  
  # 他の処理は省略
end

更新処理の実行前にBankenが提供するauthorize!メソッドを呼ぶことで以降の処理が実行可能かどうかを判定することができます。authorize!は以下の処理を実行します。

  1. PostsControllerと同名のPostsLoyaltyクラスのインスタンスを作成する
  2. PostsLoyaltyクラスのインスタンスのupdate?メソッドを実行する

上記のauthorize!の処理はちょうど以下と同じような事を行っていると考えると分かりやすいかもしれません。

  def update
    raise "not authorized" unless PostsLoyalty.new(current_user, @post).update?

    if @post.update(post_params)
      redirect_to @post, notice: 'Post was successfully updated.'
    else
      render :edit
    end
  end

注目する点はPostsLoyaltyクラスのインスタンスを作成する際に第一引数としてcurrent_userが実行される事です。なのでApplicationControllerなどにcurrent_userを定義しておく必要があります。また第二引数の@postauthorize!の第一引数として渡したオブジェクトです。authorize!の第一引数は任意なので必要がなければ渡す必要はありません。

Loyaltyクラスの実装

では次にPostsLoyaltyクラスのインスタンスメソッドであるupdate?メソッドを実装していきましょう。

# app/loyalties/posts_loyalty.rb
class PostsLoyalty < ApplicationLoyalty
  def update?
    user.admin? || record.unpublished?
  end
end

usercurrent_userrecordauthorize!の第一引数として渡したオブジェクトの@postです。これで、未公開記事の場合と現在のユーザが管理者の場合はupdate?trueになるように実装できました。update?falseを返す場合はBankenは例外(Banken::NotAuthorizedError)をおこします。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  rescue_from Banken::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized(exception)
    loyalty_name = exception.loyalty.class.to_s.underscore

    flash[:error] = t "#{loyalty_name}.#{exception.query}", scope: "banken", default: :default
    redirect_to(request.referrer || root_path)
  end
end
ja:
  banken:
    default: 'You cannot perform this action.'
    posts_loyalty:
      update?: 'You cannot edit this post!'
      create?: 'You cannot create posts!'

Banken::NotAuthorizedErrorは例外が発生したLoyaltyのインスタンスをloyalty(PostsLoyaltyのインスタンス)、例外を起こしたメソッド名をquery(update?), 例外を起こしたコントローラ名をcontroller(posts_controller)で提供するので、必要があればこのようにI18nを使って任意のエラーメッセージを作成し指定されたページにリダイレクトさせる事が可能です。(デフォルトのエラーメッセージはmessageで取得可能)

Viewの制御

updateが実行できる条件をPostsLoyaltyクラスのインスタンスメソッドのupdate?に実装することができました。Viewでもupdateが実行できる場合にだけ記事の編集リンクを表示させるのが良いでしょう。

<% if loyalty(@post, :posts).update? %>
  <%= link_to "Edit post", edit_post_path(@post) %>
<% end %>

Bankenはloyaltyというhelperを用意しておりViewからでも第二引数と同名のloyaltyクラスのインスタンスを作成する事ができます。これはちょうど以下と同じような事を行っていると考えると分かりやすいかもしれません。

<% if PostsLoyalty.new(current_user, @post).update? %>
  <%= link_to "Edit post", edit_post_path(@post) %>
<% end %>

これでBankenを使った権限制御が実装できました。

さいごに

Bankenはとても小さなライブラリですがControllerとViewでの権限判定をLoyalty層に分離できFat Controller対策としても有用です。またすべてのLoyaltyクラスは単なるrubyのクラスであることを思い出してください。よって一部の共通処理をモジュールに分離したり、継承を増やすことでDRYにすることも簡単ですし、ライブラリ特有の特殊なDSLを覚える必要もありません。同様にBanken内部の実装も非常にシンプルでRailsの拡張はしていないのでRailsのバージョンアップがあってもBankenは今まで通り元気に尻尾を振って動いてくれるでしょう。