Ruby on Rails における権限Gem まとめ と GlobalState | Offers Tech Blog
こんにちは!🐯
プロダクト開発人材の副業転職プラットフォーム Offers を運営する株式会社 overflow のエンジニアの Taiga🐯 です。
overflow の新規サービスにおいて、権限関連の制御(Model
ベースアクセス)をどう導入するか検討した結果、GlobalState を Model レイヤー用に導入することとなりました。
付随しますが、権限関連の Gem
での現状の選択肢は、「CanCanCan
」「Banken
」「Pundit
」の 3 つであり、左記に関しては紹介記事も多いので二番煎じ感も否めません。
ですが、今回は、GlobalStateと上記 Gem
3 種について備忘録としてまとめてみます。少しでも参考になれば幸いです。
はじめに
権限管理はサービスの 1 つの要であり、情報を扱う私達にとっては重要なファクターです。
ビジネスロジックの分岐によりコードが肥大化していく問題や、情報の漏れを防ぐためにどの権限管理方法を導入する前に、しっかりとメリット・デメリットを理解しておくべきですよね![1]
cancancan
CanCanCan は Ability
クラスに認可のロジックを集約させ、Controller
ベースで制御をかけていきます。
使い方
class Ability
include CanCan::Ability
# Modelは適当です 他の初期値は省略
def initialize(admin_user)
if admin_user.admin?
can :manage, :all
# 全てのコントローラーで全てのアクション実行可能
elsif admin_user.author?
can :manage, Article
cannot :destroy, Article
# Article の destroy を 除く全てのアクションが実行できる
elsif admin_user.sales?
can :read, Fee
# Feeの読み取り(index, show)だけができる
end
end
end
上記のように、各権限毎に app/models/ability.rb
にてアクセス制御を行い、下記のように Controller
にて定義します。
この場合の load_and_authorize_resource
では、 @article = Article.find(params[:id])
が action 定義時に自動で読み込まれています。
class ArticlesController < ApplicationController
load_and_authorize_resource
def index
end
def show
end
def create
@article.create
end
def edit
end
def update
@article.update(article_params)
end
def destroy
@article.destroy
end
private
def article_params
params.require(:article).permit(:body)
end
end
メリット
-
Ability
クラスを見れば権限一覧が理解しやすい -
RailsWay
かつ権限が平易な場合は管理しやすい
デメリット
-
RailsWay
の 7 つのResource
以外のaction
を追加した場合、処理が複雑化し肥大化する -
Controller
ベースのためModel
単位で制御は難しいため、権限管理が複雑な場合も肥大化しやすい -
load_and_authorize_resource
などといった独自DSL
が多く、使いこなしづらく、覚えるのも辛い
業務(現職ではありません)で使用したことがありますが、Ability
クラスが数千行になり、メリットの可視性も享受できなくなりツラミを感じる回数が多かった印象にあります。
pundit
使い方
Pundit は Controller
の各アクションで authorize
リソースオブジェクトを呼ぶと対象のリソースに対して権限があるかどうかを確認してくれます。
その設定を app/policies
配下にある policy file
で細かく定義できます。
class AppolicationPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? || !post.published?
end
end
使い方はシンプルで、ApplicationPolicy
を継承して Policy
を定義していくイメージです。
class PostPolicy < ApplicationPolicy
def update?
user.admin? or not record.published?
end
end
実際のアクションで、制御をかけます。
def update
@post = Post.find(params[:id])
authorize @post
if @post.update(post_params)
redirect_to @post
else
render :edit
end
end
メリット
- 継承し
Policy
を定義出来るためRuby
に慣れていると使いやすい -
Pundit
の内部実装はRails
のバージョンアップによる影響を受けにくい(モンキーパッチなど拡張していない)
デメリット
-
Model
毎に作成するPolicy
クラスには、Controller
のAction
に紐づくメソッドを実装するので、Model:Policy:Controller
が1対1対1という縛りが生まれる - 特定のモデルを扱う
Controller
が複数存在し 1 つのModel
に対し複数のPolicy
を必要とする場合Policy
モデルをモンキーパッチするなり、拡張するなり工夫を強いられる
Pundit
は Model
ベースで定義出来ますが、Model:Policy:Controller
が1対1対1という暗黙的な縛りが生まれるため、結局エンジニアが Policy
モデルを拡張することになる可能性が高い印象です。
個人開発レベルで使ったことはありますが、業務レベルだと権限が複雑化し継承先の Policy
をカスタマイズすると複雑性が増しそうだなといった印象です。
banken
banken は、Pundit
での 1対1対1の Model
縛りを避けるために、Controller
毎に Action
に対する認可条件を定義する Gem です。
参考:日本語ドキュメント もあります。
使い方
ドキュメントの内容を転記しますが、
流れとしては、
-
ApplicationController
にinclude Banken
する -
app/loyalties/application_loyalty.rb
を作成する -
app/loyalties/
配下に、対象のController
のPolicy
クラスを作成(例ではPosts
) - アクションにて制御
まず大元で定義し、
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Banken
protect_from_forgery
end
#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
Loyalty
クラスでも作成し定義
# app/loyalties/posts_loyalty.rb
class PostsLoyalty < ApplicationLoyalty
def update?
user.admin? || record.unpublished?
end
end
authorize! @post
にて、Pundit
と似たように Controller
(Pundit
では Model
で制御していましたね) から、Loyalty
クラスでの制御に変わりました。
# 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
メリット
-
Pundit
のデメリットの1対1対1の制限を受けずに、Controller
ベースで権限制御できる - 継承し
Loyality
を定義出来るためRuby
に慣れていると使いやすい -
Banken
の内部実装はRails
のバージョンアップによる影響を受けにくい(モンキーパッチなど拡張していない)
デメリット
- 使用者が少ない
-
NameScope
機能がない
個人的所感としては、Controller
毎に定義できる点が魅力ですが、権限系は一度実装すると変更が容易ではなくなるので、メジャーな Pundit
をうまく継承しつつ
使ったほうが良いのでは?と思っております。
GlobalState
GlobalState とはなにか
GlobalState は Ruby on Rails の CurrentAttributes を扱った代物です。
定義したい要素を Global な Scope で定義し、参照できるといったイメージです。
使い方
簡略化して説明します。
まず CurrentAttributes
を使える用 module
等に定義します。
module GlobalStateRegistry
class RolePolicy < ::ActiveSupport::CurrentAttributes
attributes :address
attributes :user
# set -> with_isolated_registry
alias_method :with_isolated_registry, :set
def serialize
self.attributes
end
end
end
そして、実際に policy
を定義をします。
(今回はこのメソッドが User
のインスタンスから叩かれる想定です)
(User
モデルにて、include UserRolePolicyMethods
します )
module UserRolePolicyMethods
extend ActiveSupport::Concern
def generate_role_policy
role_policy = {
address: false
user: self,
}
end
end
すると、GlobalStateRegistry::RolePolicy
が定義されている場合、
GlobalContextRegistry::RolePolicy.user
は、スコープ外で user
インスタンスへアクセス出来るようになります。
なぜ GlobalState を使ったか
弊社の新規サービスでは、センシティブな情報が複雑な条件の管理の元に実装されており、更に Serilalizer
毎に返していい情報を分岐で処理していました。
先程の例でいうと address
がセンシティブな情報だった場合、 model
単位でアクセスの制御が出来ていませんでした。
漏洩するリスクを減らしたいため、 下記のステップを踏ませるように変更しました。
-
set_address_policy
メソッドで、「GlobalStateRegistry::RolePolicy.user
がaddress
にアクセスする権限があるか」をチェック -
GlobalState
を介していなければアクセスできない
結果として、「いかなるときでもそのカラムにアクセスされた場合には必ず GlobalState
を介していなければデータを返さない」といった構成が完成しました。
# 一例です
def address
if address_can_read_by_user?
self.read_attribute(:address) || ""
else
""
end
end
def address_can_read_by_user?
# Bool返す & イメージです
GlobalStateRegistry::RolePolicy.user.set_address_policy(self.id)
GlobalStateRegistry::RolePolicy.address
end
メリット
・情報をモデル単位で制御できる安心感
・複雑な条件になっても、Policy
を追加すればいい
デメリット
・スコープが限定されていないので、用法用量を守れないとコードが荒れる
・テストコードの難易度が上がる(GlobalState
を定義しないとテストにならない)
まとめ
要件により権限権限のベタープラクティスは変わるかもしれません。しかし、自身で権限設定を追加出来るGlobalStateは用法用量[2]を守れば使い勝手のいい手法といった印象であるため(基本的には有害と言われていますが)今回導入しました。
本来なら RLS を使うと更に better かもしれませんが、通常の Gem
に加えて更に制御を加えたい場合や、スコープを無視して制御せざるを得ない場合には、GlobalState
も有効かと思います。
副業転職の Offers 開発チームがお送りするテックブログです。【エンジニア積極採用中】カジュアル面談、副業からのトライアル etc 承っております💪 jobs.overflow.co.jp
Discussion