背景
ActiveAdminを、運用上のマスター管理だけではなく、編集者や寄稿者など権限設定して色々できるようにしたい。
ActiveAdminにはデフォルトでCanCanCanのアダプターが搭載されていますが、ちょっと癖があるのでうまく使えるようになりたい。
やりたいこと
- CanCanCanで権限管理
- Indexページでは自分に割り当てられたレコードのみ一覧可能
- Showと更新系ページも自分に割り当てられたもののみアクセス可能
- 他人のページは絶対に見れない
- ActiveAdmin外でアクセス制限したいときにも同じAbilityで対応したい
ユーザー権限は例えばこんな感じ
- AdminUser(管理者)は全てのテーブルをCRUD可能
- OwnerUser(オーナー)は自分の記事のみCRUD可能
- EditorUser(編集者)はオーナーにぶら下がり、オーナーの記事のみCRUD可能
- ProofreaderUser(校閲係)も同様だが、編集のみ可能(RU)
準備
ActiveAdminでCanCanCanを使う
# Admin Page
gem 'activeadmin', github: 'activeadmin'
# Role
gem 'cancancan', '~> 1.10'
config.authorization_adapter = ActiveAdmin::CanCanAdapter
config.on_unauthorized_access = :access_denied
class ApplicationController < ActionController::Base
# 権限がなくてアクセスできないページはダッシュボードへ飛ばしてメッセージを表示
# 認証が切れてアクセスできない場合はログイン画面を表示
def access_denied(exception)
if current_user
redirect_to admin_root_path, alert: I18n.t('active_admin.access_denied.message')
else
redirect_to new_user_session_path, alert: exception.message
end
end
# ログアウトしたらログイン画面へ戻る
def after_sign_out_path_for(resource)
new_user_session_path
end
end
class Ability
include CanCan::Ability
def initialize(user)
@user = user
if @user.present?
[:super_admin,:admin,:owner,:editor].each do |role|
# ユーザーに割り当てられたロール毎に権限を付与
# ※本当は複数ロールを割り当てられている状態も扱いたいが、今回は1ユーザー1ロールの前提
send(role.to_s) if user.has_role? role
end
# ログインしたユーザーは全てダッシュボードのアクセスが可能
can :read, ActiveAdmin::Page, :name => "Dashboard"
end
end
# 記事の校閲のみ行うアルバイト
def proofreader
end
# オーナーから記事作成を委託されたライター
def editor
end
# 記事の所有者
def owner
end
# 運用上の管理人
def admin
end
# システム的なマスターの管理やメンテナンスを行う
def super_admin
end
end
CanCanCan
CanCanCanの基礎
can/cannot
- canはANDでマージされる
- cannotは上書きされる ※順番が重要
Alias
- デフォルトのエイリアス
alias_action :index , :show, :to => :read
alias_action :create, :new , :to => :create
alias_action :update, :edit, :to => :update
alias_action :create, :read, :update, :destroy, :to => :manage
書式
# 基本は
# can/cannot action, model, condition, block
# actionとmodelは配列にもできる
can [:index,:show], [Article,User]
# conditionは絞り込み条件
can :read, Article, category: :news
# リレーションも指定できる
can :read, Article, owner: { id: 3 }
# Scopeでも指定できる
can :read, Article, Article.open
# blockで権限チェックをできる
can :show, Article do |subject, extra_args|
subject.assigned_to? @user
end
# conditionとblockは一緒に指定できるが、その場合はHashの形は使用できない
#can :show, Article, category: :new do |subject, extra_args|
# subject.assigned_to? @user
#end
# Scopeなら併用できる
# ※ただし、ActiveAdminとは相性が悪いので使えない
#can :show, Article, Article.open do |subject, extra_args|
# subject.assigned_to? @user
#end
【本題】ActiveAdmin+CanCanCan
Indexページは自動的に絞り込まれる
適切に権限設定していれば、indexに表示されるレコードは自動的に絞り込まれます。
例えば、オーナーでログインしたとき、オーナー自身の記事しかindexに表示させたくない場合は、次のようにします。
class Article < ActiveRecord::Base
belongs_to :owner_user, class_name: :User, foreign_key: :owner_user_id
end
...
def owner
can :read, Article, owner_user: @user
end
def admin
can :read, Article
end
このようになっていると、管理者でログインすると全てのレコードが表示され、オーナーでログインすると自分の記事だけに絞り込んで表示されます。
内部的には、Indexページを表示するときにこのようになっているみたいです。
def scope_collection(collection, action = ActiveAdmin::Auth::READ)
collection.accessible_by(cancan_ability, action)
end
accessible_by
にaction= :read
が渡ることで上の条件が追加されてcollection
から更に絞り込まれています。
更新系は工夫が必要
とりあえず、
上を踏まえてこうしてみました。
...
def owner
can :manage, Article, owner_user: @user
end
def admin
can :manage, Article
end
manage
は:read~:destroy
まで全て含んだAliasなのでこれで行ける!
かと思いきや、create
のときも絞り込みが入ってしまい、/new
ページが開けません。(でも「Articleを新規作成する」ボタンは表示されます)
ということでこうしてみる。
...
def owner
can :create, Article
can :manage, Article, owner_user: @user
end
def admin
can :manage, Article
end
いけます
だが待てよ。
上でさりげなくカッコ書きしましたが、
/new
ページが開けません。(でも「Articleを新規作成する」ボタンは表示されます)
これだと、proofreader
(校閲係)にこれをコピーするとき、「createの行だけ消せばいいじゃん」とはなりません。
/:id用のAlias
そこでこのように、/:id
なアクション用に新しくAliasを作ります。
...
def initialize(user)
alias_action :show, :update, :destroy, :to => :manage_own
...
そして、こうしてみます。
def owner
can :create , Article #新規作成
can :index , Article, owner_user: @user #一覧
can :manage_own, Article, owner_user: @user #/:id系
end
「アクションを実行する権限がありません」
Indexが見えなくなりました。
グローバルメニューからも消えます。
Showのパスを直叩きしても見えません。
なぜかというと、「Indexのページは:read
でチェックしている」みたいです。
(:read
には:index
も含まれてるような気がしますが、そう簡単なことではないらしい・・・。もしかして:read
は:index
と:show
をマージしたもの?)
その他諸々、Article自体にアクセスするかどうかのチェックは:read
で行ってるみたいです。
【完成】
ということで、
こうしてみます。
def owner
can :create , Article #新規作成
can :read , Article, owner_user: @user #閲覧系
can :manage_own, Article, owner_user: @user #更新系
end
バッチリ
proofreader
にはcreate
以外をコピーすればOKです。
ではActiveAdmin外でも、
class ArticlesController < ApplicationController
load_and_authorize_resource
def show
end
end
わーい
自分の記事も表示できるけど、他人の記事にもアクセスし放題
この辺はもうよく分からない。考えたくもない。
でもおそらく、ActiveAdminは何でもかんでも:read
で判定していたからとかそんな感じだろうと当たりをつけまして、こんな感じでパッチしました。
module CanCan
class ControllerResource
def authorization_action_with_authorization_read_action
action = authorization_action_without_authorization_read_action
[:index,:show].include?(action) ? :read : action
end
alias_method_chain :authorization_action, :authorization_read_action
end
end
ActiveAdminと同じように、:index
と:show
を:read
で判定するようにしてみたところ、なんとなく動いてます。(投げやり)
Filter、Collection
もう少しです。
Indexページのフィルターや編集画面の選択肢なども、CanCanCanで自分のものだけに絞り込むことができます。
ActiveAdmin.register Article do
...
filter :category, as: :select, collection: -> { Category.accessible_by(current_ability,:index) }
form do |f|
f.semantic_errors
f.inputs do
f.input :category , as: :select, collection: Category.accessible_by(current_ability,:index)
...
def proofreader
can :read , Category, article: { owner_user: @user.owner_user }
end
def editor
can :create , Category
can :read , Category, article: { owner_user: @user.owner_user }
can :manage_own, Category, article: { owner_user: @user.owner_user }
end
def owner
can :create , Category
can :read , Category, article: { owner_user: @user }
can :manage_own, Category, article: { owner_user: @user }
end
accessible_by
めっちゃ便利です
ね?
冗長っぽいですが
もっと複雑なことをやろうとすると、ブロックを使いたくなったりしてきます。
でも、閲覧系にはなぜかブロックを使えないので、更新系だけブロックで閲覧系は頑張って何とかする感じになっちゃいます。
例えば、
def proofreader
articles = Article.assigned_to(@user).select([:id])
can :read , [Article], id: articles.map(&:id) #がんばってなんとかしてる
can [:show,:update], [Article] do |subject, extra_args|
# なんか複雑なjoinやexistsを駆使してやっとこさ判定
subject.assigned_to? @user
end
end
なんてことになってしまった時に、周りと記述が違ってしまわないように、3段構成を基本にしておけばいいかなーくらいな感じです。
そういう意味でいうと、create
の方も行ごと消さないでcannot
にして置いておけばいいのかもですが試してないのでやめておきます。
#おわり
:read
のせいで要らないコードリーディング発生して大変でした。
これはバグなのか意図した動きなのかだけでも知りたいです・・・ Issue投げてみようかな・・・