Это вторая статья из серии «Авторизация с Rails». В предыдущей статье мы обсуждали CanCanCan , широко известное решение, созданное Райаном Бейтсом и поддерживаемое группой энтузиастов. Сегодня я собираюсь представить вам немного менее популярную, но все еще жизнеспособную библиотеку под названием Pundit, созданную людьми из ELabs .
«Пандит» означает «ученый», «умный парень» (иногда используется как негативное описание), но вам не нужно быть гением, чтобы использовать его в своих проектах. Пандит действительно легко понять, и, поверьте мне, он вам понравится. Я влюбился в него, когда просматривал документацию.
Идея, лежащая в основе Pundit, заключается в использовании простых старых классов и методов Ruby без привлечения каких-либо специальных DSL. Этот драгоценный камень добавляет только пару полезных помощников, так что в целом вы можете создать свою систему так, как считаете нужным. Это решение немного более низкого уровня, чем CanCanCan, и действительно интересно сравнить их.
В этой статье мы обсудим все основные функции Pundit: работа с правилами доступа, использование вспомогательных методов,
и определение и определение разрешенных атрибутов.
Рабочая демоверсия доступна по адресу sitepoint-pundit.herokuapp.com .
Исходный код можно найти на GitHub .
Препараты
Наши приготовления будут очень быстрыми. Идите вперед и создайте новое приложение Rails. Pundit работает с Rails 3 и 4, но для этой демонстрационной версии будет использоваться версия 4.1.
Вставьте следующие драгоценные камни в свой Gemfile :
Gemfile
[...]
gem 'pundit'
gem 'clearance'
gem 'bootstrap-sass'
[...]
Клиренс будет использоваться для очень быстрой настройки аутентификации. Эта жемчужина была рассмотрена в моей статье «Простая аутентификация Rails с очисткой» , так что вы можете обратиться к ней для получения дополнительной информации.
Bootstrap будет использоваться для базовых стилей, хотя вы можете пропустить это как всегда.
Не забудь бежать
$ bundle install
Теперь запустите генератор Clearance, который собирается создать модель User
$ rails generate clearance:install
Измените макет, включив в него флеш-сообщения, так как Clearance и Pundit используют их для отображения информации пользователю:
макеты / application.html.erb
[...]
<div id="flash">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>"><%= value %></div>
<% end %>
</div>
[...]
Создайте новый эшафот для Post
$ rails g scaffold Post title:string body:text
Мы также хотим, чтобы наши пользователи вошли в систему перед началом работы с приложением:
application_controller.rb
[...]
before_action :require_login
[...]
Настройте корневой маршрут:
routes.rb
[...]
root to: 'posts#index'
[...]
Администратор
Чтобы завершить настройку нашей лабораторной среды, нам нужно добавить поле admin
users
xxx_create_users.rb
[...]
t.boolean :admin, default: false, null: false
[...]
Запустите миграции:
$ rake db:migrate
Наконец, давайте добавим небольшую кнопку, чтобы легко переключаться между состояниями администратора:
макеты / application.html.erb
[...]
<% if current_user %>
<div class="well well-sm">
Admin: <strong><%= current_user.admin? %></strong><br>
<%= link_to 'Toggle admin rights', user_path(current_user), method: :patch, class: 'btn btn-info' %>
</div>
<% end %>
[...]
Мы должны проверить, присутствует ли current_user
Добавить маршрут:
routes.rb
[...]
resources :users, only: [:update]
[...]
и контроллер:
users_controller.rb
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
@user.toggle!(:admin)
flash[:success] = 'OK!'
redirect_to root_path
end
end
Это оно! Лабораторная среда готова, так что теперь мы можем начать играть с Pundit.
Интегрирование Pundit
Для начала добавьте следующую строку в ApplicationController
application_controller.rb
[...]
include Pundit
[...]
Затем запустите генератор Pundit:
$ rails g pundit:install
Это создаст базовый класс с политиками внутри папки app / icies. Классы политики — это ядро Pundit, и мы будем активно с ними работать. Базовый класс политики выглядит следующим образом:
приложение / политика / application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
@user = user
@record = record
end
def index?
false
end
def show?
scope.where(:id => record.id).exists?
end
def create?
false
end
def new?
create?
end
# [...]
# some stuff omitted
class Scope
# [...]
end
end
Каждая политика является базовым классом Ruby, но вы должны иметь в виду несколько вещей:
- Политики должны быть названы в честь модели, к которой они принадлежат, но с префиксом «
Policy
Например, используйтеPostPolicy
Post
Если у вас нет связанной модели, вы все равно можете использовать Pundit — читайте больше здесь . - Первым аргументом метода
initialize
Pundit использует методcurrent_user
pundit_user
Узнайте больше здесь . - Второй аргумент — это модельный объект. Это не обязательно должен быть объект
ActiveModel
- Класс политики должен реализовывать методы запросов, такие как
create?
илиnew?
проверить права доступа.
Если вы используете базовый класс политики и наследуете его, вам не нужно беспокоиться о большинстве этих вещей, но могут быть случаи, когда вам нужен собственный класс политики (например, когда у вас нет соответствующая модель).
Предоставление правил доступа
Теперь давайте напишем наше первое правило доступа. Например, мы хотим, чтобы только администраторы могли уничтожать посты. Это легко сделать! Создайте новый файл post_policy.rb внутри папки политики и вставьте следующий код:
политика / post_policy.rb
class PostPolicy < ApplicationPolicy
def destroy?
user.admin?
end
end
Как вы видите, destroy?
Метод будет возвращать true
false
Внутри контроллера нам нужно проверить наше правило:
posts_controller.rb
[...]
def destroy
authorize @post
@post.destroy
redirect_to posts_url, notice: 'Post was successfully destroyed.'
end
[...]
authorize
Это полезно, если имя вашего действия отличается, например:
def publish
authorize @post, :update?
end
Вы также можете передать имя класса вместо экземпляра, например, если у вас нет ресурса для работы:
authorize Post
Если пользователю не разрешено выполнять действие, возникнет ошибка. Нам нужно спастись от него и вместо этого отобразить полезное сообщение. Самым простым решением будет просто отобразить некоторый основной текст:
application_controller.rb
[...]
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:warning] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
end
[...]
Однако вам может потребоваться настроить собственное сообщение для разных случаев или перевести его на другие языки. Это также возможно:
application_controller.rb
[...]
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized(exception)
policy_name = exception.policy.class.to_s.underscore
flash[:warning] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
redirect_to(request.referrer || root_path)
end
[...]
Не забудьте обновить файл переводов:
конфиг / локали / en.yml
en:
pundit:
default: 'You cannot perform this action.'
post_policy:
destroy?: 'You cannot destroy this post!'
Если пользователь не может уничтожить сообщение, нет смысла рендерить кнопку «Уничтожить». К счастью, Pundit предоставляет специальный вспомогательный метод:
просмотров / сообщений / index.html.erb
[...]
<% if policy(post).destroy? %>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
[...]
Идите и проверьте это! Все должно работать хорошо.
Вы можете подумать, что внутри политик мы не рассматриваем случай, когда пользователь вообще не аутентифицирован. Чтобы это исправить, просто добавьте следующую строку:
политика / application_policy.rb
[...]
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
@user = user
@record = record
end
[...]
Только не забудьте спасти от ошибки Pundit::NotAuthorizedError
ApplicationController
Настройка отношений
Теперь давайте установим отношения «один ко многим» между сообщениями и пользователями. Создайте новую миграцию и примените ее:
$ rails g migration add_user_id_to_posts user:references
$ rake db:migrate
Изменить файлы модели:
модели / post.rb
[...]
belongs_to :user
[...]
модели / user.rb
[...]
has_many :posts
[...]
Кроме того, было бы здорово заполнить базу данных некоторыми демонстрационными записями. Для этого используйте seed.rb :
20.times do |i|
Post.create({title: "Post #{i + 1}", body: 'test body', user_id: i > 10 ? 1 : 2})
end
Мы просто создаем 20 постов, принадлежащих разным пользователям. Не стесняйтесь изменять этот код по мере необходимости.
Бегать
$ rake db:seed
заполнить базу данных.
Измените политику, чтобы позволить пользователям уничтожать свои собственные сообщения:
политика / post_policy.rb
[...]
def destroy?
user.admin? || record.user == user
end
[...]
Как вы, наверное, помните, record
Это хорошо, но мы можем сделать больше. Как насчет создания области для загрузки только сообщений, которыми владеет пользователь? Pundit это тоже поддерживает!
Работа с областями действия
Прежде всего, создайте новое, не RESTful действие:
posts_controller.rb
def user_posts
end
routes.rb
resources :posts do
collection do
get '/user_posts', to: 'posts#user_posts', as: :user
end
end
Добавить верхнее меню:
макеты / application.html.erb
[...]
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'Pundit', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar">
<ul class="nav navbar-nav">
<li><%= link_to 'All posts', posts_path %></li>
<li><%= link_to 'Your posts', user_posts_path %></li>
</ul>
</div>
</div>
</nav>
[...]
Нам нужно создать реальную сферу. Внутри файла application_policy.rb есть класс Scope
политика / application_policy.rb
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
scope
end
end
Как и в случае с политиками, необходимо учитывать несколько моментов:
- Класс должен называться
Scope
- Первый аргумент, передаваемый методу
initialize
пользователь , как и с политиками. - Второй аргумент — это область действия (экземпляр
ActiveRecord
ActiveRecord::Relation
- Класс
Scope
Просто наследуйте от базового класса и реализуйте свой собственный метод resolve
политика / post_policy.rb
resolve
Эта область просто загружает все сообщения, которые принадлежат пользователю. Используйте эту новую область внутри вашего контроллера:
posts_controller.rb
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.where(user: user)
end
end
[...]
end
Возможно, вы захотите извлечь некоторый код из представления индекса в партиалы (и немного подправить таблицу):
просмотров / сообщений / _post.html.erb
[...]
def user_posts
@posts = policy_scope(Post)
end
[...]
просмотров / сообщений / _list.html.erb
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<% if policy(post).destroy? %>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
</tr>
просмотров / сообщений / index.html.erb
<table class="table table-bordered table-striped table-condensed table-hover">
<thead>
<tr>
<th>Title</th>
<th>Body</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<%= render @posts %>
</tbody>
</table>
<br>
<%= link_to 'New Post', new_post_path %>
Теперь просто создайте новый вид:
просмотров / сообщений / user_posts.html.erb
<h1>Listing Posts</h1>
<%= render 'list' %>
Загрузи сервер и наблюдай за результатом!
Внутри представлений вы можете использовать следующий код, чтобы выбрать только необходимые записи:
<h1>Your Posts</h1>
<%= render 'list' %>
Принудительная авторизация
Если вы хотите проверить, что авторизация имела место в вашем контроллере, используйте <% policy_scope(@user.posts).each do |post| %>
Вы также можете выбрать, какие действия проверять:
<% end %>
posts_controller.rb
verify_authorized
То же самое можно сделать, чтобы убедиться в том, что обзор был выполнен:
posts_controller.rb
[...]
after_action :verify_authorized, only: [:destroy]
[...]
Однако в некоторых случаях проверка авторизации может быть необоснованной, поэтому вы можете пропустить ее. Например, если уничтожаемая запись не была найдена, мы просто возвращаемся без дальнейших действий. Для этого [...]
after_action :verify_policy_scoped, only: [:user_posts]
[...]
skip_authorization
[...]
def destroy
if @post.present?
authorize @post
@post.destroy
else
skip_authorization
end
redirect_to posts_url, notice: 'Post was successfully destroyed.'
end
private
def set_post
@post = Post.find_by(id: params[:id])
end
[...]
Разрешенные параметры
Последняя функция, которую мы собираемся обсудить, — это возможность определения разрешенных параметров в политиках Pundit. Обратите внимание, что для правильной работы вы должны использовать Rails 4 или Rails 3 с гемом strong_params .
Предположим, у нас есть некоторый «специальный» параметр, который могут изменять только администраторы. Давайте добавим соответствующую миграцию:
$ rails g migration add_special_to_posts special:boolean
Немного измените миграцию:
xxx_add_special_to_posts.rb
[...]
add_column :posts, :special, :boolean, default: false
[...]
и применить его:
$ rake db:migrate
Теперь определите новый метод в ваших политиках:
политика / post_policy.rb
[...]
def permitted_attributes
if user.admin?
[:title, :body, :special]
else
[:title, :body]
end
end
[...]
Таким образом, администратор может установить все атрибуты, тогда как пользователи могут изменять только title
body
Наконец, обновите методы контроллера:
posts_controller.rb
[...]
def create
@post = Post.new
@post.update_attributes(permitted_attributes(@post))
if @post.save
redirect_to @post, notice: 'Post was successfully created.'
else
render :new
end
end
def update
if @post.update(permitted_attributes(@post))
redirect_to @post, notice: 'Post was successfully updated.'
else
render :edit
end
end
[...]
permitted_attributes
Внутри метода create
@post
Если вы делаете это:
@post = Post.new(permitted_attributes(@post))
возникнет ошибка, потому что вы пытаетесь передать несуществующий объект в permitted_attributes
Вместо использования permitted_attributes
post_params
posts_controller.rb
[...]
def post_params
params.require(:post).permit(policy(@post).permitted_attributes)
end
[...]
Наконец, измените представления:
просмотров / сообщений / _form.html.erb
[...]
<div class="field">
<%= f.label :special %><br>
<%= f.check_box :special %>
</div>
[...]
просмотров / сообщений / _list.html.erb
[...]
<thead>
<tr>
<th>Title</th>
<th>Body</th>
<th>Special?</th>
<th colspan="3"></th>
</tr>
</thead>
[...]
просмотров / сообщений / _post.html.erb
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= post.special? %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<% if policy(post).destroy? %>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
</tr>
Теперь загрузите сервер и попытайтесь установить для «специального» параметра значение true, будучи обычным пользователем — вы не сможете этого сделать.
Вывод
В этой статье мы обсудили Pundit — отличное решение для авторизации, использующее базовые классы Ruby. Мы рассмотрели большинство его возможностей. Я рекомендую вам просмотреть его документацию, потому что вас может заинтересовать, как предоставить дополнительный контекст или указать класс политики вручную .
Вы когда-нибудь использовали Pundit раньше? Рассматриваете ли вы использовать его в будущем? Как вы думаете, это удобнее, чем CanCanCan, или это просто другое решение? Поделитесь своим мнением в комментариях!
Как всегда, я благодарю вас за то, что вы остались со мной. Если вы хотите, чтобы я осветил тему, не стесняйтесь спрашивать. Увидимся!