Возвращать нулевой объект, если after_initialize callback возвращает false
В приложении My Rails есть много моделей, которые образуют иерархию. Например: Розничный торговец > Отдел > Категория продуктa > Продукт > Обзор.
Бизнес-требование состоит в том, что высокопоставленные пользователи могут "делиться" с каким-либо отдельным элементом в иерархии с новым или существующим "обычным" пользователем. Не имея общего с ними объекта, обычные пользователи не имеют права видеть (или делать что-либо еще) с любым объектом на любом уровне иерархии.
Процесс совместного доступа включает в себя выбор того, разрешает ли данный ресурс разрешение только на чтение, чтение-обновление или полный CRUD на целевой объект.
Обмен любыми объектами предоставляет R/O, R/W или CRUD-разрешения этому объекту и всем объектам нижнего уровня в иерархии и R/O-разрешение всем прямым предкам объекта. Коллекция объектов вырастает органично, поэтому система разрешений работает только путем регистрации пользователя user_id, объекта_объекта и характера ресурса (R/O, CRUD и т.д.). Поскольку совокупность объектов в этой иерархии растет все время, нецелесообразно создавать запись явного разрешения в БД для каждой комбинации пользователя/объекта.
Вместо этого, в начале цикла пользовательского запроса ApplicationController
собирает все записи разрешений (пользователь X имеет разрешение CRUD для Департамента № 5) и удерживает их в хеше в памяти. Модель разрешений знает, как оценить хэш, когда какой-либо объект передан ему - Permission.allow? (: Show, Department # 5) вернет true или false в зависимости от содержимого хеша разрешения пользователя.
Возьмем, например, модель отдела:
# app/models/department.rb
class Department < ActiveRecord::Base
after_initialize :check_permission
private
def check_permission
# some code that returns true or false
end
end
Когда метод check_permission
возвращает true, я хочу, чтобы Department.first
возвращал первую запись в базу данных как обычно, НО, если check_permission
возвращает false, я хочу вернуть nil
.
В настоящее время у меня есть решение, при котором области по умолчанию запускают проверку разрешений, но это вызывает 2X количество запросов, а для классов с большим количеством объектов проблемы с памятью и проблемы времени/производительности обязательно будут на горизонт.
Моя цель - использовать обратные вызовы after_initialize
для предварительного разрешения объектов.
Однако, похоже, что after_initialize
не может заблокировать возврат исходного объекта. Это позволяет мне reset значения атрибутов объекта, но не обойтись без него.
Кто-нибудь знает, как добиться этого?
EDIT:
Большое спасибо за все предоставленные ответы и комментарии; надеюсь, эта расширенная версия вопроса прояснит ситуацию.
Ответы
Ответ 1
В основном вам нужно проверить права доступа (или разрешения) перед возвратом результата запроса базы данных. И вы пытаетесь интегрировать эту логику в свои модели.
Возможно, но не с дизайном, описанным в вашем вопросе. Нецелесообразно реализовать это непосредственно в адаптивных адаптерах ActiveRecord (например, first
, all
, last
и т.д.). Вам нужно переосмыслить свой дизайн.
(перейдите к пункту "D", если это слишком много).
У вас есть несколько вариантов, которые зависят от того, как определяются ваши разрешения. Давайте рассмотрим несколько случаев:
A. У пользователя есть список отделов, которыми он владеет, и только он может получить к ним доступ.
Вы можете просто реализовать это как ассоциацию has_many
/belongs_to
с Ассоциациями активных записей
B. Пользователи и отделы независимы (другими словами: без владения, как описано в предыдущем случае), и разрешение может быть установлено индивидуально для каждого пользователя и каждого отдела.
Просто снова вы можете реализовать ассоциацию has_and_belongs_to_many
с Ассоциациями активных записей. Вам нужно будет создать веб-логику, чтобы администратор вашего приложения мог добавлять/редактировать/удалять права доступа.
C. Более сложный случай: существующие библиотеки авторизации
Большинство людей обратятся к решениям авторизации, таким как cancan, pundit или другой
D. Когда эти библиотеки авторизации имеют большие размеры для ваших нужд (на самом деле, мой случай в большинстве моих проектов), я обнаружил, что реализация авторизации через rails scoping отвечает на все мои потребности.
Посмотрим на простой пример. Я хочу, чтобы администраторы имели доступ к всем записям базы данных; и обычные пользователи получают доступ только к отделам со статусом = открыто и только во время работы (например, с 8:00 до 18:00). Я пишу область, которая реализует мою логику разрешений
# Class definition
class Department
scope :accessible_by -> (user) do
# admin user have all access, always
if user.is_admin?
all
# Regular user can access only 'open' departments, and only
# if their request is done between 8am and 6pm
elsif Time.now.hour >= 8 and Time.now.hour <= 18
where status: 'open'
# Fallback to return ActiveRecord empty result set
else
none
end
end
end
# Fetching without association
Department.accessible_by(current_user)
# Fetching through association
Building.find(5).departments.accessible_by(current_user)
Определение области обязывает нас использовать ее везде в нашем коде. Вы можете подумать о риске "забыть", проходя через область действия и напрямую обратиться к модели (т.е. Писать Department.all
вместо Department.accessible_by(current_user)
). Итак, почему вы должны твердо проверить свои разрешения в своих спецификациях (на уровне контроллера или функций).
Примечание. В этом примере мы не возвращаем nil
, когда разрешение выходит из строя (как вы упомянули в своем вопросе), но вместо этого создается пустой набор результатов. Как правило, это лучше, поэтому вы сохраняете возможность цепочки ActiveRecord. Но вы также можете создать исключение и спасти его от своего контроллера, а затем перенаправить на страницу "не авторизовано".
Ответ 2
Это не то, для чего используется обратный вызов after_initialize
. Вместо этого вы можете просто определить метод, который делает то же самое. Например, поместите это в свою модель Department
и она должна достичь результатов, которые вы ищете:
def self.get_first
check_permission ? first : nil
end
UPDATE
Я не совсем уверен, насколько безопасным будет что-то подобное, но вы можете просто переопределить метод all
, поскольку другие методы запроса основаны на нем.
class Department < ActiveRecord::Base
def self.all
check_permission ? super : super.none
end
private
def self.check_permission
# some code that returns true or false
end
end
Скорее всего, вам лучше использовать некоторую среду авторизации.
ОБНОВЛЕНИЕ 2
Размышляя об этом немного, я настоятельно рекомендую использовать другой подход. Вы действительно не должны переопределять такие методы, как all
, так как обязательно будут непреднамеренные побочные эффекты.
Практической альтернативой было бы создать связь has_and_belongs_to_many
между Department
и User
. Вот как вы его настроили:
user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :departments
...
end
department.rb
class Department < ActiveRecord::Base
has_and_belongs_to_many :users
...
end
Затем запустите эти команды в терминале:
rails g migration CreateJoinTableDepartmentsUsers departments users
rake db:migrate
Теперь вы можете добавить пользователей в отдел с @department.users << @user
или отделами для пользователя с помощью @user.departments << @department
. Это должно обеспечить функциональность, которую вы ищете.
@user.departments
вернет только отделы для этого пользователя, @user.departments.first
вернет первый отдел этого пользователя или nil
, если он его не имеет, а @user.departments.find(1)
вернет соответствующий отдел, только если он принадлежит пользователю или исключить исключение.
Ответ 3
Вы можете использовать обратный вызов before_create, чтобы остановить создание записи, если разрешение проверки ложно. Просто верните false в файле check_permission, и запись не будет создана.
class Department < ActiveRecord::Base
before_create :check_permission
private
def check_permission
# return false if permission is not allowed
end
end