Pundit предоставляет набор помощников, которые помогут вам использовать обычные классы Ruby и шаблоны объектно-ориентированного проектирования для создания простой, надежной и масштабируемой системы авторизации.
Спонсор: Варвет
Обратите внимание , что README на GitHub соответствует последней версии кода на 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
Метод авторизации автоматически делает вывод, что Post
будет иметь соответствующий класс PostPolicy
, и создает экземпляр этого класса, передавая текущего пользователя и данную запись. Затем из имени действия он делает вывод, что следует вызвать update?
в этом экземпляре политики. В этом случае вы можете себе представить, что authorize
сделала бы что-то вроде этого:
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
Вы можете передать второй аргумент для 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
Обратите внимание, что безголовая политика по-прежнему должна принимать два аргумента. В данном случае вторым аргументом будет символ :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
Как и в случае с методом авторизации, вы также можете переопределить класс области политики:
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
вручную к каждому действию контроллера, его очень легко пропустить.
К счастью, у 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
. Однако он отслеживает, используется ли policy_scope
вместо authorize
. Это в основном полезно для действий контроллера, таких как 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
Для поддержки шаблона нулевого объекта вы можете обнаружить, что хотите реализовать 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
вы можете спасать_from в своем ApplicationController
. Вы можете настроить метод 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
В качестве альтернативы вы можете глобально обрабатывать Pundit::NotAuthorizedError, заставляя рельсы обрабатывать их как ошибку 403 и отображать страницу с ошибкой 403. Добавьте в application.rb следующее:
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
NotAuthorizedError
предоставляют информацию о том, какой запрос (например :create?
), какая запись (например, экземпляр Post
) и какая политика (например, экземпляр PostPolicy
) вызвали возникновение ошибки.
Один из способов использования этих свойств query
, record
и policy
— подключить их к 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 не знает, как вы реализуете обмен сообщениями об ошибках.
Иногда вам нужно получить политику для записи вне контроллера или представления. Например, когда вы делегируете разрешения от одной политики другой.
Вы можете легко получить политики и области следующим образом:
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
Методы взрыва вызовут исключение, если политика не существует, тогда как методы без взрыва вернут ноль.
В некоторых случаях ваш контроллер может не иметь доступа к 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
и authorize
помощникам в вашем AdminController
автоматически применять пространство имен:
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-адреса. В этом случае одним из вариантов является создание специального класса, который объединяет пользователя и 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
o