require 'resolv' class Setting < ApplicationRecord audited :except => [:name, :description, :category, :settings_type, :full_name, :encrypted], :on => [:update] extend FriendlyId friendly_id :name include ActiveModel::Validations include EncryptValue include PermissionName self.inheritance_column = 'category' graphql_type '::Types::Setting' TYPES = %w{integer boolean hash array string} FROZEN_ATTRS = %w{name category} NONZERO_ATTRS = %w{puppet_interval idle_timeout entries_per_page outofsync_interval} BLANK_ATTRS = %w{ host_owner trusted_hosts login_delegation_logout_url root_pass default_location default_organization websockets_ssl_key websockets_ssl_cert oauth_consumer_key oauth_consumer_secret login_text oidc_audience oidc_issuer oidc_algorithm smtp_address smtp_domain smtp_user_name smtp_password smtp_openssl_verify_mode smtp_authentication sendmail_arguments sendmail_location http_proxy http_proxy_except_list default_locale default_timezone ssl_certificate ssl_ca_file ssl_priv_key default_pxe_item_global default_pxe_item_local oidc_jwks_url instance_title } ARRAY_HOSTNAMES = %w{trusted_hosts} URI_ATTRS = %w{foreman_url unattended_url} URI_BLANK_ATTRS = %w{login_delegation_logout_url} IP_ATTRS = %w{libvirt_default_console_address} REGEXP_ATTRS = %w{remote_addr} EMAIL_ATTRS = %w{administrator email_reply_address} NOT_STRIPPED = %w{} class ValueValidator < ActiveModel::Validator def validate(record) record.send("validate_#{record.name}", record) end end validates_lengths_from_database validates :name, :presence => true, :uniqueness => true validates :description, :presence => true validates :default, :presence => true, :unless => proc { |s| s.settings_type == "boolean" || BLANK_ATTRS.include?(s.name) } validates :default, :inclusion => {:in => [true, false]}, :if => proc { |s| s.settings_type == "boolean" } validates :value, :numericality => true, :length => {:maximum => 8}, :if => proc { |s| s.settings_type == "integer" } validates :value, :numericality => {:greater_than => 0}, :if => proc { |s| NONZERO_ATTRS.include?(s.name) } validates :value, :inclusion => {:in => [true, false]}, :if => proc { |s| s.settings_type == "boolean" } validates :value, :presence => true, :if => proc { |s| s.settings_type == "array" && !BLANK_ATTRS.include?(s.name) } validates :settings_type, :inclusion => {:in => TYPES}, :allow_nil => true, :allow_blank => true validates :value, :url_schema => ['http', 'https'], :if => proc { |s| URI_ATTRS.include?(s.name) } validates :value, :url_schema => ['http', 'https'], :if => proc { |s| URI_BLANK_ATTRS.include?(s.name) && s.value.present? } validate :validate_host_owner, :if => proc { |s| s.name == "host_owner" } validates :value, :format => { :with => Resolv::AddressRegex }, :if => proc { |s| IP_ATTRS.include? s.name } validates :value, :regexp => true, :if => proc { |s| REGEXP_ATTRS.include? s.name } validates :value, :array_type => true, :if => proc { |s| s.settings_type == "array" } validates_with ValueValidator, :if => proc { |s| s.respond_to?("validate_#{s.name}") } validates :value, :array_hostnames_ips => true, :if => proc { |s| ARRAY_HOSTNAMES.include? s.name } validates :value, :email => true, :if => proc { |s| EMAIL_ATTRS.include? s.name } before_validation :set_setting_type_from_value before_save :clear_value_when_default before_save :clear_cache validate :validate_frozen_attributes after_find :readonly_when_overridden default_scope -> { order(:name) } # Filer out settings from disabled plugins scope :disabled_plugins, -> { where(:category => descendants.map(&:to_s)) unless Rails.env.development? } scope :order_by, ->(attr) { except(:order).order(attr) } scoped_search :on => :name, :complete_value => :true scoped_search :on => :description, :complete_value => :true def self.config_file 'settings.yaml' end def self.live_descendants disabled_plugins.order_by(:full_name) end def self.stick_general_first sticky_setting = 'Setting::General' (where(:category => sticky_setting) + where.not(:category => sticky_setting)).group_by(&:category) end # can't use our own settings def self.per_page 20 end def self.humanized_category nil end def self.cache_key(name) "settings/#{name}" end def self.[](name) name = name.to_s cache.fetch(cache_key(name)) do find_by(:name => name)&.value end end def self.[]=(name, value) name = name.to_s record = where(:name => name).first! record.value = value record.save! end def self.setting_type_from_value(value_for_type) t = value_for_type.class.to_s.downcase return t if TYPES.include?(t) return "integer" if value_for_type.is_a?(Integer) return "boolean" if value_for_type.is_a?(TrueClass) || value_for_type.is_a?(FalseClass) end def value=(v) v = v.to_yaml unless v.nil? # the has_attribute is for enabling DB migrations on older versions if has_attribute?(:encrypted) && encrypted # Don't re-write the attribute if the current encrypted value is identical to the new one current_value = self[:value] unless is_decryptable?(current_value) && decrypt_field(current_value) == v self[:value] = encrypt_field(v) end else self[:value] = v end end def value v = self[:value] v = decrypt_field(v) v.nil? ? default : YAML.load(v) end alias_method :value_before_type_cast, :value def default d = self[:default] d.nil? ? nil : YAML.load(d) end def default=(v) self[:default] = v.to_yaml end alias_method :default_before_type_cast, :default def parse_string_value(val) case settings_type when "boolean" boolean = Foreman::Cast.to_bool(val) if boolean.nil? invalid_value_error _("must be boolean") return false end self.value = boolean when "integer" if val.to_s =~ /\A\d+\Z/ self.value = val.to_i else invalid_value_error _("must be integer") return false end when "array" if val =~ /\A\[.*\]\Z/ begin self.value = YAML.load(val.gsub(/(\,)(\S)/, "\\1 \\2")) rescue => e invalid_value_error e.to_s return false end else invalid_value_error _("must be an array") return false end when "string", nil # string is taken as default setting type for parsing self.value = NOT_STRIPPED.include?(name) ? val : val.to_s.strip when "hash" raise Foreman::Exception, "parsing hash from string is not supported" else raise Foreman::Exception.new(N_("parsing settings type '%s' from string is not defined"), settings_type) end true end # in order to avoid code duplication, this method was introduced def self.create_find_by_name(opts) # self.name can be set by default scope, e.g. from first_or_create use opts ||= { name: new.name } opts.symbolize_keys! s = Setting.find_by_name(opts[:name].to_s) return create_existing(s, opts) if s column_check(opts) if block_given? yield opts.merge!(value: readonly_value(opts[:name].to_sym) || opts[:value]) end end def self.create(opts) create_find_by_name(opts) { super } end def self.create!(opts) create_find_by_name(opts) { super } end def self.regexp_expand_wildcard_string(string, options = {}) prefix = options[:prefix] || '\A' suffix = options[:suffix] || '\Z' prefix + Regexp.escape(string).gsub('\*', '.*') + suffix end def self.convert_array_to_regexp(array, regexp_options = {}) Regexp.new(array.map { |string| regexp_expand_wildcard_string(string, regexp_options) }.join('|')) end def has_readonly_value? SETTINGS.key?(name.to_sym) end def self.readonly_value(name) SETTINGS[name] end def read_attribute_before_type_cast(attr_name) return value if attr_name == :value super(attr_name) end def self.create_existing(s, opts) bypass_readonly(s) do attrs = column_check([:default, :description, :full_name, :encrypted]) to_update = Hash[opts.select { |k, v| attrs.include? k }] to_update[:value] = readonly_value(s.name.to_sym) if s.has_readonly_value? # default is converted to yaml so we need to convert the yaml here too, # in order to check, if the update of default is needed # if it is the same, we don't try to update default, it would trigger # update query for every setting to_update.delete(:default) if to_update[:default].to_yaml.strip == s[:default] s.attributes = to_update s.save if s.changed? # to bypass name uniqueness validator to query db s.update_column :category, opts[:category] if s.category != opts[:category] s.update_column :full_name, opts[:full_name] if column_check([:full_name]).present? && s.full_name != opts[:full_name] raw_value = s.read_attribute(:value) if s.is_encryptable?(raw_value) && attrs.include?(:encrypted) && opts[:encrypted] s.update_column :value, s.encrypt_field(raw_value) end if s.is_decryptable?(raw_value) && attrs.include?(:encrypted) && !opts[:encrypted] s.update_column :value, s.decrypt_field(raw_value) end end s end def self.bypass_readonly(s, &block) s.instance_variable_set("@readonly", false) if (old_readonly = s.readonly?) yield(s) ensure s.readonly! if old_readonly end def self.cache Rails.cache end # Methods for loading default settings def self.default_settings [] end def self.load_defaults return false unless table_exists? dbcache = Hash[Setting.where(:category => name).map { |s| [s.name, s] }] transaction do default_settings.compact.each do |s| val = s.update(:category => name).symbolize_keys dbcache.key?(val[:name]) ? create_existing(dbcache[val[:name]], s) : create!(s) end end true end def self.select_collection_registry @@select_collection ||= SettingSelectCollection.new end def self.set(name, description, default, full_name = nil, value = nil, options = {}) if options.has_key? :collection select_collection_registry.add(name, options) end options[:encrypted] ||= false {:name => name, :value => value, :description => description, :default => default, :full_name => full_name, :encrypted => options[:encrypted]} end def select_collection self.class.select_collection_registry.collection_for self end def self.model_name ActiveModel::Name.new(Setting) end def self.column_check(opts) opts.keep_if { |k, v| Setting.column_names.include?(k.to_s) } end # End methods for loading default settings private def validate_host_owner owner_type_and_id = value return if owner_type_and_id.blank? owner = OwnerClassifier.new(owner_type_and_id).user_or_usergroup errors.add(:value, _("Host owner is invalid")) if owner.nil? end def invalid_value_error(error) errors.add(:value, _("is invalid: %s") % error) end def set_setting_type_from_value self.settings_type ||= self.class.setting_type_from_value(default) end def validate_frozen_attributes return true if new_record? changed_attributes.each do |c, old| # Allow settings_type to change at first (from nil) since it gets populated during validation if FROZEN_ATTRS.include?(c.to_s) || (c.to_s == :settings_type && !old.nil?) errors.add(c, _("is not allowed to change")) return false end end true end def clear_value_when_default if self[:value] == self[:default] self[:value] = nil end end def clear_cache # Rails cache returns false if the delete failed and nil if the key is missing if Setting.cache.delete(cache_key) == false Rails.logger.warn "Failed to remove #{name} from cache" end end def cache_key Setting.cache_key(name) end def readonly_when_overridden readonly! if !new_record? && has_readonly_value? end end