Возвращать нулевой объект, если 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