Авторизация в Rails-приложениях — это как охрана в ночном клубе: если пропустит не того, будет скандал, а если слишком строгая — никто не войдёт. В этой статье разберём два популярных инструмента — Pundit и CanCanCan — и выясним, когда какой выбрать, чтобы не превратить код в крепость с колючей проволокой.
🧠 Теория: Pundit vs CanCanCan — в чём разница?
Оба гема решают одну задачу: “Может ли пользователь сделать X?”, но подходят к ней с разных сторон.
| Особенность | CanCanCan | Pundit |
|---|---|---|
| Подход | Централизованный (Ability-класс) | Децентрализованный (Policy-классы) |
| Где живут правила | Один файл ability.rb |
Множество *_policy.rb |
| Синтаксис | can :read, Project |
policy(@project).read? |
| Гибкость | Умеренная | Высокая |
CanCanCan напоминает швейцарский нож — все правила в одном месте. Pundit — это модульный инструмент, где каждая сущность получает свою “политику”.
🔧 CanCanCan: пример настройки
Устанавливаем гем:
# Gemfile
gem 'cancancan'
Создаём Ability-класс:
# app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # гость
if user.admin?
can :manage, :all
else
can :read, Project, public: true
can :manage, Project, user_id: user.id
end
end
end
Использование в контроллере:
class ProjectsController < ApplicationController
load_and_authorize_resource # магия!
def show
# @project уже авторизован
end
end
🔧 Pundit: пример настройки
Устанавливаем гем:
# Gemfile
gem 'pundit'
Политика для проекта:
# app/policies/project_policy.rb
class ProjectPolicy
attr_reader :user, :project
def initialize(user, project)
@user = user
@project = project
end
def read?
project.public? || user.admin? || project.user == user
end
end
Контроллер:
class ProjectsController < ApplicationController
include Pundit::Authorization
def show
@project = Project.find(params[:id])
authorize @project, :read?
end
end
💡 Когда что выбрать?
CanCanCan подходит, если:
- Правила простые и логически связаны
- Хочется минимального кода в контроллерах
- Нужна “из коробки” интеграция с Rails Admin
Pundit выручает, когда:
- Правила сложные и зависят от контекста
- Требуется тонкий контроль над сообщениями об ошибках
- Политики должны быть тестируемы изолированно
🧪 Тестирование: CanCanCan vs Pundit
CanCanCan (RSpec):
describe Ability do
it "разрешает админу всё" do
admin = build(:user, admin: true)
ability = Ability.new(admin)
expect(ability).to be_able_to(:manage, :all)
end
end
Pundit (RSpec):
describe ProjectPolicy do
let(:user) { build(:user) }
let(:project) { build(:project, user: user) }
it "разрешает чтение владельцу" do
expect(ProjectPolicy.new(user, project).to permit_action(:read?)
end
end
Pundit здесь выигрывает за счёт явной изоляции тестов.
🔥 Антипаттерны
Для CanCanCan:
- Ability-монстр (500+ строк в одном файле)
- Магические условия (
can :update, Project, { tasks: { assignee_id: user.id } }) - Глобальные правила (когда
cannot :manage, :allперекрывает всё)
Для Pundit:
- Policy-спагетти (дублирование кода между политиками)
- Контроллерный хаос (ручные
authorizeв каждом методе) - Наследование-костыль (когда
AdminPolicy < UserPolicyзапутывает всё)
🎤 Что сказать на собеседовании
— Почему вы выбрали Pundit, а не CanCanCan?
— В нашем проекте политики доступа были тесно связаны с бизнес-контекстом (например, доступ к черновикам статей зависел от 5+ факторов). Pundit позволил разнести логику по доменным объектам и тестировать её изолированно.
🧾 Вывод
CanCanCan — это “быстро и просто” для стандартных сценариев. Pundit — “гибко и явно” для сложных систем.
Выбирайте первый, если правила авторизации — это просто проверка ролей. Второй — когда “можно ли?” зависит от состояния объекта, времени суток и фазы луны. Главное — не превращайте авторизацию в лабиринт, из которого даже вы не сможете выбраться через полгода.