harmonize settings & configuration

They are for now still available as separate entities but that is more due to existing references to them both. Under the hood, they now depend on the
same structure `Settings::Definition` which just as well could have been named `Configuration::Definition`, that defines:
* the name
* the default value
* the type (which might be deferred from the default value)
* the array of allowed values

Both Setting and Configuration can now be overwritten using the same mechanisms:
* Default value
* Database value
* configuration.yml (settings.yml is removed)
* ENV vars
pull/10296/head
ulferts 3 years ago
parent a33524ef6d
commit 0b5575aa64
No known key found for this signature in database
GPG Key ID: A205708DE1284017
  1. 10
      app/helpers/accounts_helper.rb
  2. 34
      app/helpers/settings_helper.rb
  3. 2
      app/models/permitted_params/allowed_settings.rb
  4. 187
      app/models/setting.rb
  5. 4
      app/seeders/basic_data/setting_seeder.rb
  6. 2
      app/seeders/demo_data/project_seeder.rb
  7. 10
      app/views/admin/settings/display_settings/show.html.erb
  8. 2
      config/configuration.yml.example
  9. 318
      config/constants/settings/definition.rb
  10. 935
      config/constants/settings/definitions.rb
  11. 1
      config/initializers/migrate_email_settings.rb
  12. 373
      config/settings.yml
  13. 2
      db/migrate/20211103120946_clean_emails_footer.rb
  14. 7
      lib/api/v3/configuration/configuration_representer.rb
  15. 481
      lib/open_project/configuration.rb
  16. 7
      lib/open_project/configuration/helpers.rb
  17. 13
      lib/open_project/plugins/acts_as_op_engine.rb
  18. 5
      lib/redmine/plugin.rb
  19. 4
      modules/openid_connect/spec/requests/openid_connect_spec.rb
  20. 8
      modules/reporting/lib/open_project/reporting/engine.rb
  21. 53
      modules/reporting/lib/open_project/reporting/patches/open_project/configuration_patch.rb
  22. 13
      modules/reporting/spec/lib/open_project/configuration_spec.rb
  23. 630
      spec/constants/settings/definition_spec.rb
  24. 12
      spec/controllers/admin/settings/projects_settings_controller_spec.rb
  25. 124
      spec/helpers/settings_helper_spec.rb
  26. 561
      spec/lib/open_project/configuration_spec.rb
  27. 7
      spec/models/repository/git_spec.rb
  28. 81
      spec/models/repository/subversion_spec.rb
  29. 45
      spec/models/setting_spec.rb
  30. 12
      spec/views/account/register.html.erb_spec.rb

@ -36,16 +36,10 @@ module AccountsHelper
end
##
# Gets the registration footer in the given language.
# If registration footers are defined via the OpenProject configuration
# then any footers defined via settings will be ignored.
# Gets the registration footer in the given language from the settings.
#
# @param lang [String] ISO 639-1 language code (e.g. 'en', 'de')
def registration_footer_for(lang:)
if footer = OpenProject::Configuration.registration_footer.presence
footer[lang.to_s].presence
else
Setting.registration_footer[lang.to_s].presence
end
Setting.registration_footer[lang.to_s].presence
end
end

@ -75,7 +75,8 @@ module SettingsHelper
setting_label(setting, options) +
wrap_field_outer(options) do
styled_select_tag("settings[#{setting}]",
options_for_select(choices, Setting.send(setting).to_s), options)
options_for_select(choices, Setting.send(setting).to_s),
disabled_setting_option(setting).merge(options))
end
end
@ -85,7 +86,9 @@ module SettingsHelper
hidden_field_tag("settings[#{setting}][]", '') +
choices.map do |choice|
text, value, choice_options = (choice.is_a?(Array) ? choice : [choice, choice])
choice_options = (choice_options || {}).merge(options.except(:id))
choice_options = disabled_setting_option(setting)
.merge(choice_options || {})
.merge(options.except(:id))
choice_options[:id] = "#{setting}_#{value}"
content_tag(:label, class: 'form--label-with-check-box') do
@ -106,13 +109,17 @@ module SettingsHelper
def setting_text_field(setting, options = {})
setting_field_wrapper(setting, options) do
styled_text_field_tag("settings[#{setting}]", Setting.send(setting), options)
styled_text_field_tag("settings[#{setting}]",
Setting.send(setting),
disabled_setting_option(setting).merge(options))
end
end
def setting_number_field(setting, options = {})
setting_field_wrapper(setting, options) do
styled_number_field_tag("settings[#{setting}]", Setting.send(setting), options)
styled_number_field_tag("settings[#{setting}]",
Setting.send(setting),
disabled_setting_option(setting).merge(options))
end
end
@ -151,7 +158,9 @@ module SettingsHelper
value = value.join("\n")
end
styled_text_area_tag("settings[#{setting}]", value, options)
styled_text_area_tag("settings[#{setting}]",
value,
disabled_setting_option(setting).merge(options))
end
end
@ -159,14 +168,19 @@ module SettingsHelper
setting_label(setting, options) +
wrap_field_outer(options) do
tag(:input, type: 'hidden', name: "settings[#{setting}]", value: 0, id: "settings_#{setting}_hidden") +
styled_check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options)
styled_check_box_tag("settings[#{setting}]",
1,
Setting.send("#{setting}?"),
disabled_setting_option(setting).merge(options))
end
end
def setting_password(setting, options = {})
setting_label(setting, options) +
wrap_field_outer(options) do
styled_password_field_tag("settings[#{setting}]", Setting.send(setting), options)
styled_password_field_tag("settings[#{setting}]",
Setting.send(setting),
disabled_setting_option(setting).merge(options))
end
end
@ -219,11 +233,15 @@ module SettingsHelper
unless exceptions.include?(setting)
styled_check_box_tag("settings[#{setting}][]", value,
Setting.send(setting).include?(value),
id: "#{setting}_#{value}")
disabled_setting_option(setting).merge(id: "#{setting}_#{value}"))
end
end
end.join.html_safe
end
end.join.html_safe
end
def disabled_setting_option(setting)
{ disabled: !Setting.send(:"#{setting}_writable?") }
end
end

@ -20,7 +20,7 @@ class PermittedParams
module_function
def all
keys = Setting.available_settings.keys
keys = Setting.definitions.map(&:name)
restrictions.select(&:applicable?).each do |restriction|
restricted_keys = restriction.restricted_keys

@ -30,23 +30,6 @@ class Setting < ApplicationRecord
extend CallbacksHelper
extend Aliases
DATE_FORMATS = [
'%Y-%m-%d',
'%d/%m/%Y',
'%d.%m.%Y',
'%d-%m-%Y',
'%m/%d/%Y',
'%d %b %Y',
'%d %B %Y',
'%b %d, %Y',
'%B %d, %Y'
]
TIME_FORMATS = [
'%H:%M',
'%I:%M %p'
]
ENCODINGS = %w(US-ASCII
windows-1250
windows-1251
@ -85,61 +68,83 @@ class Setting < ApplicationRecord
EUC-KR
Big5
Big5-HKSCS
TIS-620)
TIS-620).freeze
cattr_accessor :available_settings
class << self
def create_setting(name, value = {})
::Settings::Definition.add(name, **value.symbolize_keys)
end
def self.create_setting(name, value = nil)
@@available_settings[name] = value
end
def create_setting_accessors(name)
return if [:installation_uuid].include?(name.to_sym)
def self.create_setting_accessors(name)
return if [:installation_uuid].include?(name.to_sym)
# Defines getter and setter for each setting
# Then setting values can be read using: Setting.some_setting_name
# or set using Setting.some_setting_name = "some value"
src = <<-END_SRC
def self.#{name}
# when running too early, there is no settings table. do nothing
self[:#{name}] if settings_table_exists_yet?
end
# Defines getter and setter for each setting
# Then setting values can be read using: Setting.some_setting_name
# or set using Setting.some_setting_name = "some value"
src = <<-END_SRC
def self.#{name}
# when running too early, there is no settings table. do nothing
self[:#{name}] if settings_table_exists_yet?
end
def self.#{name}?
# when running too early, there is no settings table. do nothing
return unless settings_table_exists_yet?
value = self[:#{name}]
ActiveRecord::Type::Boolean.new.cast(value)
end
def self.#{name}?
# when running too early, there is no settings table. do nothing
return unless settings_table_exists_yet?
value = self[:#{name}]
ActiveRecord::Type::Boolean.new.cast(value)
end
def self.#{name}=(value)
if settings_table_exists_yet?
self[:#{name}] = value
else
logger.warn "Trying to save a setting named '#{name}' while there is no 'setting' table yet. This setting will not be saved!"
nil # when running too early, there is no settings table. do nothing
end
end
def self.#{name}=(value)
if settings_table_exists_yet?
self[:#{name}] = value
else
logger.warn "Trying to save a setting named '#{name}' while there is no 'setting' table yet. This setting will not be saved!"
nil # when running too early, there is no settings table. do nothing
def self.#{name}_writable?
Settings::Definition[:#{name}].writable?
end
END_SRC
class_eval src, __FILE__, __LINE__
end
def definitions
Settings::Definition.all
end
def method_missing(method, *args, &block)
if exists?(accessor_base_name(method))
create_setting_accessors(accessor_base_name(method))
send(method, *args)
else
super
end
END_SRC
class_eval src, __FILE__, __LINE__
end
end
@@available_settings = YAML::load(File.open(Rails.root.join('config/settings.yml')))
def respond_to_missing?(method_name, include_private = false)
exists?(accessor_base_name(method_name)) || super
end
private
# Defines getter and setter for each setting
# Then setting values can be read using: Setting.some_setting_name
# or set using Setting.some_setting_name = "some value"
@@available_settings.each do |name, _params|
create_setting_accessors(name)
def accessor_base_name(name)
name.to_s.sub(/(_writable\?)|(\?)|=\z/, '')
end
end
validates_uniqueness_of :name
validates_inclusion_of :name, in: lambda { |_setting|
@@available_settings.keys
} # lambda, because @available_settings changes at runtime
validates_numericality_of :value, only_integer: true, if: Proc.new { |setting|
@@available_settings[setting.name]['format'] == 'int'
}
validates :name,
inclusion: {
in: ->(*) { Settings::Definition.all.map(&:name) } # @available_settings change at runtime
}
validates :value,
numericality: {
only_integer: true,
if: Proc.new { |setting| setting.format == :integer }
}
def value
self.class.deserialize(name, read_attribute(:value))
@ -152,9 +157,7 @@ class Setting < ApplicationRecord
def formatted_value(value)
return value if value.blank?
default = @@available_settings[name]
if default['serialized']
if config.serialized?
return value.to_yaml
end
@ -163,7 +166,7 @@ class Setting < ApplicationRecord
# Returns the value of the setting named name
def self.[](name)
filtered_cached_or_default(name)
cached_or_default(name)
end
def self.[]=(name, v)
@ -192,7 +195,7 @@ class Setting < ApplicationRecord
# Check whether a setting was defined
def self.exists?(name)
@@available_settings.has_key?(name)
Settings::Definition[name].present?
end
def self.installation_uuid
@ -246,28 +249,25 @@ class Setting < ApplicationRecord
end
# Returns the Setting instance for the setting named name
# and allows to filter the returned value
def self.filtered_cached_or_default(name)
# The setting can come from either
# * The database
# * The cached database value
# * The setting definition
#
# In case the definition is overwritten, e.g. via an ENV var,
# the definition value will always be used.
def self.cached_or_default(name)
name = name.to_s
raise "There's no setting named #{name}" unless exists? name
value = cached_or_default(name)
case name
when "work_package_list_default_highlighting_mode"
value = "none" unless EnterpriseToken.allows_to? :conditional_highlighting
end
value
end
definition = Settings::Definition[name]
# Returns the Setting instance for the setting named name
# (record found in cache or default value)
def self.cached_or_default(name)
name = name.to_s
raise "There's no setting named #{name}" unless exists? name
value = if definition.writable?
cached_settings.fetch(name) { Settings::Definition[name].value }
else
definition.value
end
value = cached_settings.fetch(name) { @@available_settings[name]['default'] }
deserialize(name, value)
end
@ -312,12 +312,12 @@ class Setting < ApplicationRecord
# Unserialize a serialized settings value
def self.deserialize(name, v)
default = @@available_settings[name]
default = Settings::Definition[name]
if default['serialized'] && v.is_a?(String)
if default.serialized? && v.is_a?(String)
YAML::safe_load(v, permitted_classes: [Symbol, ActiveSupport::HashWithIndifferentAccess, Date, Time])
elsif v.present?
read_formatted_setting v, default["format"]
read_formatted_setting v, default.format
else
v
end
@ -325,18 +325,27 @@ class Setting < ApplicationRecord
def self.read_formatted_setting(value, format)
case format
when "boolean"
when :boolean
ActiveRecord::Type::Boolean.new.cast(value)
when "symbol"
when :symbol
value.to_sym
when "int"
when :integer
value.to_i
when "date"
when :date
Date.parse value
when "datetime"
when :datetime
DateTime.parse value
else
value
end
end
protected
def config
@config ||= Settings::Definition[name]
end
delegate :format,
to: :config
end

@ -47,8 +47,8 @@ module BasicData
def data
@settings ||= begin
settings = Setting.available_settings.each_with_object({}) do |(k, v), hash|
hash[k] = v['default'] || ''
settings = Setting.definitions.each_with_object({}) do |definition, hash|
hash[definition.name] = definition.value || ''
end
# deviate from the defaults specified in settings.yml here

@ -67,7 +67,7 @@ module DemoData
seeder.seed!
end
Setting.demo_projects_available = 'true'
Setting.demo_projects_available = true
end
puts ' ↳ Assign groups to projects'

@ -54,11 +54,17 @@ See COPYRIGHT and LICENSE files for more details.
</div>
<div class="form--field">
<%= setting_select :date_format, Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, blank: :label_language_based, container_class: '-slim' %>
<%= setting_select :date_format,
Settings::Definition[:date_format].allowed.collect {|f| [Date.today.strftime(f), f]},
blank: :label_language_based,
container_class: '-slim' %>
</div>
<div class="form--field">
<%= setting_select :time_format, Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, blank: :label_language_based, container_class: '-slim' %>
<%= setting_select :time_format,
Settings::Definition[:time_format].allowed.collect {|f| [Time.now.strftime(f), f]},
blank: :label_language_based,
container_class: '-slim' %>
</div>
<div class="form--field">

@ -213,7 +213,7 @@ default:
# Grace period until uploaded but unassigned (i.e. to a container like work packages,
# wiki pages) attachments are deleted (in minutes)
# attachment_grace_period: 180
# attachments_grace_period: 180
# Configuration of the autologin cookie.
# autologin_cookie_name: the name of the cookie (default: autologin)

@ -0,0 +1,318 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module Settings
class Definition
ENV_PREFIX = 'OPENPROJECT_'.freeze
attr_accessor :name,
:format
attr_writer :value,
:allowed
def initialize(name,
value:,
format: nil,
writable: true,
allowed: nil)
self.name = name.to_s
self.format = format ? format.to_sym : deduce_format(value)
self.value = value
self.writable = writable
self.allowed = allowed
end
def value
return nil if @value.nil?
case format
when :integer
@value.to_i
when :float
@value.to_f
when :boolean
@value.is_a?(Integer) ? ActiveRecord::Type::Boolean.new.cast(@value) : @value
when :symbol
@value.to_sym
else
if @value.respond_to?(:call)
@value.call
else
@value
end
end
end
def serialized?
%i[array hash].include?(format)
end
def writable?
if writable.respond_to?(:call)
writable.call
else
!!writable
end
end
def override_value(other_value)
if format == :hash
value.deep_merge! other_value
else
self.value = other_value
end
raise ArgumentError, "Value for #{name} must be one of #{allowed.join(', ')} but is #{value}" unless valid?
self.writable = false
end
def valid?
!allowed ||
(format == :array && (value - allowed).empty?) ||
allowed.include?(value)
end
def allowed
if @allowed.respond_to?(:call)
@allowed.call
else
@allowed
end
end
class << self
# Adds a setting definition to the set of configured definitions. A definition will define a name and a default value.
# However, that value can be overwritten by (lower tops higher):
# * a value stored in the database (`settings` table)
# * a value in the config/configuration.yml file
# * a value provided by an ENV var
#
# @param [Object] name The name of the definition
# @param [Object] value The default value the setting has if not overwritten.
# @param [nil] format The format the value is in e.g. symbol, array, hash, string. If a value is present,
# the format is deferred.
# @param [TrueClass] writable Whether the value can be set in the UI. In case the value is set via file or ENV var,
# this will be set to false later on and UI elements that refer to the definition will be disabled.
# @param [nil] allowed The array of allowed values that can be assigned to the definition.
# Will serve to be validated against. A lambda can be provided returning an array in case
# the array needs to be evaluated dynamically. In case of e.g. boolean format, setting
# an allowed array is not necessary.
def add(name,
value:,
format: nil,
writable: true,
allowed: nil)
return if @by_name.present? && @by_name[name.to_s].present?
@by_name = nil
all << new(name,
format: format,
value: value,
writable: writable,
allowed: allowed)
end
def define(&block)
instance_exec(&block)
end
def [](name)
by_name ||= all.group_by(&:name).transform_values(&:first)
by_name[name.to_s]
end
def exists?(name)
by_name.keys.include?(name.to_s)
end
def all_of_prefix(prefix)
all.select { |definition| definition.name.start_with?(prefix) }
end
def all
@all ||= []
unless loaded
self.loaded = true
load './config/constants/settings/definitions.rb'
# The test setup should govern the configuration
load_config_from_file unless Rails.env.test?
override_config
end
@all
end
private
# Currently only required for testing
def reset
@all = nil
@loaded = false
@by_name = nil
end
def by_name
@by_name ||= all.group_by(&:name).transform_values(&:first)
end
def load_config_from_file
filename = Rails.root.join('config/configuration.yml')
if File.file?(filename)
file_config = YAML.load_file(filename)
if file_config.is_a? Hash
load_env_from_config(file_config, Rails.env)
else
warn "#{filename} is not a valid OpenProject configuration file, ignoring."
end
end
end
def load_env_from_config(config, env)
config['default']&.each do |name, value|
self[name]&.override_value(value)
end
config[env]&.each do |name, value|
self[name]&.override_value(value)
end
end
# Replace config values for which an environment variable with the same key in upper case
# exists.
# Also merges the existing values that are hashes with values from ENV if they follow the naming
# schema.
def override_config(source = default_override_source)
override_config_values(source)
merge_hash_config(source)
end
def override_config_values(source)
all
.map(&:name)
.select { |key| source.include? key.upcase }
.each { |key| self[key].override_value(extract_value(key, source[key.upcase])) }
end
def merge_hash_config(source, prefix: ENV_PREFIX)
source.select { |k, _| k =~ /^#{prefix}/i }.each do |k, raw_value|
name, value = path_to_hash(*path(prefix, k),
extract_value(k, raw_value))
.first
# There might be ENV vars that match the OPENPROJECT_ prefix but are no OP instance
# settings, e.g. OPENPROJECT_DISABLE_DEV_ASSET_PROXY
self[name]&.override_value(value)
end
end
def path(prefix, env_var_name)
env_var_name
.sub(/^#{prefix}/, '')
.gsub(/([a-zA-Z0-9]|(__))+/)
.map do |seg|
unescape_underscores(seg.downcase)
end
end
# takes the path provided and transforms it into a deeply nested hash
# where the last parameter becomes the value.
#
# e.g. path_to_hash(:a, :b, :c, :d) => { a: { b: { c: :d } } }
def path_to_hash(*path)
value = path.pop
path.reverse.inject(value) do |path_hash, key|
{ key => path_hash }
end
end
def unescape_underscores(path_segment)
path_segment.gsub '__', '_'
end
##
# Extract the configuration value from the given input
# using YAML.
#
# @param key [String] The key of the input within the source hash.
# @param original_value [String] The string from which to extract the actual value.
# @return A ruby object (e.g. Integer, Float, String, Hash, Boolean, etc.)
# @raise [ArgumentError] If the string could not be parsed.
def extract_value(key, original_value)
# YAML parses '' as false, but empty ENV variables will be passed as that.
# To specify specific values, one can use !!str (-> '') or !!null (-> nil)
return original_value if original_value == ''
parsed = YAML.safe_load(original_value)
if parsed.is_a?(String)
original_value
else
parsed
end
rescue StandardError => e
raise ArgumentError, "Configuration value for '#{key}' is invalid: #{e.message}"
end
##
# The default source for overriding configuration values
# is ENV, but may be changed for testing purposes
def default_override_source
ENV
end
attr_accessor :loaded
end
private
attr_accessor :serialized,
:writable
def deduce_format(value)
case value
when TrueClass, FalseClass
:boolean
when Integer, Date, DateTime, String, Hash, Array, Float, Symbol
value.class.name.underscore.to_sym
when ActiveSupport::Duration
:duration
else
raise ArgumentError, "Cannot deduce the format for the setting definition #{name}"
end
end
end
end

@ -0,0 +1,935 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
# The list of all defined settings that however might get extended e.g. in modules/plugins
# Contains what was formerly defined in both the settings.yml as well as in configuration.rb.
# Values from the later can typically be identified by having
# writable: false
# set. That would not be strictly necessary since they currently don't have a UI anyway and
# that is where the value is actually used.
Settings::Definition.define do
add :activity_days_default,
value: 30
add :additional_footer_content,
format: :string,
value: nil
add :after_first_login_redirect_url,
format: :string,
value: nil,
writable: false
add :after_login_default_redirect_url,
format: :string,
value: nil,
writable: false
add :apiv3_cors_enabled,
value: false
add :apiv3_cors_origins,
value: []
add :apiv3_docs_enabled,
value: true
add :apiv3_enable_basic_auth,
value: true,
writable: false
add :apiv3_max_page_size,
value: 1000
add :app_title,
value: 'OpenProject'
add :attachment_max_size,
value: 5120
# Existing setting
add :attachment_whitelist,
value: []
##
# Carrierwave storage type. Possible values are, among others, :file and :fog.
# The latter requires further configuration.
add :attachments_storage,
value: :file,
format: :symbol,
allowed: %i[file fog],
writable: false
add :attachments_storage_path,
format: :string,
value: nil,
writable: false
add :attachments_grace_period,
value: 180,
writable: false
add :auth_source_sso,
format: :string,
value: nil,
writable: false
add :autofetch_changesets,
value: true
# autologin duration in days
# 0 means autologin is disabled
add :autologin,
value: 0
add :autologin_cookie_name,
value: 'autologin',
writable: false
add :autologin_cookie_path,
value: '/',
writable: false
add :autologin_cookie_secure,
value: false,
writable: false
add :available_languages,
format: :hash,
value: %w[en de fr es pt pt-BR it zh-CN ko ru].freeze,
allowed: -> { Redmine::I18n.all_languages }
add :avatar_link_expiry_seconds,
value: 24.hours.to_i,
writable: false
# Allow users with the required permissions to create backups via the web interface or API.
add :backup_enabled,
value: true,
writable: false
add :backup_daily_limit,
value: 3,
writable: false
add :backup_initial_waiting_period,
value: 24.hours,
format: :integer,
writable: false
add :backup_include_attachments,
value: true,
writable: false
add :backup_attachment_size_max_sum_mb,
value: 1024,
writable: false
add :blacklisted_routes,
value: [],
writable: false
add :bcc_recipients,
value: true
add :boards_demo_data_available,
value: false
add :brute_force_block_minutes,
value: 30
add :brute_force_block_after_failed_logins,
value: 20
add :cache_expires_in_seconds,
format: :integer,
value: nil,
writable: false
add :cache_formatted_text,
value: true
# use dalli defaults for memcache
add :cache_memcache_server,
format: :string,
value: nil,
writable: false
add :cache_namespace,
format: :string,
value: nil,
writable: false
add :commit_fix_done_ratio,
value: 100
add :commit_fix_keywords,
value: 'fixes,closes'
add :commit_fix_status_id,
format: :integer,
value: nil,
allowed: -> { Status.pluck(:id) + [nil] }
# encoding used to convert commit logs to UTF-8
add :commit_logs_encoding,
value: 'UTF-8'
add :commit_logtime_activity_id,
format: :integer,
value: nil,
allowed: -> { TimeEntryActivity.pluck(:id) + [nil] }
add :commit_logtime_enabled,
value: false
add :commit_ref_keywords,
value: 'refs,references,IssueID'
add :consent_decline_mail,
format: :string,
value: nil
# Time after which users have to have consented to what ever they need to consent
# to (depending on other settings) such as a privacy policy.
add :consent_time,
value: nil,
format: :datetime
# Additional info about what the user is consenting to (optional).
add :consent_info,
value: {
en: "## Consent\n\nYou need to agree to the [privacy and security policy]" +
"(https://www.openproject.org/data-privacy-and-security/) of this OpenProject instance."
}
# Indicates whether or not users need to consent to something such as privacy policy.
add :consent_required,
value: false
add :cross_project_work_package_relations,
value: true
# Allow in-context translations to be loaded with CSP
add :crowdin_in_context_translations,
value: true,
writable: false
add :database_cipher_key,
format: :string,
value: nil,
writable: false
add :date_format,
format: :string,
value: nil,
allowed: [
'%Y-%m-%d',
'%d/%m/%Y',
'%d.%m.%Y',
'%d-%m-%Y',
'%m/%d/%Y',
'%d %b %Y',
'%d %B %Y',
'%b %d, %Y',
'%B %d, %Y'
].freeze
add :default_auto_hide_popups,
value: true
# user configuration
add :default_comment_sort_order,
value: 'asc',
writable: false
add :default_language,
value: 'en'
add :default_projects_modules,
value: %w[calendar board_view work_package_tracking news costs wiki],
allowed: -> { OpenProject::AccessControl.available_project_modules.map(&:to_s) }
add :default_projects_public,
value: false
add :demo_projects_available,
value: false
add :diff_max_lines_displayed,
value: 1500
# only applicable in conjunction with fog (effectively S3) attachments
# which will be uploaded directly to the cloud storage rather than via OpenProject's
# server process.
add :direct_uploads,
value: true,
writable: false
add :disable_browser_cache,
value: true,
writable: false
# allow to disable default modules
add :disabled_modules,
value: [],
writable: false
add :disable_password_choice,
value: false,
writable: false
add :disable_password_login,
value: false,
writable: false
add :display_subprojects_work_packages,
value: true
# Destroy all sessions for current_user on logout
add :drop_old_sessions_on_logout,
value: true,
writable: false
# Destroy all sessions for current_user on login
add :drop_old_sessions_on_login,
value: false,
writable: false
add :edition,
format: :string,
value: 'standard',
writable: false,
allowed: %w[standard bim]
add :ee_manager_visible,
value: true,
writable: false
# Enable internal asset server
add :enable_internal_assets_server,
value: false,
writable: false
# email configuration
add :email_delivery_configuration,
value: 'inapp',
allowed: %w[inapp legacy],
writable: false
add :email_delivery_method,
format: :symbol,
value: nil
add :emails_footer,
value: {
'en' => ''
}
add :emails_header,
value: {
'en' => ''
}
# use email address as login, hide login in registration form
add :email_login,
value: false
add :enabled_projects_columns,
value: %w[project_status public created_at latest_activity_at required_disk_space],
allowed: -> { Projects::TableCell.new(nil, current_user: User.admin.first).all_columns.map(&:first).map(&:to_s) }
add :enabled_scm,
value: %w[subversion git]
# Allow connections for trial creation and booking
add :enterprise_trial_creation_host,
value: 'https://augur.openproject.com',
writable: false
add :enterprise_chargebee_site,
value: 'openproject-enterprise',
writable: false
add :enterprise_plan,
value: 'enterprise-on-premises---euro---1-year',
writable: false
add :feeds_enabled,
value: true
add :feeds_limit,
value: 15
# Maximum size of files that can be displayed
# inline through the file viewer (in KB)
add :file_max_size_displayed,
value: 512
add :first_week_of_year,
value: nil,
format: :integer,
allowed: [1, 4]
# Configure fog, e.g. when using an S3 uploader
add :fog,
value: {}
add :fog_download_url_expires_in,
value: 21600, # 6h by default as 6 hours is max in S3 when using IAM roles
writable: false
# Additional / overridden help links
add :force_help_link,
format: :string,
value: nil,
writable: false
add :force_formatting_help_link,
format: :string,
value: nil,
writable: false
add :forced_single_page_size,
value: 250
add :host_name,
value: "localhost:3000"
# Health check configuration
add :health_checks_authentication_password,
format: :string,
value: nil,
writable: false
# Maximum number of backed up jobs (that are not yet executed)
# before health check fails
add :health_checks_jobs_queue_count_threshold,
format: :integer,
value: 50,
writable: false
## Maximum number of minutes that jobs have not yet run after their designated 'run_at' time
add :health_checks_jobs_never_ran_minutes_ago,
format: :integer,
value: 5,
writable: false
## Maximum number of unprocessed requests in puma's backlog.
add :health_checks_backlog_threshold,
format: :integer,
value: 20,
writable: false
# Default gravatar image, set to something other than 404
# to ensure a default is returned
add :gravatar_fallback_image,
value: '404',
writable: false
add :hidden_menu_items,
value: {},
writable: false
# Impressum link to be set, nil by default (= hidden)
add :impressum_link,
format: :string,
value: nil,
writable: false
add :installation_type,
value: 'manual',
writable: false
add :installation_uuid,
format: :string,
value: nil
add :internal_password_confirmation,
value: true,
writable: false
add :invitation_expiration_days,
value: 7
add :journal_aggregation_time_minutes,
value: 5
# Allow override of LDAP options
add :ldap_force_no_page,
format: :string,
value: nil,
writable: false
add :ldap_auth_source_tls_options,
format: :string,
value: nil,
writable: false
# Allow users to manually sync groups in a different way
# than the provided job using their own cron
add :ldap_groups_disable_sync_job,
value: false,
writable: false
add :log_level,
value: 'info',
writable: false
add :log_requesting_user,
value: false
# Use lograge to format logs, off by default
add :lograge_formatter,
value: nil,
format: :string,
writable: false
add :login_required,
value: false
add :lost_password,
value: true
add :mail_from,
value: 'openproject@example.net'
add :mail_handler_api_key,
format: :string,
value: nil
add :mail_handler_body_delimiters,
value: ''
add :mail_handler_body_delimiter_regex,
value: ''
add :mail_handler_ignore_filenames,
value: 'signature.asc'
add :mail_suffix_separators,
value: '+'
add :main_content_language,
value: 'english',
writable: false
# Check for missing migrations in internal errors
add :migration_check_on_exceptions,
value: true,
writable: false
# Role given to a non-admin user who creates a project
add :new_project_user_role_id,
format: :integer,
value: nil,
allowed: -> { Role.pluck(:id) }
add :oauth_allow_remapping_of_existing_users,
value: false
add :omniauth_direct_login_provider,
format: :string,
value: nil,
writable: false
add :override_bcrypt_cost_factor,
format: :string,
value: nil,
writable: false
add :notification_retention_period_days,
value: 30
add :notification_email_delay_minutes,
value: 15
add :notification_email_digest_time,
value: '08:00'
add :onboarding_video_url,
value: 'https://player.vimeo.com/video/163426858?autoplay=1',
writable: false
add :onboarding_enabled,
value: true,
writable: false
add :password_active_rules,
value: %w[lowercase uppercase numeric special],
allowed: %w[lowercase uppercase numeric special]
add :password_count_former_banned,
value: 0
add :password_days_valid,
value: 0
add :password_min_length,
value: 10
add :password_min_adhered_rules,
value: 0
# TODO: turn into array of ints
# Requires a migration to be written
# replace Setting#per_page_options_array
add :per_page_options,
value: '20, 100'
add :plain_text_mail,
value: false
add :protocol,
value: "http",
allowed: %w[http https]
add :project_gantt_query,
value: nil,
format: :string
add :rails_asset_host,
format: :string,
value: nil,
writable: false
add :rails_cache_store,
format: :symbol,
value: :file_store,
writable: false,
allowed: %i[file_store memcache]
# url-path prefix
add :rails_relative_url_root,
value: '',
writable: false
add :rails_force_ssl,
value: false,
writable: false
add :registration_footer,
value: {
'en' => ''
},
writable: false
add :repositories_automatic_managed_vendor,
value: nil,
format: :string,
allowed: -> { OpenProject::SCM::Manager.registered.keys.map(&:to_s) }
# encodings used to convert repository files content to UTF-8
# multiple values accepted, comma separated
add :repositories_encodings,
value: nil,
format: :string
add :repository_authentication_caching_enabled,
value: true
add :repository_checkout_data,
value: {
"git" => { "enabled" => 0 },
"subversion" => { "enabled" => 0 }
}
add :repository_log_display_limit,
value: 100
add :repository_storage_cache_minutes,
value: 720
add :repository_truncate_at,
value: 500
add :rest_api_enabled,
value: true
add :scm,
format: :hash,
value: {},
writable: false
add :scm_git_command,
format: :string,
value: nil,
writable: false
add :scm_local_checkout_path,
value: 'repositories', # relative to OpenProject directory
writable: false
add :scm_subversion_command,
format: :string,
value: nil,
writable: false
# Display update / security badge, enabled by default
add :security_badge_displayed,
value: true,
writable: false
add :security_badge_url,
value: "https://releases.openproject.com/v1/check.svg",
writable: false
add :self_registration,
value: 2
add :sendmail_arguments,
format: :string,
value: "-i",
writable: false
add :sendmail_location,
format: :string,
value: "/usr/sbin/sendmail",
writable: false
# Which breadcrumb loggers to enable
add :sentry_breadcrumb_loggers,
value: ['active_support_logger'],
writable: false
# Log errors to sentry instance
add :sentry_dsn,
format: :string,
value: nil,
writable: false
# Allow separate error reporting for frontend errors
add :sentry_frontend_dsn,
format: :string,
value: nil,
writable: false
add :sentry_host,
format: :string,
value: nil,
writable: false
# Allow sentry to collect tracing samples
# set to 1 to enable default tracing samples (see sentry initializer)
# set to n >= 1 to enable n times the default tracing
add :sentry_trace_factor,
value: 0,
writable: false
# Allow sentry to collect tracing samples on frontend
# set to n >= 1 to enable n times the default tracing
add :sentry_frontend_trace_factor,
value: 0,
writable: false
add :session_cookie_name,
value: '_open_project_session',
writable: false
# where to store session data
add :session_store,
value: :active_record_store,
writable: false
add :session_ttl_enabled,
value: false
add :session_ttl,
value: 120
add :show_community_links,
value: true,
writable: false
# Show pending migrations as warning bar
add :show_pending_migrations_warning,
value: true,
writable: false
# Show mismatched protocol/hostname warning
# in settings where they must differ this can be disabled
add :show_setting_mismatch_warning,
value: true,
writable: false
# Render storage information
add :show_storage_information,
value: true,
writable: false
# Render warning bars (pending migrations, deprecation, unsupported browsers)
# Set to false to globally disable this for all users!
add :show_warning_bars,
value: true,
writable: false
add :smtp_enable_starttls_auto,
format: :boolean,
value: false
add :smtp_openssl_verify_mode,
format: :string,
value: "none",
allowed: %w[none peer client_once fail_if_no_peer_cert],
writable: false
add :smtp_ssl,
format: :boolean,
value: false
add :smtp_address,
format: :string,
value: ''
add :smtp_domain,
format: :string,
value: 'your.domain.com'
add :smtp_user_name,
format: :string,
value: ''
add :smtp_port,
format: :integer,
value: 587
add :smtp_password,
format: :string,
value: ''
add :smtp_authentication,
format: :string,
value: 'plain',
writable: false
add :software_name,
value: 'OpenProject'
add :software_url,
value: 'https://www.openproject.org/'
# Slow query logging threshold in ms
add :sql_slow_query_threshold,
value: 2000,
writable: false
add :start_of_week,
value: nil,
format: :integer,
allowed: [1, 6, 7]
# enable statsd metrics (currently puma only) by configuring host
add :statsd,
value: {
'host' => nil,
'port' => 8125
},
writable: false
add :sys_api_enabled,
value: false
add :sys_api_key,
value: nil,
format: :string
add :time_format,
format: :string,
value: nil,
allowed: [
'%H:%M',
'%I:%M %p'
].freeze
add :user_default_timezone,
value: nil,
format: :string,
allowed: ActiveSupport::TimeZone.all + [nil]
add :users_deletable_by_admins,
value: false
add :users_deletable_by_self,
value: false
add :user_format,
value: :firstname_lastname,
allowed: -> { User::USER_FORMATS_STRUCTURE.keys }
add :web,
value: {
'workers' => 2,
'timeout' => 120,
'wait_timeout' => 10,
'min_threads' => 4,
'max_threads' => 16
},
writable: false
add :welcome_text,
format: :string,
value: nil
add :welcome_title,
format: :string,
value: nil
add :welcome_on_homescreen,
value: false
add :work_package_done_ratio,
value: 'field',
allowed: %w[field status disabled]
add :work_packages_export_limit,
value: 500
add :work_package_list_default_highlighted_attributes,
value: [],
allowed: -> {
Query.available_columns(nil).select(&:highlightable).map(&:name).map(&:to_s)
}
add :work_package_list_default_highlighting_mode,
format: :string,
value: -> { EnterpriseToken.allows_to?(:conditional_highlighting) ? 'inline' : 'none' },
allowed: -> { Query::QUERY_HIGHLIGHTING_MODES },
writable: -> { EnterpriseToken.allows_to?(:conditional_highlighting) }
add :work_package_list_default_columns,
value: %w[id subject type status assigned_to priority],
allowed: -> { Query.new.available_columns.map(&:name).map(&:to_s) }
add :work_package_startdate_is_adddate,
value: false
add :youtube_channel,
value: 'https://www.youtube.com/c/OpenProjectCommunity',
writable: false
end

@ -3,7 +3,6 @@ OpenProject::Application.configure do
# settings table may not exist when you run db:migrate at installation
# time, so just ignore this block when that happens.
if Setting.settings_table_exists_yet?
OpenProject::Configuration.load
OpenProject::Configuration.migrate_mailer_configuration!
OpenProject::Configuration.reload_mailer_configuration!
end

@ -1,373 +0,0 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
# DO NOT MODIFY THIS FILE !!!
# Settings can be defined through the application in Admin -> Settings
email_delivery_method:
default:
format: 'symbol'
sendmail_location:
default: "/usr/sbin/sendmail"
sendmail_arguments:
default: "-i"
smtp_openssl_verify_mode:
default: "none"
smtp_enable_starttls_auto:
default: 0
format: boolean
smtp_ssl:
default: 0
format: boolean
smtp_address:
default: ""
smtp_port:
default: 587
format: int
smtp_domain:
default: "your.domain.com"
smtp_authentication:
default: "plain"
smtp_user_name:
default: ""
smtp_password:
default: ""
additional_footer_content:
default: ''
# The instance name
app_title:
default: OpenProject
app_subtitle:
default: Project management
brute_force_block_minutes:
default: 30
format: int
brute_force_block_after_failed_logins:
default: 20
format: int
# Time after which users have to have consented to what ever they need to consent
# to (depending on other settings) such as a privacy policy.
consent_time:
format: datetime
# Additional info about what the user is consenting to (optional).
consent_info:
serialized: true
default:
en: "## Consent\n\nYou need to agree to the [privacy and security policy](https://www.openproject.org/data-privacy-and-security/) of this OpenProject instance."
# Indicates whether or not users need to consent to something such as privacy policy.
consent_required:
default: 0
format: boolean
consent_decline_mail:
default:
welcome_title:
default:
welcome_text:
default:
welcome_on_homescreen:
default: 0
format: boolean
log_requesting_user:
default: 0
format: int
login_required:
default: 0
self_registration:
default: '2'
lost_password:
default: 1
format: int
password_min_length:
format: int
default: 10
password_active_rules:
serialized: true
default:
- lowercase
- uppercase
- numeric
- special
password_min_adhered_rules:
format: int
default: 0
password_count_former_banned:
format: int
default: 0
password_days_valid:
format: int
default: 0
software_name:
default: OpenProject
software_url:
default: 'https://www.openproject.org/'
attachment_max_size:
format: int
default: 5120
attachment_whitelist:
serialized: true
default: []
work_packages_export_limit:
format: int
default: 500
activity_days_default:
format: int
default: 30
per_page_options:
default: '20, 100'
forced_single_page_size:
default: 250
mail_from:
default: openproject@example.net
bcc_recipients:
default: 1
plain_text_mail:
default: 0
cache_formatted_text:
default: 0
wiki_compression:
default: ""
available_languages:
serialized: true
default:
- en
- de
- fr
- es
- pt
- 'pt-BR'
- it
- 'zh-CN'
- ko
- ru
default_language:
default: en
default_auto_hide_popups:
default: 1
format: boolean
email_login: # use email address as login, hide login in registration form
default: 0
host_name:
default: localhost:3000
protocol:
default: http
feeds_enabled:
default: 1
feeds_limit:
format: int
default: 15
# Maximum size of files that can be displayed
# inline through the file viewer (in KB)
file_max_size_displayed:
format: int
default: 512
diff_max_lines_displayed:
format: int
default: 1500
enabled_scm:
serialized: true
default:
- subversion
- git
autofetch_changesets:
default: 1
sys_api_enabled:
default: 0
sys_api_key:
default: ''
repository_authentication_caching_enabled:
default: 1
repositories_automatic_managed_vendor:
default: ''
commit_ref_keywords:
default: 'refs,references,IssueID'
commit_fix_keywords:
default: 'fixes,closes'
commit_fix_status_id:
format: int
default: 0
commit_fix_done_ratio:
default: 100
commit_logtime_enabled:
default: 0
commit_logtime_activity_id:
format: int
default: 0
# autologin duration in days
# 0 means autologin is disabled
autologin:
format: int
default: 0
# date format
date_format:
default: ''
time_format:
default: ''
user_format:
default: :firstname_lastname
format: symbol
cross_project_work_package_relations:
default: 1
format: boolean
mail_handler_body_delimiters:
default: ''
mail_handler_ignore_filenames:
default: 'signature.asc'
mail_handler_body_delimiter_regex:
default: ''
mail_handler_api_key:
default:
mail_suffix_separators:
default: '+'
work_package_list_default_columns:
serialized: true
default:
- id
- subject
- type
- status
- assigned_to
- priority
work_package_list_default_highlighting_mode:
default: 'inline'
work_package_list_default_highlighted_attributes:
serialized: true
default: []
display_subprojects_work_packages:
default: 1
work_package_done_ratio:
default: 'field'
default_projects_public:
default: 0
default_projects_modules:
serialized: true
default:
- calendar
- board_view
- work_package_tracking
- news
- costs
- wiki
enabled_projects_columns:
serialized: true
default:
- project_status
- public
- created_at
- latest_activity_at
- required_disk_space
project_gantt_query:
default: ''
# Role given to a non-admin user who creates a project
new_project_user_role_id:
format: int
default: ''
# encodings used to convert repository files content to UTF-8
# multiple values accepted, comma separated
repositories_encodings:
default: ''
# encoding used to convert commit logs to UTF-8
commit_logs_encoding:
default: 'UTF-8'
repository_log_display_limit:
format: int
default: 100
emails_footer:
serialized: true
default: {}
start_of_week:
default: ''
first_week_of_year:
default: ''
rest_api_enabled:
default: 1
session_ttl_enabled:
default: 0
session_ttl:
format: int
default: 120
emails_header:
serialized: true
default:
en: ''
work_package_startdate_is_adddate:
default: 0
format: boolean
user_default_timezone:
default: ""
users_deletable_by_admins:
default: 0
users_deletable_by_self:
default: 0
invitation_expiration_days:
default: 7
format: int
journal_aggregation_time_minutes:
default: 5
format: int
registration_footer:
serialized: true
default:
en:
repository_storage_cache_minutes:
default: 720
format: int
repository_truncate_at:
default: 500
format: int
repository_checkout_data:
serialized: true
default:
git:
enabled: 0
subversion:
enabled: 0
demo_projects_available:
default: false
boards_demo_data_available:
default: false
security_badge_displayed:
default: true
installation_uuid:
default: null
oauth_allow_remapping_of_existing_users:
default: false
format: boolean
apiv3_cors_enabled:
default: false
format: boolean
apiv3_cors_origins:
serialized: true
default: []
apiv3_docs_enabled:
default: true
format: boolean
apiv3_max_page_size:
default: 1000
notification_retention_period_days:
default: 30
format: int

@ -1,5 +1,7 @@
class CleanEmailsFooter < ActiveRecord::Migration[6.1]
def up
return unless Setting.exists?(name: 'emails_footer')
Setting.reset_column_information
filtered_footer = Setting
.emails_footer

@ -134,9 +134,10 @@ module API
end
def reformated(setting, &block)
format = setting.gsub(/%\w/, &block)
format.blank? ? nil : format
setting
.to_s
.gsub(/%\w/, &block)
.presence
end
end
end

@ -33,296 +33,15 @@ module OpenProject
module Configuration
extend Helpers
ENV_PREFIX ||= 'OPENPROJECT_'.freeze
# Configuration default values
@defaults = {
'edition' => 'standard',
'attachments_storage' => 'file',
'attachments_storage_path' => nil,
'attachments_grace_period' => 180,
'autologin_cookie_name' => 'autologin',
'autologin_cookie_path' => '/',
'autologin_cookie_secure' => false,
# Allow users with the required permissions to create backups via the web interface or API.
'backup_enabled' => true,
'backup_daily_limit' => 3,
'backup_initial_waiting_period' => 24.hours,
'backup_include_attachments' => true,
'backup_attachment_size_max_sum_mb' => 1024,
'database_cipher_key' => nil,
# only applicable in conjunction with fog (effectively S3) attachments
# which will be uploaded directly to the cloud storage rather than via OpenProject's
# server process.
'direct_uploads' => true,
'fog_download_url_expires_in' => 21600, # 6h by default as 6 hours is max in S3 when using IAM roles
'show_community_links' => true,
'log_level' => 'info',
'scm_git_command' => nil,
'scm_subversion_command' => nil,
'scm_local_checkout_path' => 'repositories', # relative to OpenProject directory
'disable_browser_cache' => true,
# default cache_store is :file_store in production and :memory_store in development
'rails_cache_store' => nil,
'cache_expires_in_seconds' => nil,
'cache_namespace' => nil,
# use dalli defaults for memcache
'cache_memcache_server' => nil,
# where to store session data
'session_store' => :active_record_store,
'session_cookie_name' => '_open_project_session',
# Destroy all sessions for current_user on logout
'drop_old_sessions_on_logout' => true,
# Destroy all sessions for current_user on login
'drop_old_sessions_on_login' => false,
# url-path prefix
'rails_relative_url_root' => '',
'rails_force_ssl' => false,
'rails_asset_host' => nil,
# Enable internal asset server
'enable_internal_assets_server' => false,
# Additional / overridden help links
'force_help_link' => nil,
'force_formatting_help_link' => nil,
# Impressum link to be set, nil by default (= hidden)
'impressum_link' => nil,
# user configuration
'default_comment_sort_order' => 'asc',
# email configuration
'email_delivery_configuration' => 'inapp',
'email_delivery_method' => nil,
'smtp_address' => nil,
'smtp_port' => nil,
'smtp_domain' => nil, # HELO domain
'smtp_authentication' => nil,
'smtp_user_name' => nil,
'smtp_password' => nil,
'smtp_enable_starttls_auto' => nil,
'smtp_openssl_verify_mode' => nil, # 'none', 'peer', 'client_once' or 'fail_if_no_peer_cert'
'sendmail_location' => '/usr/sbin/sendmail',
'sendmail_arguments' => '-i',
'disable_password_login' => false,
'auth_source_sso' => nil,
'omniauth_direct_login_provider' => nil,
'internal_password_confirmation' => true,
'disable_password_choice' => false,
'override_bcrypt_cost_factor' => nil,
'disabled_modules' => [], # allow to disable default modules
'hidden_menu_items' => {},
'blacklisted_routes' => [],
'apiv3_enable_basic_auth' => true,
'onboarding_video_url' => 'https://player.vimeo.com/video/163426858?autoplay=1',
'onboarding_enabled' => true,
'youtube_channel' => 'https://www.youtube.com/c/OpenProjectCommunity',
'ee_manager_visible' => true,
# Health check configuration
'health_checks_authentication_password' => nil,
# Maximum number of backed up jobs (that are not yet executed)
# before health check fails
'health_checks_jobs_queue_count_threshold' => 50,
# Maximum number of minutes that jobs have not yet run after their designated 'run_at' time
'health_checks_jobs_never_ran_minutes_ago' => 5,
# Maximum number of unprocessed requests in puma's backlog.
'health_checks_backlog_threshold' => 20,
'after_login_default_redirect_url' => nil,
'after_first_login_redirect_url' => nil,
'main_content_language' => 'english',
# Allow in-context translations to be loaded with CSP
'crowdin_in_context_translations' => true,
'avatar_link_expiry_seconds' => 24.hours.to_i,
# Default gravatar image, set to something other than 404
# to ensure a default is returned
'gravatar_fallback_image' => '404',
'registration_footer' => {},
# Display update / security badge, enabled by default
'security_badge_displayed' => true,
'installation_type' => "manual",
'security_badge_url' => "https://releases.openproject.com/v1/check.svg",
# Check for missing migrations in internal errors
'migration_check_on_exceptions' => true,
# Show pending migrations as warning bar
'show_pending_migrations_warning' => true,
# Show mismatched protocol/hostname warning
# in settings where they must differ this can be disabled
'show_setting_mismatch_warning' => true,
# Render warning bars (pending migrations, deprecation, unsupported browsers)
# Set to false to globally disable this for all users!
'show_warning_bars' => true,
# Render storage information
'show_storage_information' => true,
# Log errors to sentry instance
'sentry_dsn' => nil,
# Allow separate error reporting for frontend errors
'sentry_frontend_dsn' => nil,
'sentry_host' => nil,
# Allow sentry to collect tracing samples
# set to 1 to enable default tracing samples (see sentry initializer)
# set to n >= 1 to enable n times the default tracing
'sentry_trace_factor' => 0,
# Allow sentry to collect tracing samples on frontend
# set to n >= 1 to enable n times the default tracing
'sentry_frontend_trace_factor' => 0,
# Which breadcrumb loggers to enable
'sentry_breadcrumb_loggers' => ['active_support_logger'],
# enable statsd metrics (currently puma only) by configuring host
'statsd' => {
'host' => nil,
'port' => 8125
},
# Allow connections for trial creation and booking
'enterprise_trial_creation_host' => 'https://augur.openproject.com',
'enterprise_chargebee_site' => 'openproject-enterprise',
'enterprise_plan' => 'enterprise-on-premises---euro---1-year',
# Allow override of LDAP options
'ldap_auth_source_tls_options' => nil,
'ldap_force_no_page' => false,
# Allow users to manually sync groups in a different way
# than the provided job using their own cron
'ldap_groups_disable_sync_job' => false,
# Slow query logging threshold in ms
'sql_slow_query_threshold' => 2000,
# Use lograge to format logs, off by default
'lograge_formatter' => nil,
'web' => {
'workers' => 2,
'timeout' => 120,
'wait_timeout' => 10,
'min_threads' => 4,
'max_threads' => 16
}
}
@config = nil
class << self
# Loads the OpenProject configuration file
# Valid options:
# * <tt>:file</tt>: the configuration file to load (default: config/configuration.yml)
# * <tt>:env</tt>: the environment to load the configuration for (default: Rails.env)
def load(options = {})
filename = options[:file] || File.join(Rails.root, 'config', 'configuration.yml')
env = options[:env] || Rails.env
@config = @defaults.dup
load_config_from_file(filename, env, @config)
convert_old_email_settings(@config)
override_config!(@config)
define_config_methods
@config = @config.with_indifferent_access
end
# Replace config values for which an environment variable with the same key in upper case
# exists
def override_config!(config, source = default_override_source)
config.keys.select { |key| source.include? key.upcase }
.each { |key| config[key] = extract_value key, source[key.upcase] }
config.deep_merge! merge_config(config, source)
end
def merge_config(config, source, prefix: ENV_PREFIX)
new_config = config.dup.with_indifferent_access
source.select { |k, _| k =~ /^#{prefix}/i }.each do |k, value|
path = self.path prefix, k
path_config = path_to_hash(*path, extract_value(k, value))
new_config.deep_merge! path_config
end
new_config
end
def path(prefix, env_var_name)
path = []
env_var_name = env_var_name.sub /^#{prefix}/, ''
env_var_name.gsub(/([a-zA-Z0-9]|(__))+/) do |seg|
path << unescape_underscores(seg.downcase).to_sym
end
path
end
# takes the path provided and transforms it into a deeply nested hash
# where the last parameter becomes the value.
#
# e.g. path_to_hash(:a, :b, :c, :d) => { a: { b: { c: :d } } }
def path_to_hash(*path)
value = path.pop
path.reverse.inject(value) do |path_hash, key|
{ key => path_hash }
end
end
def get_value(value)
value
end
def unescape_underscores(path_segment)
path_segment.gsub '__', '_'
end
# Returns a configuration setting
def [](name)
load unless @config
@config[name]
Settings::Definition[name]&.value
end
# Sets configuration setting
def []=(name, value)
load unless @config
@config[name] = value
end
# Yields a block with the specified hash configuration settings
def with(settings)
settings.stringify_keys!
load unless @config
was = settings.keys.inject({}) { |h, v| h[v] = @config[v]; h }
@config.merge! settings
yield if block_given?
@config.merge! was
Settings::Definition[name].value = value
end
def configure_cache(application_config)
@ -330,13 +49,12 @@ module OpenProject
# rails defaults to :file_store, use :mem_cache_store when :memcache is configured in configuration.yml
# Also use :mem_cache_store for when :dalli_store is configured
cache_store = @config['rails_cache_store'].try(:to_sym)
cache_store = self['rails_cache_store'].try(:to_sym)
case cache_store
when :memcache, :dalli_store
cache_config = [:mem_cache_store]
cache_config << @config['cache_memcache_server'] \
if @config['cache_memcache_server']
cache_config << self['cache_memcache_server'] if self['cache_memcache_server']
# default to :file_store
when NilClass, :file_store
cache_config = [:file_store, Rails.root.join('tmp/cache')]
@ -344,7 +62,7 @@ module OpenProject
cache_config = [cache_store]
end
parameters = cache_parameters(@config)
parameters = cache_parameters
cache_config << parameters if parameters.size > 0
application_config.cache_store = cache_config
@ -356,43 +74,43 @@ module OpenProject
# or there is something to overwrite it
application_config.cache_store.nil? ||
application_config.cache_store == :file_store ||
@config['rails_cache_store'].present?
self['rails_cache_store'].present?
end
def migrate_mailer_configuration!
# do not migrate if forced to legacy configuration (using settings or ENV)
return true if @config['email_delivery_configuration'] == 'legacy'
return true if self['email_delivery_configuration'] == 'legacy'
# do not migrate if no legacy configuration
return true if @config['email_delivery_method'].blank?
return true if self['email_delivery_method'].blank?
# do not migrate if the setting already exists and is not blank
return true if Setting.email_delivery_method.present?
Rails.logger.info 'Migrating existing email configuration to the settings table...'
Setting.email_delivery_method = @config['email_delivery_method'].to_sym
Setting.email_delivery_method = self['email_delivery_method'].to_sym
['smtp_', 'sendmail_'].each do |config_type|
mail_delivery_config = filter_hash_by_key_prefix(@config, config_type)
unless mail_delivery_config.empty?
mail_delivery_config.symbolize_keys! if mail_delivery_config.respond_to?(:symbolize_keys!)
mail_delivery_config.each do |k, v|
Setting["#{config_type}#{k}"] = case v
when TrueClass
1
when FalseClass
0
else
v
end
end
mail_delivery_configs = Settings::Definition.all_of_prefix(config_type)
next if mail_delivery_configs.empty?
mail_delivery_configs.each do |config|
Setting["#{config_type}#{config.name}"] = case config.value
when TrueClass
1
when FalseClass
0
else
v
end
end
end
true
end
def reload_mailer_configuration!
if @config['email_delivery_configuration'] == 'legacy'
configure_legacy_action_mailer(@config)
if self['email_delivery_configuration'] == 'legacy'
configure_legacy_action_mailer
else
case Setting.email_delivery_method
when :smtp
@ -413,19 +131,18 @@ module OpenProject
# This is used to configure email sending from users who prefer to
# continue using environment variables of configuration.yml settings. Our
# hosted SaaS version requires this.
def configure_legacy_action_mailer(config)
return true if config['email_delivery_method'].blank?
def configure_legacy_action_mailer
return true if self['email_delivery_method'].blank?
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.delivery_method = config['email_delivery_method'].to_sym
ActionMailer::Base.delivery_method = self['email_delivery_method'].to_sym
['smtp_', 'sendmail_'].each do |config_type|
mail_delivery_config = filter_hash_by_key_prefix(config, config_type)
%w[smtp_ sendmail_].each do |config_type|
config = settings_of_prefix(config_type)
unless mail_delivery_config.empty?
mail_delivery_config.symbolize_keys! if mail_delivery_config.respond_to?(:symbolize_keys!)
ActionMailer::Base.send("#{config_type + 'settings'}=", mail_delivery_config)
end
next if config.empty?
ActionMailer::Base.send("#{config_type}settings=", config)
end
end
@ -458,128 +175,54 @@ module OpenProject
ActionMailer::Base.smtp_settings[:ssl] = Setting.smtp_ssl?
end
##
# The default source for overriding configuration values
# is ENV, but may be changed for testing purposes
def default_override_source
ENV
end
##
# Extract the configuration value from the given input
# using YAML.
#
# @param key [String] The key of the input within the source hash.
# @param original_value [String] The string from which to extract the actual value.
# @return A ruby object (e.g. Integer, Float, String, Hash, Boolean, etc.)
# @raise [ArgumentError] If the string could not be parsed.
def extract_value(key, original_value)
# YAML parses '' as false, but empty ENV variables will be passed as that.
# To specify specific values, one can use !!str (-> '') or !!null (-> nil)
return original_value if original_value == ''
parsed = YAML.load(original_value)
if parsed.is_a?(String)
original_value
else
parsed
end
rescue StandardError => e
raise ArgumentError, "Configuration value for '#{key}' is invalid: #{e.message}"
end
def load_config_from_file(filename, env, config)
if File.file?(filename)
file_config = YAML::load(ERB.new(File.read(filename)).result)
if file_config.is_a? Hash
config.deep_merge!(load_env_from_config(file_config, env))
else
warn "#{filename} is not a valid OpenProject configuration file, ignoring."
end
end
end
def load_env_from_config(config, env)
merged_config = {}
if config['default']
merged_config.deep_merge!(config['default'])
end
if config[env]
merged_config.deep_merge!(config[env])
end
merged_config
end
# Convert old mail settings
#
# SMTP Example:
# mail_delivery.smtp_settings.<key> is converted to smtp_<key>
# options:
# disable_deprecation_message - used by testing
def convert_old_email_settings(config, options = {})
if config['email_delivery']
unless options[:disable_deprecation_message]
ActiveSupport::Deprecation.warn 'Deprecated mail delivery settings used. Please ' +
'update them in config/configuration.yml or use ' +
'environment variables. See doc/CONFIGURATION.md for ' +
'more information.'
end
config['email_delivery_method'] = config['email_delivery']['delivery_method'] || :smtp
['sendmail', 'smtp'].each do |settings_type|
settings_key = "#{settings_type}_settings"
if config['email_delivery'][settings_key]
config['email_delivery'][settings_key].each do |key, value|
config["#{settings_type}_#{key}"] = value
end
end
end
config.delete('email_delivery')
end
end
def cache_parameters(config)
def cache_parameters
mapping = {
'cache_expires_in_seconds' => %i[expires_in to_i],
'cache_namespace' => %i[namespace to_s]
}
parameters = {}
mapping.each_pair do |from, to|
if config[from]
if self[from]
to_key, method = to
parameters[to_key] = config[from].method(method).call
parameters[to_key] = self[from].method(method).call
end
end
parameters
end
# Filters a hash with String keys by a key prefix and removes the prefix from the keys
def filter_hash_by_key_prefix(hash, prefix)
filtered_hash = {}
hash.each do |key, value|
if key.start_with? prefix
filtered_hash[key[prefix.length..-1]] = value
end
def method_missing(name, *args, &block)
setting_name = name.to_s.sub(/(=|\?)$/, '')
if Settings::Definition.exists?(setting_name)
define_config_methods(setting_name)
send(setting_name, *args, &block)
else
super
end
filtered_hash
end
def define_config_methods
@config.keys.each do |setting|
next if respond_to? setting
def respond_to_missing?(name, include_private = false)
Settings::Definition.exists?(name.to_s.sub(/(=|\?)$/, '')) || super
end
define_singleton_method setting do
self[setting]
end
def define_config_methods(setting_name)
define_singleton_method setting_name do
self[setting_name]
end
define_singleton_method "#{setting}?" do
['true', true, '1'].include? self[setting]
end
define_singleton_method "#{setting_name}?" do
['true', true, '1'].include? self[setting_name]
end
end
# Filters a hash with String keys by a key prefix and removes the prefix from the keys
def settings_of_prefix(prefix)
Settings::Definition
.all_of_prefix(prefix)
.to_h { |setting| [setting.name.delete_prefix(prefix), setting.value] }
.symbolize_keys!
end
end
end
end

@ -32,13 +32,6 @@ module OpenProject
# To be included into OpenProject::Configuration in order to provide
# helper methods for easier access to certain configuration options.
module Helpers
##
# Carrierwave storage type. Possible values are, among others, :file and :fog.
# The latter requires further configuration.
def attachments_storage
(self['attachments_storage'] || 'file').to_sym
end
def direct_uploads
return false unless direct_uploads_supported?

@ -30,6 +30,7 @@ require 'open_project/ui/extensible_tabs'
require_relative '../../../config/constants/api_patch_registry'
require_relative '../../../config/constants/open_project/activity'
require_relative '../../../config/constants/views'
require_relative '../../../config/constants/settings/definition'
module OpenProject::Plugins
module ActsAsOpEngine
@ -196,18 +197,6 @@ module OpenProject::Plugins
end
p.instance_eval(&block) if p && block
end
# Workaround to ensure settings are available after unloading in development mode
plugin_name = engine_name
if options.include? :settings
self.class.class_eval do
config.to_prepare do
Setting.create_setting("plugin_#{plugin_name}",
'default' => options[:settings][:default], 'serialized' => true)
Setting.create_setting_accessors("plugin_#{plugin_name}")
end
end
end
end
##

@ -107,8 +107,9 @@ module Redmine #:nodoc:
registered_plugins[id] = p
if p.settings
Setting.create_setting("plugin_#{id}", 'default' => p.settings[:default], 'serialized' => true)
Setting.create_setting_accessors("plugin_#{id}")
Settings::Definition.add("plugin_#{id}",
value: p.settings[:default],
format: :hash)
end
# If there are plugins waiting for us to be loaded, we try loading those, again

@ -61,8 +61,8 @@ describe 'OpenID Connect', type: :rails_request do
OpenIDConnect::ResponseObject::UserInfo.new(user_info)
)
# enable storing the access token in a cookie
OpenProject::Configuration['omniauth_store_access_token_in_cookie'] = true
# Enable storing the access token in a cookie is not necessary since it is currently hard wired to always
# be true.
end
describe 'sign-up and login' do

@ -95,7 +95,13 @@ module OpenProject::Reporting
require_relative 'patches/to_date_patch'
end
patches %i[CustomFieldsController OpenProject::Configuration]
initializer 'reporting.configuration' do
::Settings::Definition.add 'cost_reporting_cache_filter_classes',
value: true,
format: :boolean
end
patches %i[CustomFieldsController]
patch_with_namespace :BasicData, :RoleSeeder
patch_with_namespace :BasicData, :SettingSeeder
end

@ -1,53 +0,0 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject::Reporting::Patches
module OpenProject::ConfigurationPatch
def self.included(base)
base.class_eval do
extend ModuleMethods
@defaults['cost_reporting_cache_filter_classes'] = true
if config_loaded_before_patch?
@config['cost_reporting_cache_filter_classes'] = true
end
end
end
module ModuleMethods
def config_loaded_before_patch?
@config.present? && !@config.has_key?('cost_reporting_cache_filter_classes')
end
def cost_reporting_cache_filter_classes
@config['cost_reporting_cache_filter_classes']
end
end
end
end

@ -30,19 +30,6 @@ require 'spec_helper'
describe 'OpenProject::Configuration' do
context '.cost_reporting_cache_filter_classes' do
before do
# This prevents the values from the actual configuration file to influence
# the test outcome.
#
# TODO: I propose to port this over to the core to always prevent this for specs.
OpenProject::Configuration.load(file: 'bogus')
end
after do
# resetting for now to avoid braking specs, who by now rely on having the file read.
OpenProject::Configuration.load
end
it 'is a true by default via the method' do
expect(OpenProject::Configuration.cost_reporting_cache_filter_classes).to be_truthy
end

@ -0,0 +1,630 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe Settings::Definition do
shared_context 'with clean definitions' do
let!(:definitions_before) { described_class.all.dup }
before do
described_class.send(:reset)
end
after do
described_class.send(:reset)
described_class.instance_variable_set(:@all, definitions_before)
end
end
describe '.all' do
subject(:all) { described_class.all }
it "is a list of setting definitions" do
expect(all)
.to(be_all { |d| d.is_a?(described_class) })
end
it 'contains a definition from settings' do
expect(all)
.to(be_any { |d| d.name == 'smtp_address' })
end
it 'contains a definition from configuration' do
expect(all)
.to(be_any { |d| d.name == 'edition' })
end
it 'contains a definition from settings.yml' do
expect(all)
.to(be_any { |d| d.name == 'sendmail_location' })
end
it 'casts the value from settings.yml' do
expect(all.detect { |d| d.name == 'brute_force_block_after_failed_logins' }.value)
.to eq 20
end
context 'when overriding from ENV' do
include_context 'with clean definitions'
it 'allows overriding configuration from ENV' do
stub_const('ENV', { 'OPENPROJECT_EDITION' => 'bim' })
expect(all.detect { |d| d.name == 'edition' }.value)
.to eql 'bim'
end
it 'overriding boolean configuration from ENV will cast the value' do
stub_const('ENV', { 'OPENPROJECT_REST__API__ENABLED' => '0' })
expect(all.detect { |d| d.name == 'rest_api_enabled' }.value)
.to be false
end
it 'overriding configuration from ENV will set it to non writable' do
stub_const('ENV', { 'OPENPROJECT_EDITION' => 'bim' })
expect(all.detect { |d| d.name == 'edition' })
.not_to be_writable
end
it 'allows overriding settings array from ENV' do
stub_const('ENV', { 'OPENPROJECT_PASSWORD__ACTIVE__RULES' => YAML.dump(['lowercase']) })
expect(all.detect { |d| d.name == 'password_active_rules' }.value)
.to eql ['lowercase']
end
it 'overriding settings from ENV will set it to non writable' do
stub_const('ENV', { 'OPENPROJECT_WELCOME__TITLE' => 'Some title' })
expect(all.detect { |d| d.name == 'welcome_title' })
.not_to be_writable
end
it 'allows overriding settings hash partially from ENV' do
stub_const('ENV', { 'OPENPROJECT_REPOSITORY__CHECKOUT__DATA_GIT_ENABLED' => '1' })
expect(all.detect { |d| d.name == 'repository_checkout_data' }.value)
.to eql({
'git' => { 'enabled' => 1 },
'subversion' => { 'enabled' => 0 }
})
end
it 'ENV vars for which no definition exists will not be handled' do
stub_const('ENV', { 'OPENPROJECT_BOGUS' => '1' })
expect(all.detect { |d| d.name == 'bogus' })
.to be_nil
end
end
context 'when overriding from file' do
include_context 'with clean definitions'
let(:file_contents) do
{
'default' => {
'edition' => 'bim',
'sendmail_location' => 'default location'
},
'test' => {
'smtp_address' => 'test address',
'sendmail_location' => 'test location',
'bogus' => 'bogusvalue'
}
}
end
before do
allow(YAML)
.to receive(:load_file)
.with(Rails.root.join('config/configuration.yml'))
.and_return(file_contents)
allow(YAML)
.to receive(:load_file)
.with(Rails.root.join('config/settings.yml'))
.and_return({})
allow(File)
.to receive(:file?)
.with(Rails.root.join('config/configuration.yml'))
.and_return(true)
# Loading of the config file is disabled in test env normally.
allow(Rails.env)
.to receive(:test?)
.and_return(false)
end
it 'overrides from file default' do
expect(all.detect { |d| d.name == 'edition' }.value)
.to eql 'bim'
end
it 'marks the value overwritten from file default unwritable' do
expect(all.detect { |d| d.name == 'edition' })
.not_to be_writable
end
it 'overrides from file default path but once again from current env' do
expect(all.detect { |d| d.name == 'sendmail_location' }.value)
.to eql 'test location'
end
it 'marks the value overwritten from file default and again from current unwritable' do
expect(all.detect { |d| d.name == 'sendmail_location' })
.not_to be_writable
end
it 'overrides from file current env' do
expect(all.detect { |d| d.name == 'smtp_address' }.value)
.to eql 'test address'
end
it 'marks the value overwritten from file current unwritable' do
expect(all.detect { |d| d.name == 'smtp_address' })
.not_to be_writable
end
it 'does not accept undefined settings' do
expect(all.detect { |d| d.name == 'bogus' })
.to be_nil
end
context 'when having invalid values in the file' do
let(:file_contents) do
{
'default' => {
'smtp_openssl_verify_mode' => 'bogus'
}
}
end
it 'is invalid' do
expect { all }
.to raise_error ArgumentError
end
end
context 'when overwritten from ENV' do
before do
stub_const('ENV', { 'OPENPROJECT_SENDMAIL__LOCATION' => 'env location' })
end
it 'overrides from ENV' do
expect(all.detect { |d| d.name == 'sendmail_location' }.value)
.to eql 'env location'
end
it 'marks the overwritten value unwritable' do
expect(all.detect { |d| d.name == 'sendmail_location' })
.not_to be_writable
end
end
end
context 'when adding an additional setting' do
include_context 'with clean definitions'
it 'includes the setting' do
all
described_class.add 'bogus',
value: 1,
format: :integer
expect(all.detect { |d| d.name == 'bogus' }.value)
.to eq(1)
end
end
end
describe ".[name]" do
subject(:definition) { described_class[key] }
context 'with a string' do
let(:key) { 'smtp_address' }
it 'returns the value' do
expect(definition.name)
.to eql key
end
end
context 'with a symbol' do
let(:key) { :smtp_address }
it 'returns the value' do
expect(definition.name)
.to eql key.to_s
end
end
context 'with a non existing key' do
let(:key) { 'bogus' }
it 'returns the value' do
expect(definition)
.to be_nil
end
end
context 'when adding a setting late' do
include_context 'with clean definitions'
let(:key) { 'bogus' }
before do
described_class[key]
described_class.add 'bogus',
value: 1,
format: :integer
end
it 'has the setting' do
expect(definition.name)
.to eql key.to_s
end
end
end
describe '#override_value' do
let(:format) { :string }
let(:value) { 'abc' }
let(:instance) do
described_class
.new 'bogus',
format: format,
value: value
end
context 'with string format' do
before do
instance.override_value('xyz')
end
it 'overwrites' do
expect(instance.value)
.to eql 'xyz'
end
it 'turns the definition unwritable' do
expect(instance)
.not_to be_writable
end
end
context 'with hash format' do
let(:format) { :hash }
let(:value) do
{
abc: {
a: 1,
b: 2
},
cde: 1
}
end
before do
instance.override_value({ abc: { a: 5 }, xyz: 2 })
end
it 'deep merges' do
expect(instance.value)
.to eql({
abc: {
a: 5,
b: 2
},
cde: 1,
xyz: 2
})
end
it 'turns the definition unwritable' do
expect(instance)
.not_to be_writable
end
end
context 'with array format' do
let(:format) { :array }
let(:value) { [1, 2, 3] }
before do
instance.override_value([4, 5, 6])
end
it 'overwrites' do
expect(instance.value)
.to eql [4, 5, 6]
end
it 'turns the definition unwritable' do
expect(instance)
.not_to be_writable
end
end
context 'with an invalid value' do
let(:instance) do
described_class
.new 'bogus',
format: format,
value: 'foo',
allowed: %w[foo bar]
end
it 'raises an error' do
expect { instance.override_value('invalid') }
.to raise_error ArgumentError
end
end
end
describe '.exists?' do
context 'with an existing setting' do
it 'is truthy' do
expect(described_class)
.to exist('smtp_address')
end
end
context 'with a non existing setting' do
it 'is truthy' do
expect(described_class)
.not_to exist('foobar')
end
end
end
describe '.new' do
context 'with all the attributes' do
let(:instance) do
described_class.new 'bogus',
format: :integer,
value: 1,
writable: false,
allowed: [1, 2, 3]
end
it 'has the name' do
expect(instance.name)
.to eql 'bogus'
end
it 'has the format (in symbol)' do
expect(instance.format)
.to eq :integer
end
it 'has the value' do
expect(instance.value)
.to eq 1
end
it 'is not serialized' do
expect(instance)
.not_to be_serialized
end
it 'has the writable value' do
expect(instance)
.not_to be_writable
end
it 'has the allowed value' do
expect(instance.allowed)
.to eql [1, 2, 3]
end
end
context 'with the minimal attributes (integer value)' do
let(:instance) do
described_class.new 'bogus',
value: 1
end
it 'has the name' do
expect(instance.name)
.to eql 'bogus'
end
it 'has the format (in symbol) deduced' do
expect(instance.format)
.to eq :integer
end
it 'has the value' do
expect(instance.value)
.to eq 1
end
it 'is not serialized' do
expect(instance)
.not_to be_serialized
end
it 'has the writable value' do
expect(instance)
.to be_writable
end
end
context 'with the minimal attributes (hash value)' do
let(:instance) do
described_class.new 'bogus',
value: { a: 'b' }
end
it 'has the format (in symbol) deduced' do
expect(instance.format)
.to eq :hash
end
it 'is serialized' do
expect(instance)
.to be_serialized
end
end
context 'with the minimal attributes (array value)' do
let(:instance) do
described_class.new 'bogus',
value: %i[a b]
end
it 'has the format (in symbol) deduced' do
expect(instance.format)
.to eq :array
end
it 'is serialized' do
expect(instance)
.to be_serialized
end
end
context 'with the minimal attributes (true value)' do
let(:instance) do
described_class.new 'bogus',
value: true
end
it 'has the format (in symbol) deduced' do
expect(instance.format)
.to eq :boolean
end
end
context 'with the minimal attributes (false value)' do
let(:instance) do
described_class.new 'bogus',
value: false
end
it 'has the format (in symbol) deduced' do
expect(instance.format)
.to eq :boolean
end
end
context 'with the minimal attributes (date value)' do
let(:instance) do
described_class.new 'bogus',
value: Time.zone.today
end
it 'has the format (in symbol) deduced' do
expect(instance.format)
.to eq :date
end
end
context 'with the minimal attributes (datetime value)' do
let(:instance) do
described_class.new 'bogus',
value: DateTime.now
end
it 'has the format (in symbol) deduced' do
expect(instance.format)
.to eq :date_time
end
end
context 'with the minimal attributes (string value)' do
let(:instance) do
described_class.new 'bogus',
value: 'abc'
end
it 'has the format (in symbol) deduced' do
expect(instance.format)
.to eq :string
end
end
context 'with procs for value, writable and allowed' do
let(:instance) do
described_class.new 'bogus',
format: 'string',
value: -> { 'some value' },
writable: -> { false },
allowed: -> { %w[a b c] }
end
it 'returns the procs return value for value' do
expect(instance.value)
.to eql 'some value'
end
it 'returns the procs return value for writable' do
expect(instance.writable?)
.to be false
end
it 'returns the procs return value for allowed' do
expect(instance.allowed)
.to eql %w[a b c]
end
end
context 'with an integer provided as a string' do
let(:instance) do
described_class.new 'bogus',
format: :integer,
value: '5'
end
it 'returns value as an int' do
expect(instance.value)
.to eq 5
end
end
context 'with a float provided as a string' do
let(:instance) do
described_class.new 'bogus',
format: :float,
value: '0.5'
end
it 'returns value as a float' do
expect(instance.value)
.to eq 0.5
end
end
end
end

@ -149,7 +149,7 @@ describe Admin::Settings::ProjectsSettingsController, type: :controller do
password_min_adhered_rules: 0,
password_days_valid: 365,
password_count_former_banned: 2,
lost_password: 1
lost_password: true
}
end
@ -160,7 +160,7 @@ describe Admin::Settings::ProjectsSettingsController, type: :controller do
password_min_adhered_rules: 7,
password_days_valid: 13,
password_count_former_banned: 80,
lost_password: 3
lost_password: false
}
end
@ -214,8 +214,8 @@ describe Admin::Settings::ProjectsSettingsController, type: :controller do
expect(Setting[:password_count_former_banned]).to eq 80
end
it 'sets the lost password option to the nonsensical 3' do
expect(Setting[:lost_password]).to eq 3
it 'sets the lost password option to false' do
expect(Setting[:lost_password]).to be false
end
end
@ -250,8 +250,8 @@ describe Admin::Settings::ProjectsSettingsController, type: :controller do
expect(Setting[:password_count_former_banned]).to eq 2
end
it 'does not set the lost password option to the nonsensical 3' do
expect(Setting[:lost_password]).to eq 1
it 'keeps the lost password option' do
expect(Setting[:lost_password]).to be true
end
end
end

@ -34,57 +34,93 @@ describe SettingsHelper, type: :helper do
let(:options) { { class: 'custom-class' } }
describe '#setting_select' do
before do
allow(Setting).to receive(:field).and_return('2')
before do
allow(Setting)
.to receive(:field_writable?)
.and_return true
end
shared_examples_for 'field disabled if non writable' do
context 'when the setting is writable' do
it 'is enabled' do
expect(output)
.to have_field 'settings_field', disabled: false
end
end
context 'when the setting isn`t writable' do
before do
allow(Setting)
.to receive(:field_writable?)
.and_return false
end
it 'is disabled' do
expect(output)
.to have_field 'settings_field', disabled: true
end
end
end
describe '#setting_select' do
subject(:output) do
helper.setting_select :field, [['Popsickle', '1'], ['Jello', '2'], ['Ice Cream', '3']], options
end
before do
allow(Setting).to receive(:field).and_return('2')
end
it_behaves_like 'labelled by default'
it_behaves_like 'wrapped in field-container by default'
it_behaves_like 'wrapped in container', 'select-container'
it_behaves_like 'field disabled if non writable'
it 'should output element' do
it 'outputs element' do
expect(output).to have_selector 'select.form--select > option', count: 3
expect(output).to have_select 'settings_field', selected: 'Jello'
end
end
describe '#setting_multiselect' do
before do
allow(Setting).to receive(:field).at_least(:once).and_return('1')
end
subject(:output) do
helper.setting_multiselect :field, [['Popsickle', '1'], ['Jello', '2'], ['Ice Cream', '3']], options
end
before do
allow(Setting).to receive(:field).at_least(:once).and_return('1')
end
it_behaves_like 'wrapped in container' do
let(:container_count) { 3 }
end
it 'should have checkboxes wrapped in checkbox-container' do
it 'has checkboxes wrapped in checkbox-container' do
expect(output).to have_selector 'span.form--check-box-container', count: 3
end
it 'should have three labels' do
it 'has three labels' do
expect(output).to have_selector 'label.form--label-with-check-box', count: 3
end
it 'should output element' do
it 'outputs element' do
expect(output).to have_selector 'input[type="checkbox"].form--check-box', count: 3
end
end
describe '#settings_matrix' do
before do
allow(Setting).to receive(:field_a).at_least(:once).and_return('2')
allow(Setting).to receive(:field_b).at_least(:once).and_return('3')
context 'when the setting isn`t writable' do
before do
allow(Setting)
.to receive(:field_writable?)
.and_return false
end
it 'is disabled' do
expect(output).to have_selector 'input[type="checkbox"][disabled="disabled"].form--check-box', count: 3
end
end
end
describe '#settings_matrix' do
subject(:output) do
settings = %i[field_a field_b]
choices = [
@ -107,6 +143,13 @@ describe SettingsHelper, type: :helper do
helper.settings_matrix settings, choices
end
before do
allow(Setting).to receive(:field_a).at_least(:once).and_return('2')
allow(Setting).to receive(:field_b).at_least(:once).and_return('3')
allow(Setting).to receive(:field_a_writable?).and_return true
allow(Setting).to receive(:field_b_writable?).and_return true
end
it_behaves_like 'not wrapped in container'
it 'is structured as a table' do
@ -151,22 +194,42 @@ describe SettingsHelper, type: :helper do
expect(output).to have_checked_field 'field_a_2'
expect(output).to have_checked_field 'field_b_3'
end
end
describe '#setting_text_field' do
before do
allow(Setting).to receive(:field).and_return('important value')
context 'when the setting isn`t writable' do
before do
allow(Setting)
.to receive(:field_a_writable?)
.and_return false
end
it 'is disabled' do
expect(output).to be_html_eql(%{
<td class="form--matrix-checkbox-cell">
<span class="form--check-box-container">
<input class="form--check-box" id="field_a_1"
name="settings[field_a][]" type="checkbox" disabled="disabled" value="1">
</span>
</td>
}).at_path('tr.form--matrix-row:first-child > td:nth-of-type(2)')
end
end
end
describe '#setting_text_field' do
subject(:output) do
helper.setting_text_field :field, options
end
before do
allow(Setting).to receive(:field).and_return('important value')
end
it_behaves_like 'labelled by default'
it_behaves_like 'wrapped in field-container by default'
it_behaves_like 'wrapped in container', 'text-field-container'
it_behaves_like 'field disabled if non writable'
it 'should output element' do
it 'outputs element' do
expect(output).to be_html_eql(%{
<input class="custom-class form--text-field"
id="settings_field" name="settings[field]" type="text" value="important value" />
@ -175,19 +238,20 @@ describe SettingsHelper, type: :helper do
end
describe '#setting_text_area' do
before do
allow(Setting).to receive(:field).and_return('important text')
end
subject(:output) do
helper.setting_text_area :field, options
end
before do
allow(Setting).to receive(:field).and_return('important text')
end
it_behaves_like 'labelled by default'
it_behaves_like 'wrapped in field-container by default'
it_behaves_like 'wrapped in container', 'text-area-container'
it_behaves_like 'field disabled if non writable'
it 'should output element' do
it 'outputs element' do
expect(output).to be_html_eql(%{
<textarea class="custom-class form--text-area" id="settings_field" name="settings[field]">
important text</textarea>
@ -208,8 +272,9 @@ important text</textarea>
it_behaves_like 'labelled by default'
it_behaves_like 'wrapped in field-container by default'
it_behaves_like 'wrapped in container', 'check-box-container'
it_behaves_like 'field disabled if non writable'
it 'should output element' do
it 'outputs element' do
expect(output).to have_selector 'input[type="checkbox"].custom-class.form--check-box'
expect(output).to have_checked_field 'settings_field'
end
@ -223,8 +288,9 @@ important text</textarea>
it_behaves_like 'labelled by default'
it_behaves_like 'wrapped in field-container by default'
it_behaves_like 'wrapped in container', 'check-box-container'
it_behaves_like 'field disabled if non writable'
it 'should output element' do
it 'outputs element' do
expect(output).to have_selector 'input[type="checkbox"].custom-class.form--check-box'
expect(output).to have_unchecked_field 'settings_field'
end
@ -244,7 +310,7 @@ important text</textarea>
it_behaves_like 'wrapped in field-container by default'
it_behaves_like 'wrapped in container', 'text-field-container'
it 'should output element' do
it 'outputs element' do
expect(output).to be_html_eql(%{
<input class="custom-class form--text-field -time"
id="settings_field" name="settings[field]" type="time" value="16:00" />

@ -29,284 +29,43 @@
require 'spec_helper'
describe OpenProject::Configuration do
describe '.load_config_from_file' do
let(:file_contents) do
<<-CONTENT
default:
test:
somesetting: foo
CONTENT
end
before do
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with('configfilename').and_return(file_contents)
allow(File).to receive(:file?).with('configfilename').and_return(true)
described_class.load(file: 'configfilename')
end
it 'merges the config from the file into the given config hash' do
expect(described_class['somesetting']).to eq('foo')
expect(described_class[:somesetting]).to eq('foo')
expect(described_class.somesetting).to eq('foo')
end
context 'with deep nesting' do
let(:file_contents) do
<<-CONTENT
default:
web:
timeout: 42
CONTENT
end
let!(:definitions_before) { Settings::Definition.all.dup }
it 'deepmerges the config from the file into the given config hash' do
expect(described_class['web']['workers']).not_to be_nil
expect(described_class['web']['timeout']).to eq(42)
end
end
before do
Settings::Definition.send(:reset)
end
describe '.load_env_from_config' do
describe 'with a default setting' do
let(:config) do
described_class.send(:load_env_from_config, {
'default' => { 'somesetting' => 'foo' },
'test' => {},
'someother' => { 'somesetting' => 'bar' }
}, 'test')
end
it 'loads a default setting' do
expect(config['somesetting']).to eq('foo')
end
end
describe 'with an environment-specific setting' do
let(:config) do
described_class.send(:load_env_from_config, {
'default' => {},
'test' => { 'somesetting' => 'foo' }
}, 'test')
end
it 'loads a setting' do
expect(config['somesetting']).to eq('foo')
end
end
describe 'with a default and an overriding environment-specific setting' do
let(:config) do
described_class.send(:load_env_from_config, {
'default' => { 'somesetting' => 'foo' },
'test' => { 'somesetting' => 'bar' }
}, 'test')
end
it 'loads the overriding value' do
expect(config['somesetting']).to eq('bar')
end
context 'with deep nesting' do
let(:config) do
described_class.send(:load_env_from_config, {
'default' => { 'web' => {
'overridensetting' => 'unseen!',
'somesetting' => 'foo'
} },
'test' => { 'web' => {
'overridensetting' => 'env wins',
'someothersetting' => 'bar'
} }
}, 'test')
end
it 'deepmerges configs together' do
expect(config['web']['overridensetting']).to eq('env wins')
expect(config['web']['somesetting']).to eq('foo')
expect(config['web']['someothersetting']).to eq('bar')
end
end
end
end
describe '.load_overrides_from_environment_variables' do
let(:config) do
{
'someemptysetting' => nil,
'nil' => 'foobar',
'str_empty' => 'foobar',
'somesetting' => 'foo',
'invalid_yaml' => nil,
'some_list_entry' => nil,
'nested' => {
'key' => 'value',
'hash' => 'somethingelse',
'deeply_nested' => {
'key' => nil
}
},
'foo' => {
'bar' => {
hash_with_symbols: 1234
}
}
}
end
let(:env_vars) do
{
'SOMEEMPTYSETTING' => '',
'SOMESETTING' => 'bar',
'NIL' => '!!null',
'INVALID_YAML' => "'foo'! #234@@½%%%",
'OPTEST_SOME__LIST__ENTRY' => '[foo, bar , xyz, whut wat]',
'OPTEST_NESTED_KEY' => 'baz',
'OPTEST_NESTED_DEEPLY__NESTED_KEY' => '42',
'OPTEST_NESTED_HASH' => '{ foo: bar, xyz: bla }',
'OPTEST_FOO_BAR_HASH__WITH__SYMBOLS' => '{ foo: !ruby/symbol foobar }'
}
end
before do
stub_const('OpenProject::Configuration::ENV_PREFIX', 'OPTEST')
described_class.send :override_config!, config, env_vars
end
it 'returns the original string, not the invalid YAML one' do
expect(config['invalid_yaml']).to eq env_vars['INVALID_YAML']
end
it 'does not parse the empty value' do
expect(config['someemptysetting']).to eq('')
end
it 'parses the null identifier' do
expect(config['nil']).to be_nil
end
it 'overrides the previous setting value' do
expect(config['somesetting']).to eq('bar')
end
it 'overrides a nested value' do
expect(config['nested']['key']).to eq('baz')
end
it 'overrides values nested several levels deep' do
expect(config['nested']['deeply_nested']['key']).to eq(42)
end
it 'parses simple comma-separated lists' do
expect(config['some_list_entry']).to eq(['foo', 'bar', 'xyz', 'whut wat'])
end
it 'parses simple hashes' do
expect(config['nested']['hash']).to eq('foo' => 'bar', 'xyz' => 'bla')
end
it 'parses hashes with symbols and non-string values' do
expect(config['foo']['bar']['hash_with_symbols']).to eq('foo' => :foobar)
expect(config['foo']['bar']['hash_with_symbols'][:foo]).to eq(:foobar)
end
end
describe '.with' do
before do
allow(described_class).to receive(:load_config_from_file) do |_filename, _env, config|
config.merge!('somesetting' => 'foo')
end
described_class.load(env: 'test')
end
it 'returns the overridden the setting within the block' do
expect(described_class['somesetting']).to eq('foo')
described_class.with 'somesetting' => 'bar' do
expect(described_class['somesetting']).to eq('bar')
end
expect(described_class['somesetting']).to eq('foo')
end
end
describe '.convert_old_email_settings' do
let(:settings) do
{
'email_delivery' => {
'delivery_method' => :smtp,
'perform_deliveries' => true,
'smtp_settings' => {
'address' => 'smtp.example.net',
'port' => 25,
'domain' => 'example.net'
}
}
}
end
context 'with delivery_method' do
before do
described_class.send(:convert_old_email_settings, settings,
disable_deprecation_message: true)
end
it 'adopts the delivery method' do
expect(settings['email_delivery_method']).to eq(:smtp)
end
it 'converts smtp settings' do
expect(settings['smtp_address']).to eq('smtp.example.net')
expect(settings['smtp_port']).to eq(25)
expect(settings['smtp_domain']).to eq('example.net')
end
end
context 'without delivery_method' do
before do
settings['email_delivery'].delete('delivery_method')
described_class.send(:convert_old_email_settings, settings,
disable_deprecation_message: true)
end
it 'converts smtp settings' do
expect(settings['smtp_address']).to eq('smtp.example.net')
expect(settings['smtp_port']).to eq(25)
expect(settings['smtp_domain']).to eq('example.net')
end
end
after do
Settings::Definition.send(:reset)
Settings::Definition.instance_variable_set(:@all, definitions_before)
Setting.clear_cache
end
describe '.migrate_mailer_configuration!' do
after do
# reset this setting value
Setting[:email_delivery_method] = nil
# reload configuration to isolate specs
described_class.load
# clear settings cache to isolate specs
Setting.clear_cache
before do
allow(Setting)
.to receive(:email_delivery_method=)
end
it 'does nothing if no legacy configuration given' do
described_class['email_delivery_method'] = nil
expect(Setting).not_to receive(:email_delivery_method=)
expect(described_class.migrate_mailer_configuration!).to eq(true)
expect(described_class.migrate_mailer_configuration!).to be_truthy
expect(Setting).not_to have_received(:email_delivery_method=)
end
it 'does nothing if email_delivery_configuration forced to legacy' do
described_class['email_delivery_configuration'] = 'legacy'
expect(Setting).not_to receive(:email_delivery_method=)
expect(described_class.migrate_mailer_configuration!).to eq(true)
expect(described_class.migrate_mailer_configuration!).to be_truthy
expect(Setting).not_to have_received(:email_delivery_method=)
end
it 'does nothing if setting already set' do
described_class['email_delivery_method'] = :sendmail
Setting.email_delivery_method = :sendmail
expect(Setting).not_to receive(:email_delivery_method=)
expect(described_class.migrate_mailer_configuration!).to eq(true)
allow(Setting)
.to receive(:email_delivery_method)
.and_return(:sendmail)
expect(Setting).not_to have_received(:email_delivery_method=)
expect(described_class.migrate_mailer_configuration!).to be_truthy
end
it 'migrates the existing configuration to the settings table' do
@ -318,121 +77,135 @@ describe OpenProject::Configuration do
described_class['smtp_enable_starttls_auto'] = true
described_class['smtp_ssl'] = true
expect(described_class.migrate_mailer_configuration!).to eq(true)
expect(described_class.migrate_mailer_configuration!).to be_truthy
expect(Setting.email_delivery_method).to eq(:smtp)
expect(Setting.smtp_password).to eq('p4ssw0rd')
expect(Setting.smtp_address).to eq('smtp.example.com')
expect(Setting.smtp_port).to eq(587)
expect(Setting.smtp_user_name).to eq('username')
expect(Setting.smtp_enable_starttls_auto?).to eq(true)
expect(Setting.smtp_ssl?).to eq(true)
expect(Setting).to be_smtp_enable_starttls_auto
expect(Setting).to be_smtp_ssl
end
end
describe '.reload_mailer_configuration!' do
let(:action_mailer) { double('ActionMailer::Base', smtp_settings: {}, deliveries: []) }
before do
stub_const('ActionMailer::Base', action_mailer)
end
after do
# reload configuration to isolate specs
described_class.load
# clear settings cache to isolate specs
Setting.clear_cache
allow(ActionMailer::Base)
.to receive(:perform_deliveries=)
allow(ActionMailer::Base)
.to receive(:delivery_method=)
end
it 'uses the legacy method to configure email settings' do
allow(described_class)
.to receive(:configure_legacy_action_mailer)
described_class['email_delivery_configuration'] = 'legacy'
expect(described_class).to receive(:configure_legacy_action_mailer)
described_class.reload_mailer_configuration!
expect(described_class).to have_received(:configure_legacy_action_mailer)
end
context 'without smtp_authentication and without ssl' do
it 'uses the setting values',
with_settings: {
email_delivery_method: :smtp,
smtp_authentication: :none,
smtp_password: 'old',
smtp_address: 'smtp.example.com',
smtp_domain: 'example.com',
smtp_port: 25,
smtp_user_name: 'username',
smtp_enable_starttls_auto: 1,
smtp_ssl: 0
} do
described_class.reload_mailer_configuration!
expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true)
expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp)
expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil
expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com',
port: 25,
domain: 'example.com',
enable_starttls_auto: true,
ssl: false)
end
end
it 'allows settings smtp_authentication to none' do
Setting.email_delivery_method = :smtp
Setting.smtp_authentication = :none
Setting.smtp_password = 'old'
Setting.smtp_address = 'smtp.example.com'
Setting.smtp_domain = 'example.com'
Setting.smtp_port = 25
Setting.smtp_user_name = 'username'
Setting.smtp_enable_starttls_auto = 1
Setting.smtp_ssl = 0
expect(action_mailer).to receive(:perform_deliveries=).with(true)
expect(action_mailer).to receive(:delivery_method=).with(:smtp)
described_class.reload_mailer_configuration!
expect(action_mailer.smtp_settings[:smtp_authentication]).to be_nil
expect(action_mailer.smtp_settings).to eq(address: 'smtp.example.com',
port: 25,
domain: 'example.com',
enable_starttls_auto: true,
ssl: false)
Setting.email_delivery_method = :smtp
Setting.smtp_authentication = :none
Setting.smtp_password = 'old'
Setting.smtp_address = 'smtp.example.com'
Setting.smtp_domain = 'example.com'
Setting.smtp_port = 25
Setting.smtp_user_name = 'username'
Setting.smtp_enable_starttls_auto = 0
Setting.smtp_ssl = 1
expect(action_mailer).to receive(:perform_deliveries=).with(true)
expect(action_mailer).to receive(:delivery_method=).with(:smtp)
described_class.reload_mailer_configuration!
expect(action_mailer.smtp_settings[:smtp_authentication]).to be_nil
expect(action_mailer.smtp_settings).to eq(address: 'smtp.example.com',
port: 25,
domain: 'example.com',
enable_starttls_auto: false,
ssl: true)
context 'without smtp_authentication and with ssl' do
it 'users the setting values',
with_settings: {
email_delivery_method: :smtp,
smtp_authentication: :none,
smtp_password: 'old',
smtp_address: 'smtp.example.com',
smtp_domain: 'example.com',
smtp_port: 25,
smtp_user_name: 'username',
smtp_enable_starttls_auto: 0,
smtp_ssl: 1
} do
described_class.reload_mailer_configuration!
expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true)
expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp)
expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil
expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com',
port: 25,
domain: 'example.com',
enable_starttls_auto: false,
ssl: true)
end
end
it 'correctly sets the action mailer configuration based on the settings' do
Setting.email_delivery_method = :smtp
Setting.smtp_password = 'p4ssw0rd'
Setting.smtp_address = 'smtp.example.com'
Setting.smtp_domain = 'example.com'
Setting.smtp_port = 587
Setting.smtp_user_name = 'username'
Setting.smtp_enable_starttls_auto = 1
Setting.smtp_ssl = 0
expect(action_mailer).to receive(:perform_deliveries=).with(true)
expect(action_mailer).to receive(:delivery_method=).with(:smtp)
described_class.reload_mailer_configuration!
expect(action_mailer.smtp_settings).to eq(address: 'smtp.example.com',
port: 587,
domain: 'example.com',
authentication: 'plain',
user_name: 'username',
password: 'p4ssw0rd',
enable_starttls_auto: true,
ssl: false)
Setting.email_delivery_method = :smtp
Setting.smtp_password = 'p4ssw0rd'
Setting.smtp_address = 'smtp.example.com'
Setting.smtp_domain = 'example.com'
Setting.smtp_port = 587
Setting.smtp_user_name = 'username'
Setting.smtp_enable_starttls_auto = 0
Setting.smtp_ssl = 1
expect(action_mailer).to receive(:perform_deliveries=).with(true)
expect(action_mailer).to receive(:delivery_method=).with(:smtp)
described_class.reload_mailer_configuration!
expect(action_mailer.smtp_settings).to eq(address: 'smtp.example.com',
port: 587,
domain: 'example.com',
authentication: 'plain',
user_name: 'username',
password: 'p4ssw0rd',
enable_starttls_auto: false,
ssl: true)
context 'with smtp_authentication and without ssl' do
it 'users the setting values',
with_settings: {
email_delivery_method: :smtp,
smtp_password: 'p4ssw0rd',
smtp_address: 'smtp.example.com',
smtp_domain: 'example.com',
smtp_port: 587,
smtp_user_name: 'username',
smtp_enable_starttls_auto: 1,
smtp_ssl: 0
} do
described_class.reload_mailer_configuration!
expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true)
expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp)
expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil
expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com',
port: 587,
domain: 'example.com',
authentication: 'plain',
user_name: 'username',
password: 'p4ssw0rd',
enable_starttls_auto: true,
ssl: false)
end
end
context 'with smtp_authentication and with ssl' do
it 'users the setting values',
with_settings: {
email_delivery_method: :smtp,
smtp_password: 'p4ssw0rd',
smtp_address: 'smtp.example.com',
smtp_domain: 'example.com',
smtp_port: 587,
smtp_user_name: 'username',
smtp_enable_starttls_auto: 0,
smtp_ssl: 1
} do
described_class.reload_mailer_configuration!
expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true)
expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp)
expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil
expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com',
port: 587,
domain: 'example.com',
authentication: 'plain',
user_name: 'username',
password: 'p4ssw0rd',
enable_starttls_auto: false,
ssl: true)
end
end
end
@ -445,18 +218,30 @@ describe OpenProject::Configuration do
allow(mailer).to receive(:smtp_settings=)
end
end
let(:config) do
let(:settings) do
{ 'email_delivery_method' => 'smtp',
'smtp_address' => 'smtp.example.net',
'smtp_port' => '25' }
'smtp_port' => '25' }.map do |name, value|
Hashie::Mash.new name: name, value: value
end
end
before do
allow(Settings::Definition)
.to receive(:[]) do |name|
settings.detect { |s| s.name == name }
end
allow(Settings::Definition)
.to receive(:all_of_prefix) do |prefix|
settings.select { |s| s.name.start_with?(prefix) }
end
stub_const('ActionMailer::Base', action_mailer)
end
it 'enables deliveries and configure ActionMailer smtp delivery' do
described_class.send(:configure_legacy_action_mailer, config)
described_class.send(:configure_legacy_action_mailer)
expect(action_mailer)
.to have_received(:perform_deliveries=)
@ -476,11 +261,6 @@ describe OpenProject::Configuration do
Rails::Application::Configuration.new Rails.root
end
after do
# reload configuration to isolate specs
described_class.load
end
context 'with cache store already set' do
before do
application_config.cache_store = 'foo'
@ -512,20 +292,17 @@ describe OpenProject::Configuration do
context 'without cache store already set' do
before do
application_config.cache_store = nil
described_class.send(:configure_cache, application_config)
end
context 'with additional cache store configuration' do
before do
described_class['rails_cache_store'] = 'bar'
end
context 'with additional cache store configuration', with_config: { 'rails_cache_store' => 'bar' } do
it 'changes the cache store' do
described_class.send(:configure_cache, application_config)
expect(application_config.cache_store).to eq([:bar])
end
end
context 'without additional cache store configuration' do
context 'without additional cache store configuration', with_config: { 'rails_cache_store' => nil } do
before do
described_class['rails_cache_store'] = nil
end
@ -538,36 +315,34 @@ describe OpenProject::Configuration do
end
end
describe 'helpers' do
describe '#direct_uploads?' do
let(:value) { described_class.direct_uploads? }
describe '#direct_uploads?' do
let(:value) { described_class.direct_uploads? }
it 'is false by default' do
expect(value).to be false
end
it 'is false by default' do
expect(value).to be false
end
context 'with remote storage' do
def self.storage(provider)
{
attachments_storage: :fog,
fog: {
credentials: {
provider: provider
}
context 'with remote storage' do
def self.storage(provider)
{
attachments_storage: :fog,
fog: {
credentials: {
provider: provider
}
}
end
}
end
context 'AWS', with_config: storage('AWS') do
it 'is true' do
expect(value).to be true
end
context 'with AWS', with_config: storage('AWS') do
it 'is true' do
expect(value).to be true
end
end
context 'Azure', with_config: storage('azure') do
it 'is false' do
expect(value).to be false
end
context 'with Azure', with_config: storage('azure') do
it 'is false' do
expect(value).to be false
end
end
end

@ -96,12 +96,9 @@ describe Repository::Git, type: :model do
end
end
context 'with string disabled types' do
context 'with string disabled types',
with_config: { 'scm' => { 'git' => { 'disabled_types' => %w[managed local] } } } do
before do
allow(OpenProject::Configuration).to receive(:default_override_source)
.and_return('OPENPROJECT_SCM_GIT_DISABLED__TYPES' => '[managed,local]')
OpenProject::Configuration.load
allow(adapter.class).to receive(:config).and_call_original
end

@ -78,12 +78,9 @@ describe Repository::Subversion, type: :model do
end
end
context 'with string disabled types' do
context 'with string disabled types',
with_config: { 'scm' => { 'subversion' => { 'disabled_types' => %w[managed unknowntype] } } } do
before do
allow(OpenProject::Configuration).to receive(:default_override_source)
.and_return('OPENPROJECT_SCM_SUBVERSION_DISABLED__TYPES' => '[managed,unknowntype]')
OpenProject::Configuration.load
allow(instance.class).to receive(:scm_config).and_call_original
end
@ -369,8 +366,8 @@ describe Repository::Subversion, type: :model do
it_behaves_like 'repository can be relocated', :subversion
describe 'ciphering' do
it 'password is encrypted' do
OpenProject::Configuration.with 'database_cipher_key' => 'secret' do
context 'with cipher key', with_config: { 'database_cipher_key' => 'secret' } do
it 'password is encrypted' do
r = create(:repository_subversion, password: 'foo')
expect(r.password)
.to eql('foo')
@ -380,8 +377,8 @@ describe Repository::Subversion, type: :model do
end
end
it 'password is unencrypted with blank key' do
OpenProject::Configuration.with 'database_cipher_key' => '' do
context 'with blank cipher key', with_config: { 'database_cipher_key' => '' } do
it 'password is unencrypted' do
r = create(:repository_subversion, password: 'foo')
expect(r.password)
.to eql('foo')
@ -390,8 +387,8 @@ describe Repository::Subversion, type: :model do
end
end
it 'password is unencrypted with nil key' do
OpenProject::Configuration.with 'database_cipher_key' => nil do
context 'with cipher key nil', with_config: { 'database_cipher_key' => nil } do
it 'password is unencrypted' do
r = create(:repository_subversion, password: 'foo')
expect(r.password)
@ -401,28 +398,35 @@ describe Repository::Subversion, type: :model do
end
end
it 'unciphered password is readable if activating cipher later' do
OpenProject::Configuration.with 'database_cipher_key' => nil do
context 'without a cipher key first but activating it later' do
before do
WithConfig.new(self).stub_key(:database_cipher_key, nil)
create(:repository_subversion, password: 'clear')
end
OpenProject::Configuration.with 'database_cipher_key' => 'secret' do
r = Repository.last
WithConfig.new(self).stub_key(:database_cipher_key, 'secret')
end
expect(r.password)
it 'unciphered password is readable' do
expect(Repository.last.password)
.to eql('clear')
end
end
context '#encrypt_all' do
it 'encrypts formerly unencrypted passwords' do
Repository.delete_all
OpenProject::Configuration.with 'database_cipher_key' => nil do
describe '#encrypt_all' do
context 'with unencrypted passwords first but then with an encryption key' do
before do
Repository.delete_all
WithConfig.new(self).stub_key(:database_cipher_key, nil)
create(:repository_subversion, password: 'foo')
create(:repository_subversion, password: 'bar')
WithConfig.new(self).stub_key(:database_cipher_key, 'secret')
end
OpenProject::Configuration.with 'database_cipher_key' => 'secret' do
it 'encrypts formerly unencrypted passwords' do
expect(Repository.encrypt_all(:password))
.to be_truthy
@ -443,30 +447,29 @@ describe Repository::Subversion, type: :model do
end
end
context '#decrypt_all' do
describe '#decrypt_all', with_config: { 'database_cipher_key' => 'secret' } do
it 'removes cyphering from all passwords' do
Repository.delete_all
OpenProject::Configuration.with 'database_cipher_key' => 'secret' do
foo = create(:repository_subversion, password: 'foo')
bar = create(:repository_subversion, password: 'bar')
expect(Repository.decrypt_all(:password))
.to be_truthy
foo = create(:repository_subversion, password: 'foo')
bar = create(:repository_subversion, password: 'bar')
bar.reload
expect(Repository.decrypt_all(:password))
.to be_truthy
expect(bar.password)
.to eql('bar')
expect(bar.read_attribute(:password))
.to eql('bar')
bar.reload
foo.reload
expect(bar.password)
.to eql('bar')
expect(bar.read_attribute(:password))
.to eql('bar')
expect(foo.password)
.to eql('foo')
expect(foo.read_attribute(:password))
.to eql('foo')
end
foo.reload
expect(foo.password)
.to eql('foo')
expect(foo.read_attribute(:password))
.to eql('foo')
end
end
end

@ -63,6 +63,21 @@ describe Setting, type: :model do
expect(described_class.host_name).to eq 'some name'
end
context 'when overwritten' do
let!(:setting_definition) do
Settings::Definition[:host_name].tap do |setting|
allow(setting)
.to receive(:writable?)
.and_return false
end
end
it 'takes the setting from the definition' do
expect(described_class.host_name)
.to eql setting_definition.value
end
end
it 'stores the setting' do
expect(described_class.find_by(name: 'host_name').value).to eq 'some name'
end
@ -74,6 +89,10 @@ describe Setting, type: :model do
described_class.host_name = 'some other name'
end
after do
described_class.find_by(name: 'host_name').destroy
end
it 'sets the setting' do
expect(described_class.host_name).to eq 'some other name'
end
@ -81,9 +100,31 @@ describe Setting, type: :model do
it 'stores the setting' do
expect(described_class.find_by(name: 'host_name').value).to eq 'some other name'
end
end
end
describe '.[setting]_writable?' do
before do
allow(Settings::Definition[:host_name])
.to receive(:writable?)
.and_return writable
end
context 'when definition states it to be writable' do
let(:writable) { true }
it 'is writable' do
expect(described_class)
.to be_host_name_writable
end
end
after do
described_class.find_by(name: 'host_name').destroy
context 'when definition states it to be non writable' do
let(:writable) { false }
it 'is non writable' do
expect(described_class)
.not_to be_host_name_writable
end
end
end

@ -101,18 +101,6 @@ describe 'account/register', type: :view do
expect(rendered).to include(footer)
end
context 'with a registration footer in the OpenProject configuration' do
before do
allow(OpenProject::Configuration).to receive(:registration_footer).and_return("en" => footer.reverse)
end
it 'should render the registration footer from the configuration, overriding the settings' do
render
expect(rendered).to include(footer.reverse)
end
end
end
context "with consent required", with_settings: {

Loading…
Cancel
Save