Pundit は、通常の Ruby クラスとオブジェクト指向設計パターンを活用して、簡単で堅牢かつスケーラブルな承認システムを構築するための一連のヘルパーを提供します。
主催:バルベット
GitHub の README はGitHub の最新コードを正確に反映していることに注意してください。 Pundit のリリース バージョンを使用している可能性が高いため、Pundit の最新リリース バージョンのドキュメントを参照してください。
bundle add pundit
アプリケーション コントローラーにPundit::Authorization
を含めます。
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
必要に応じて、ジェネレーターを実行すると、いくつかの便利なデフォルトを使用してアプリケーション ポリシーが設定されます。
rails g pundit:install
アプリケーションポリシーを生成した後、Rails サーバーを再起動して、Rails が新しいapp/policies/
ディレクトリ内のクラスを選択できるようにします。
評論家はポリシークラスの概念に焦点を当てています。これらのクラスをapp/policies
に配置することをお勧めします。これは、ユーザーが管理者である場合、または投稿が非公開である場合に投稿の更新を許可する例です。
class PostPolicy
attr_reader :user , :post
def initialize ( user , post )
@user = user
@post = post
end
def update?
user . admin? || ! post . published?
end
end
ご覧のとおり、これはプレーンな Ruby クラスです。評論家は、このクラスについて次のような仮定を立てています。
current_user
メソッドを呼び出して、この引数に送信する内容を取得します。update?
を実装します。 。通常、これは特定のコントローラー アクションの名前にマップされます。本当にそれだけです。
通常、ジェネレーターによって作成されたアプリケーション ポリシーから継承するか、次のものを継承する独自の基本クラスを設定します。
class PostPolicy < ApplicationPolicy
def update?
user . admin? or not record . published?
end
end
生成されたApplicationPolicy
では、モデル オブジェクトはrecord
と呼ばれます。
クラスPost
のインスタンスがあると仮定すると、Pundit ではコントローラーでこれを実行できるようになりました。
def update
@post = Post . find ( params [ :id ] )
authorize @post
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
authorize メソッドは、 Post
一致するPostPolicy
クラスがあると自動的に推測し、このクラスをインスタンス化し、現在のユーザーと指定されたレコードを渡します。次に、アクション名からupdate?
呼び出す必要があると推測します。ポリシーのこのインスタンスについて。この場合、 authorize
次のようなことを行うと想像できます。
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
確認したい権限の名前がアクション名と一致しない場合は、2 番目の引数を渡してauthorize
できます。例えば:
def publish
@post = Post . find ( params [ :id ] )
authorize @post , :update?
@post . publish!
redirect_to @post
end
必要に応じて、引数を渡してポリシー クラスをオーバーライドできます。例えば:
def create
@publication = find_publication # assume this method returns any model that behaves like a publication
# @publication.class => Post
authorize @publication , policy_class : PublicationPolicy
@publication . publish!
redirect_to @publication
end
authorize
する最初の引数のインスタンスがない場合は、クラスを渡すことができます。例えば:
ポリシー:
class PostPolicy < ApplicationPolicy
def admin_list?
user . admin?
end
end
コントローラ:
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
authorize
渡されたインスタンスを返すため、次のようにチェーンできます。
コントローラ:
def show
@user = authorize User . find ( params [ :id ] )
end
# return the record even for namespaced policies
def show
@user = authorize [ :admin , User . find ( params [ :id ] ) ]
end
ビューとコントローラーの両方でpolicy
メソッドを使用して、ポリシーのインスタンスを簡単に取得できます。これは、ビューにリンクやボタンを条件付きで表示する場合に特に便利です。
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
対応するモデル/Ruby クラスのないポリシーがある場合、シンボルを渡すことでそれを取得できます。
# app/policies/dashboard_policy.rb
class DashboardPolicy
attr_reader :user
# `_record` in this example will be :dashboard
def initialize ( user , _record )
@user = user
end
def show?
user . admin?
end
end
ヘッドレス ポリシーは依然として 2 つの引数を受け入れる必要があることに注意してください。この場合、2 番目の引数はシンボル:dashboard
になります。これは、以下でauthorize
レコードとして渡されるものです。
# In controllers
def show
authorize :dashboard , :show?
...
end
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
多くの場合、特定のユーザーがアクセスできるレコードをリストする何らかのビューが必要になることがあります。 Pundit を使用する場合は、ポリシー スコープと呼ばれるクラスを定義する必要があります。次のようになります。
class PostPolicy < ApplicationPolicy
class Scope
def initialize ( user , scope )
@user = user
@scope = scope
end
def resolve
if user . admin?
scope . all
else
scope . where ( published : true )
end
end
private
attr_reader :user , :scope
end
def update?
user . admin? or not record . published?
end
end
評論家は、このクラスについて次のような仮定を立てています。
Scope
という名前があり、ポリシー クラスの下にネストされています。current_user
メソッドを呼び出して、この引数に送信する内容を取得します。ActiveRecord::Relation
ですが、まったく別のものである可能性もあります。resolve
に応答し、反復可能な何らかの結果を返す必要があります。 ActiveRecord クラスの場合、これは通常ActiveRecord::Relation
になります。おそらく、ジェネレーターによって生成されたアプリケーション ポリシー スコープから継承するか、継承する独自の基本クラスを作成する必要があるでしょう。
class PostPolicy < ApplicationPolicy
class Scope < ApplicationPolicy :: Scope
def resolve
if user . admin?
scope . all
else
scope . where ( published : true )
end
end
end
def update?
user . admin? or not record . published?
end
end
これで、 policy_scope
メソッドを介してコントローラーからこのクラスを使用できるようになりました。
def index
@posts = policy_scope ( Post )
end
def show
@post = policy_scope ( Post ) . find ( params [ :id ] )
end
authorize メソッドと同様に、ポリシー スコープ クラスをオーバーライドすることもできます。
def index
# publication_class => Post
@publications = policy_scope ( publication_class , policy_scope_class : PublicationPolicy :: Scope )
end
この場合、これは次のことを行うためのショートカットです。
def index
@publications = PublicationPolicy :: Scope . new ( current_user , Post ) . resolve
end
このメソッドはビューで使用できます。使用することをお勧めします。
<% policy_scope(@user.posts).each do |post| %>
< p > <%= link_to post . title , post_path ( post ) %> </ p >
<% end %>
Pundit を使用してアプリケーションを開発している場合、一部のアクションを承認することを忘れてしまうことがあります。結局のところ、人は忘れやすいのです。 Pundit は、各コントローラー アクションにauthorize
呼び出しを手動で追加することを推奨しているため、1 つを見落とす可能性が非常に高くなります。
ありがたいことに、Pundit には忘れた場合に思い出させてくれる便利な機能があります。 Pundit は、コントローラー アクションのどこかでauthorize
を呼び出したかどうかを追跡します。 Pundit はまた、 verify_authorized
というメソッドをコントローラーに追加します。 authorize
まだ呼び出されていない場合、このメソッドは例外を発生させます。このメソッドをafter_action
フックで実行して、アクションの承認を忘れていないことを確認する必要があります。例えば:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_authorized
end
同様に、Pundit はコントローラーにverify_policy_scoped
も追加します。これにより、 verify_authorized
と同様の例外が発生します。ただし、 authorize
の代わりにpolicy_scope
が使用されているかどうかを追跡します。これは、スコープを持つコレクションを検索し、個々のインスタンスを許可しないindex
などのコントローラー アクションに主に役立ちます。
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_pundit_authorization
def verify_pundit_authorization
if action_name == "index"
verify_policy_scoped
else
verify_authorized
end
end
end
この検証メカニズムは、アプリケーションの開発を支援するためにのみ存在するため、 authorize
呼び出すことを忘れないようにしてください。これは、ある種のフェールセーフ メカニズムや承認メカニズムではありません。アプリの動作に一切影響を与えることなく、これらのフィルターを削除できるはずです。
この機能がわかりにくいと感じる人もいますが、非常に役立つと感じる人もいます。混乱を感じる人のカテゴリーに当てはまる場合は、それを使用する必要はありません。 Pundit はverify_authorized
とverify_policy_scoped
使用しなくても正常に動作します。
コントローラーでverify_authorized
使用しているが、条件付きで検証をバイパスする必要がある場合は、 skip_authorization
を使用できます。 verify_policy_scoped
バイパスするには、 skip_policy_scope
使用します。これらは、アクション全体の検証を無効にしたくないが、承認しない場合がある状況で役立ちます。
def show
record = Record . find_by ( attribute : "value" )
if record . present?
authorize record
else
skip_authorization
end
end
場合によっては、Pundit に推測させるのではなく、特定のクラスにどのポリシーを使用するかを明示的に宣言したい場合があります。これは次のようにして実行できます。
class Post
def self . policy_class
PostablePolicy
end
end
あるいは、インスタンス メソッドを宣言することもできます。
class Post
def policy_class
PostablePolicy
end
end
Pundit は意図的に非常に小さなライブラリであり、自分でできないことは何も行いません。ここには秘密のソースはありません。できる限りのことは行わず、その後は邪魔にならないようにします。
Pundit で利用できる少数ながら強力なヘルパーを使用すると、特別な DSL や複雑な構文を使用せずに、適切に構造化された完全に機能する承認システムを構築できます。
ポリシー クラスとスコープ クラスはすべてプレーンな Ruby クラスであることに注意してください。つまり、DRY を実行するためにいつも使用しているのと同じメカニズムを使用できます。一連の権限をモジュールにカプセル化し、それらを複数のポリシーに含めます。 alias_method
使用して、一部の権限を他の権限と同じように動作させます。権限の基本セットから継承します。どうしても必要な場合はメタプログラミングを使用してください。
提供されているジェネレーターを使用してポリシーを生成します。
rails g pundit:policy post
多くのアプリケーションでは、実際に何もできるのはログインしたユーザーだけです。このようなシステムを構築している場合、ポリシー内のユーザーがすべての権限に対してnil
ではないことを確認するのは少々面倒になる可能性があります。ポリシーとは別に、このチェックをスコープの基本クラスに追加できます。
認証されていないユーザーをログイン ページにリダイレクトするフィルターを定義することをお勧めします。二次防御として、ApplicationPolicy を定義している場合、認証されていないユーザーが何らかの理由で通過した場合に例外を発生させることをお勧めします。こうすることで、より優雅に失敗することができます。
class ApplicationPolicy
def initialize ( user , record )
raise Pundit :: NotAuthorizedError , "must be logged in" unless user
@user = user
@record = record
end
class Scope
attr_reader :user , :scope
def initialize ( user , scope )
raise Pundit :: NotAuthorizedError , "must be logged in" unless user
@user = user
@scope = scope
end
end
end
null オブジェクト パターンをサポートするには、 NilClassPolicy
を実装する必要があるかもしれません。これは、 ApplicationPolicy を拡張して、たとえばnil
になる可能性のある関連付けをある程度許容できるようにする場合に便利です。
class NilClassPolicy < ApplicationPolicy
class Scope < ApplicationPolicy :: Scope
def resolve
raise Pundit :: NotDefinedError , "Cannot scope NilClass"
end
end
def show?
false # Nobody can see nothing
end
end
Pundit はPundit::NotAuthorizedError
を発生させます。 ApplicationController
でrescue_fromできます。すべてのコントローラーでuser_not_authorized
メソッドをカスタマイズできます。
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
rescue_from Pundit :: NotAuthorizedError , with : :user_not_authorized
private
def user_not_authorized
flash [ :alert ] = "You are not authorized to perform this action."
redirect_back_or_to ( root_path )
end
end
あるいは、Rails に Pundit::NotAuthorizedError を 403 エラーとして処理させ、403 エラー ページを提供することで、Pundit::NotAuthorizedError をグローバルに処理することもできます。 application.rb に以下を追加します。
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
NotAuthorizedError
は、どのクエリ (例:create?
)、どのレコード (例: Post
のインスタンス)、およびどのポリシー (例: PostPolicy
のインスタンス) によってエラーが発生したかに関する情報を提供します。
これらのquery
、 record
、およびpolicy
プロパティを使用する 1 つの方法は、それらをI18n
に接続してエラー メッセージを生成することです。以下にその方法を示します。
class ApplicationController < ActionController :: Base
rescue_from Pundit :: NotAuthorizedError , with : :user_not_authorized
private
def user_not_authorized ( exception )
policy_name = exception . policy . class . to_s . underscore
flash [ :error ] = t " #{ policy_name } . #{ exception . query } " , scope : "pundit" , default : :default
redirect_back_or_to ( root_path )
end
end
en :
pundit :
default : ' You cannot perform this action. '
post_policy :
update? : ' You cannot edit this post! '
create? : ' You cannot create posts! '
これは一例です。評論家は、エラー メッセージをどのように実装するかについては関知しません。
コントローラーまたはビューの外部にあるレコードのポリシーを取得したい場合があります。たとえば、あるポリシーから別のポリシーにアクセス許可を委任する場合などです。
次のようにポリシーとスコープを簡単に取得できます。
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
bang メソッドはポリシーが存在しない場合に例外を発生させますが、bang のないメソッドは nil を返します。
場合によっては、コントローラーがcurrent_user
にアクセスできない場合や、Pundit によって呼び出されるメソッドがcurrent_user
ではない場合があります。これに対処するには、コントローラーにpundit_user
という名前のメソッドを定義します。
def pundit_user
User . find_by_other_means
end
場合によっては、リソースのさまざまなコンテキストに対応する複数のポリシーがあると便利な場合があります。この主な例は、ユーザー ポリシーが管理者ポリシーと異なる場合です。名前空間ポリシーを使用して認可するには、配列内のauthorize
ヘルパーに名前空間を渡します。
authorize ( post ) # => will look for a PostPolicy
authorize ( [ :admin , post ] ) # => will look for an Admin::PostPolicy
authorize ( [ :foo , :bar , post ] ) # => will look for a Foo::Bar::PostPolicy
policy_scope ( Post ) # => will look for a PostPolicy::Scope
policy_scope ( [ :admin , Post ] ) # => will look for an Admin::PostPolicy::Scope
policy_scope ( [ :foo , :bar , Post ] ) # => will look for a Foo::Bar::PostPolicy::Scope
管理ビューなどに名前空間ポリシーを使用している場合は、 policy_scope
をオーバーライドし、 AdminController
のヘルパーに名前空間を自動的に適用するauthorize
と便利です。
class AdminController < ApplicationController
def policy_scope ( scope )
super ( [ :admin , scope ] )
end
def authorize ( record , query = nil )
super ( [ :admin , record ] , query )
end
end
class Admin :: PostController < AdminController
def index
policy_scope ( Post )
end
def show
post = authorize Post . find ( params [ :id ] )
end
end
Pundit では、認可に必要なコンテキストがユーザー オブジェクトと認可を確認するドメイン モデルだけになるようにアプリケーションをモデル化することを強くお勧めします。それ以上のコンテキストが必要な場合は、適切なドメイン モデルを承認しているかどうかを検討してください。別のドメイン モデル (または複数のドメイン モデルのラッパー) が必要なコンテキストを提供できる可能性があります。
Pundit では、まさにこの理由から、ポリシーに追加の引数を渡すことを許可していません。
ただし、非常にまれなケースですが、現在認証されているユーザーだけでなく、より多くのコンテキストに基づいて承認する必要がある場合があります。たとえば、認証が認証されたユーザーに加えて IP アドレスにも依存すると仮定します。その場合、1 つのオプションは、ユーザーと IP の両方をラップしてポリシーに渡す特別なクラスを作成することです。
class UserContext
attr_reader :user , :ip
def initialize ( user , ip )
@user = user
@ip = ip
end
end
class ApplicationController
include Pundit :: Authorization
def pundit_user
UserContext . new ( current_user , request . ip )
end
end
Rails では、質量割り当ての保護はコントローラーで処理されます。 Pundit を使用すると、ユーザーがポリシーを通じて更新にアクセスできる属性を制御できます。次のようにポリシーでpermitted_attributes
メソッドを設定できます。
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user . admin? || user . owner_of? ( post )
[ :title , :body , :tag_list ]
else
[ :tag_list ]
end
end
end
これで、ポリシーから次の属性を取得できるようになります。
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
@post = Post . find ( params [ :id ] )
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
private
def post_params
params . require ( :post ) . permit ( policy ( @post ) . permitted_attributes )
end
end
ただし、これは少し面倒なので、Pundit が便利なヘルパー メソッドを提供しています。
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
@post = Post . find ( params [ :id ] )
if @post . update ( permitted_attributes ( @post ) )
redirect_to @post
else
render :edit
end
end
end
現在のアクションに基づいてさまざまな属性を許可する場合は、ポリシーでpermitted_attributes_for_#{action}
メソッドを定義できます。
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[ :title , :body ]
end
def permitted_attributes_for_edit
[ :body ]
end
end
現在のアクションのポリシーでアクション固有のメソッドを定義している場合、代わりにpermitted_attributes
ヘルパーがそれを呼び出します。