-
Notifications
You must be signed in to change notification settings - Fork 15
Tutorial (japanese)
本チュートリアルは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は柔軟かつ忠実に権限制御を行うことが可能です。
# 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ステップです。
-
ApplicationController
にBanken
モジュールをinclude
する -
rails g banken:install
でapp/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を飼う準備が整いました。
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!
は以下の処理を実行します。
-
PostsController
と同名のPostsLoyalty
クラスのインスタンスを作成する -
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
を定義しておく必要があります。また第二引数の@post
はauthorize!
の第一引数として渡したオブジェクトです。authorize!
の第一引数は任意なので必要がなければ渡す必要はありません。
では次にPostsLoyalty
クラスのインスタンスメソッドであるupdate?
メソッドを実装していきましょう。
# app/loyalties/posts_loyalty.rb
class PostsLoyalty < ApplicationLoyalty
def update?
user.admin? || record.unpublished?
end
end
user
はcurrent_user
でrecord
はauthorize!
の第一引数として渡したオブジェクトの@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
で取得可能)
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は今まで通り元気に尻尾を振って動いてくれるでしょう。