gitlab-ce/app/models/concerns/cascading_project_setting_a...

173 lines
5.4 KiB
Ruby

# frozen_string_literal: true
module CascadingProjectSettingAttribute
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
class_methods do
private
# logic based on the cascading setting logic
# CascadingNamespaceSettingAttribute
#
# Code remove for this module:
# - logic related to 'lock_#{attribute}', because projects don't need to lock attributes.
# - logic related to descendants, because projects don't have descendants.
# - logic related to a `nil` value for the setting, because the first/only
# cascading project setting (`duo_features_enabled`) has a db-level not nil constraint.
def cascading_attr(*attributes)
attributes.map(&:to_sym).each do |attribute|
# public methods
define_attr_reader(attribute)
define_attr_writer(attribute)
define_lock_methods(attribute)
# private methods
define_validator_methods(attribute)
define_attr_before_save(attribute)
validate :"#{attribute}_changeable?"
before_save :"before_save_#{attribute}", if: -> { will_save_change_to_attribute?(attribute) }
end
end
def define_attr_reader(attribute)
define_method(attribute) do
strong_memoize(attribute) do
next self[attribute] if will_save_change_to_attribute?(attribute)
next locked_value(attribute) if cascading_attribute_locked?(attribute)
next self[attribute] unless self[attribute].nil?
cascaded_value = cascaded_ancestor_value(attribute)
next cascaded_value unless cascaded_value.nil?
application_setting_value(attribute)
end
end
end
def define_attr_writer(attribute)
define_method("#{attribute}=") do |value|
return value if read_attribute(attribute).nil? && to_bool(value) == cascaded_ancestor_value(attribute)
clear_memoization(attribute)
super(value)
end
end
def define_attr_before_save(attribute)
# rubocop:disable GitlabSecurity/PublicSend -- model attribute, not user input
define_method("before_save_#{attribute}") do
new_value = public_send(attribute)
return unless public_send("#{attribute}_was").nil? && new_value == cascaded_ancestor_value(attribute)
write_attribute(attribute, nil)
end
# rubocop:enable GitlabSecurity/PublicSend
private :"before_save_#{attribute}"
end
def define_lock_methods(attribute)
define_method("#{attribute}_locked?") do
cascading_attribute_locked?(attribute)
end
define_method("#{attribute}_locked_by_ancestor?") do
locked_by_ancestor?(attribute)
end
define_method("#{attribute}_locked_by_application_setting?") do
locked_by_application_setting?(attribute)
end
define_method("#{attribute}_locked_ancestor") do
locked_ancestor(attribute)
end
end
def define_validator_methods(attribute)
define_method("#{attribute}_changeable?") do
return unless cascading_attribute_changed?(attribute)
return unless cascading_attribute_locked?(attribute)
errors.add(attribute, s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
end
private :"#{attribute}_changeable?"
end
end
private
def locked_value(attribute)
return application_setting_value(attribute) if locked_by_application_setting?(attribute)
ancestor = locked_ancestor(attribute)
ancestor.read_attribute(attribute) if ancestor
end
def locked_ancestor(attribute)
return unless direct_ancestor_present?
strong_memoize(:"#{attribute}_locked_ancestor") do
NamespaceSetting
.select(:namespace_id, "lock_#{attribute}", attribute)
.where(namespace_id: namespace_ancestor_ids)
.where(NamespaceSetting.arel_table["lock_#{attribute}"].eq(true))
.limit(1).load.first
end
end
def locked_by_ancestor?(attribute)
locked_ancestor(attribute).present?
end
def locked_by_application_setting?(attribute)
Gitlab::CurrentSettings.public_send("lock_#{attribute}") # rubocop:disable GitlabSecurity/PublicSend -- model attribute, not user input
end
def cascading_attribute_locked?(attribute)
locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
end
def cascading_attribute_changed?(attribute)
public_send("#{attribute}_changed?") # rubocop:disable GitlabSecurity/PublicSend -- model attribute, not user input
end
def cascaded_ancestor_value(attribute)
return unless direct_ancestor_present?
NamespaceSetting
.select(attribute)
.joins(
"join unnest(ARRAY[#{namespace_ancestor_ids_joined}]) with ordinality t(namespace_id, ord) USING (namespace_id)"
)
.where("#{attribute} IS NOT NULL")
.order('t.ord')
.limit(1).first&.read_attribute(attribute)
end
def application_setting_value(attribute)
Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend -- model attribute, not user input
end
def direct_ancestor_present?
project.group.present?
end
def namespace_ancestor_ids_joined
namespace_ancestor_ids.join(',')
end
def namespace_ancestor_ids
project.project_namespace.ancestor_ids(hierarchy_order: :asc)
end
strong_memoize_attr :namespace_ancestor_ids
def to_bool(value)
ActiveModel::Type::Boolean.new.cast(value)
end
end