Rails
Rails4
activeadmin
cancancan

【Rails】ActiveAdmin+CanCanCanで権限毎に色々したい

More than 1 year has passed since last update.


背景

ActiveAdminを、運用上のマスター管理だけではなく、編集者や寄稿者など権限設定して色々できるようにしたい。

ActiveAdminにはデフォルトでCanCanCanのアダプターが搭載されていますが、ちょっと癖があるのでうまく使えるようになりたい。


やりたいこと


  • CanCanCanで権限管理

  • Indexページでは自分に割り当てられたレコードのみ一覧可能

  • Showと更新系ページも自分に割り当てられたもののみアクセス可能

  • 他人のページは絶対に見れない

  • ActiveAdmin外でアクセス制限したいときにも同じAbilityで対応したい

ユーザー権限は例えばこんな感じ


  • AdminUser(管理者)は全てのテーブルをCRUD可能

  • OwnerUser(オーナー)は自分の記事のみCRUD可能

  • EditorUser(編集者)はオーナーにぶら下がり、オーナーの記事のみCRUD可能

  • ProofreaderUser(校閲係)も同様だが、編集のみ可能(RU)


準備


ActiveAdminでCanCanCanを使う


Gemfile

# Admin Page

gem 'activeadmin', github: 'activeadmin'

# Role
gem 'cancancan', '~> 1.10'



config/initialize/active_admin.rb

  config.authorization_adapter = ActiveAdmin::CanCanAdapter

config.on_unauthorized_access = :access_denied


app/controllers/application_controller.rb

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



app/models/ability.rb

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


書式


ability.rb

# 基本は

# 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に表示させたくない場合は、次のようにします。


app/models/article.rb

class Article < ActiveRecord::Base

belongs_to :owner_user, class_name: :User, foreign_key: :owner_user_id
end


app/models/ability.rb

...

def owner
can :read, Article, owner_user: @user
end

def admin
can :read, Article
end


このようになっていると、管理者でログインすると全てのレコードが表示され、オーナーでログインすると自分の記事だけに絞り込んで表示されます。

内部的には、Indexページを表示するときにこのようになっているみたいです。


lib/active_admin/cancan_adapter.rb

  def scope_collection(collection, action = ActiveAdmin::Auth::READ)

collection.accessible_by(cancan_ability, action)
end

accessible_byaction= :readが渡ることで上の条件が追加されてcollectionから更に絞り込まれています。


更新系は工夫が必要


とりあえず、

上を踏まえてこうしてみました。


app/models/ability.rb

...

def owner
can :manage, Article, owner_user: @user
end

def admin
can :manage, Article
end


manage:read~:destroyまで全て含んだAliasなのでこれで行ける!

かと思いきや、createのときも絞り込みが入ってしまい、/newページが開けません。(でも「Articleを新規作成する」ボタンは表示されます)


ということでこうしてみる。


app/models/ability.rb

...

def owner
can :create, Article
can :manage, Article, owner_user: @user
end

def admin
can :manage, Article
end


いけます :thumbsup: :thumbsup:


だが待てよ。

上でさりげなくカッコ書きしましたが、


/newページが開けません。(でも「Articleを新規作成する」ボタンは表示されます)


これだと、proofreader(校閲係)にこれをコピーするとき、「createの行だけ消せばいいじゃん」とはなりません。


/:id用のAlias

そこでこのように、/:idなアクション用に新しくAliasを作ります。


app/models/ability.rb

...

def initialize(user)
alias_action :show, :update, :destroy, :to => :manage_own
...

そして、こうしてみます。


app/models/ability.rb

  def owner

can :create , Article #新規作成
can :index , Article, owner_user: @user #一覧
can :manage_own, Article, owner_user: @user #/:id系
end


「アクションを実行する権限がありません」

Indexが見えなくなりました。 :sob: :sob:

グローバルメニューからも消えます。 :sob: :sob:

Showのパスを直叩きしても見えません。 :sob: :sob:

なぜかというと、「Indexのページは:readでチェックしている」みたいです。

:readには:indexも含まれてるような気がしますが、そう簡単なことではないらしい・・・。もしかして:read:index:showをマージしたもの?)

その他諸々、Article自体にアクセスするかどうかのチェックは:readで行ってるみたいです。


【完成】

ということで、

こうしてみます。


app/models/ability.rb

  def owner

can :create , Article #新規作成
can :read , Article, owner_user: @user #閲覧系
can :manage_own, Article, owner_user: @user #更新系
end

バッチリ :thumbsup: :thumbsup:

proofreaderにはcreate以外をコピーすればOKです。


ではActiveAdmin外でも、


app/controllers/artivcle.rb

class ArticlesController < ApplicationController

load_and_authorize_resource

def show
end
end



わーい

自分の記事も表示できるけど、他人の記事にもアクセスし放題 :sob: :sob:

この辺はもうよく分からない。考えたくもない。

でもおそらく、ActiveAdminは何でもかんでも:readで判定していたからとかそんな感じだろうと当たりをつけまして、こんな感じでパッチしました。


lib/extensions/cancan/controller_resource.rb

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で自分のものだけに絞り込むことができます。


app/admin/article.rb

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)
...



app/models/ability.rb

  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 めっちゃ便利です

ね?


冗長っぽいですが

もっと複雑なことをやろうとすると、ブロックを使いたくなったりしてきます。

でも、閲覧系にはなぜかブロックを使えないので、更新系だけブロックで閲覧系は頑張って何とかする感じになっちゃいます。

例えば、


app/models/ability.rb

  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投げてみようかな・・・