/app/models/setting.rb
Ruby | 370 lines | 291 code | 64 blank | 15 comment | 59 complexity | 5f412cf1416daeed66d7b7853e565044 MD5 | raw file
- 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