mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
1877 lines
61 KiB
Ruby
1877 lines
61 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 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.
|
|
#++
|
|
|
|
# rubocop:disable Metrics/CollectionLiteralLength
|
|
module Settings
|
|
class Definition
|
|
ENV_PREFIX = "OPENPROJECT_"
|
|
AR_BOOLEAN_TYPE = ActiveRecord::Type::Boolean.new
|
|
DEFINITIONS = {
|
|
activity_days_default: {
|
|
default: 30
|
|
},
|
|
after_first_login_redirect_url: {
|
|
format: :string,
|
|
description: "URL users logging in for the first time will be redirected to (e.g., a help screen)",
|
|
default: nil
|
|
},
|
|
after_login_default_redirect_url: {
|
|
description: "Override URL to which logged in users are redirected instead of the Home page",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
allowed_link_protocols: {
|
|
format: :array,
|
|
description: "Allowed protocols for links in the WYSIWYG editor and formatted texts",
|
|
default: []
|
|
},
|
|
apiv3_cors_enabled: {
|
|
description: "Enable CORS headers for APIv3 server responses",
|
|
default: false
|
|
},
|
|
apiv3_cors_origins: {
|
|
default: []
|
|
},
|
|
apiv3_docs_enabled: {
|
|
description: "Enable interactive APIv3 documentation as part of the application",
|
|
default: false
|
|
},
|
|
apiv3_enable_basic_auth: {
|
|
description: "Enable API token or global basic authentication for APIv3 requests",
|
|
default: true
|
|
},
|
|
apiv3_max_page_size: {
|
|
default: 1000
|
|
},
|
|
apiv3_write_readonly_attributes: {
|
|
description: "Allow overriding readonly attributes (e.g. createdAt, updatedAt, author) " +
|
|
"during the creation of resources via the REST API",
|
|
default: false
|
|
},
|
|
app_title: {
|
|
default: "OpenProject"
|
|
},
|
|
organization_name: {
|
|
default: "My Organization"
|
|
},
|
|
attachment_default_charset: {
|
|
description: "Fallback charset used when serving text attachments whose encoding was not detected on upload",
|
|
format: :string,
|
|
default: "utf-8"
|
|
},
|
|
attachment_max_size: {
|
|
default: 5120
|
|
},
|
|
# Existing setting
|
|
attachment_whitelist: {
|
|
default: []
|
|
},
|
|
##
|
|
# Carrierwave storage type. Possible values are, among others, :file and :fog.
|
|
# The latter requires further configuration.
|
|
attachments_storage: {
|
|
description: "File storage configuration",
|
|
default: :file,
|
|
format: :symbol,
|
|
allowed: %i[file fog],
|
|
writable: false
|
|
},
|
|
attachments_storage_path: {
|
|
description: "File storage disk location (only applicable for local file storage)",
|
|
format: :string,
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
attachments_grace_period: {
|
|
description: "Time in minutes to wait before uploaded files not attached to any container are removed",
|
|
default: 180
|
|
},
|
|
antivirus_scan_available: {
|
|
description: "Virus scanning option selectable in the UI",
|
|
default: true
|
|
},
|
|
antivirus_scan_mode: {
|
|
description: "Virus scanning option for files uploaded to OpenProject",
|
|
format: :symbol,
|
|
default: :disabled,
|
|
allowed: %i[disabled clamav_socket clamav_host]
|
|
},
|
|
antivirus_scan_target: {
|
|
description: "The socket or hostname to connect to ClamAV",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
antivirus_scan_action: {
|
|
description: "Virus scanning action for found infected files",
|
|
format: :symbol,
|
|
default: :quarantine,
|
|
allowed: %i[quarantine delete]
|
|
},
|
|
api_tokens_enabled: {
|
|
default: true,
|
|
description: "Decide whether users can create personal API tokens in their account settings",
|
|
# Keeping old name only for backwards-compatibility, can be removed in OpenProject 18.0
|
|
env_alias: "OPENPROJECT_REST__API__ENABLED",
|
|
format: :boolean
|
|
},
|
|
auth_source_sso: {
|
|
description: "Configuration for Header-based Single Sign-On",
|
|
format: :hash,
|
|
default: nil,
|
|
writable: false # config is cached globally so let's make it not writable
|
|
},
|
|
# Configures the authentication capabilities supported by the instance.
|
|
# Currently this is focused on the configuration for basic auth.
|
|
# e.g.
|
|
# authentication:
|
|
# global_basic_auth:
|
|
# user: admin
|
|
# password: 123456
|
|
authentication: {
|
|
description: "Configuration options for global basic auth",
|
|
format: :hash,
|
|
default: nil
|
|
},
|
|
autofetch_changesets: {
|
|
default: true
|
|
},
|
|
# autologin duration in days
|
|
# 0 means autologin is disabled
|
|
autologin: {
|
|
format: :integer,
|
|
default: 0,
|
|
allowed: [1, 7, 14, 30, 60, 90, 365]
|
|
},
|
|
autologin_cookie_name: {
|
|
description: "Cookie name for autologin cookie",
|
|
default: "autologin"
|
|
},
|
|
autologin_cookie_path: {
|
|
description: "Cookie path for autologin cookie",
|
|
default: "/"
|
|
},
|
|
available_languages: {
|
|
format: :array,
|
|
# Manually managed list with languages that have ~50+ translation ratio in Crowdin
|
|
# https://crowdin.com/project/openproject
|
|
default: %w[ca cs de el en es fr hu id it ja ko lt nl no pl pt-BR pt-PT ro ru sk sl sv tr uk vi zh-CN zh-TW].freeze,
|
|
allowed: -> { Redmine::I18n.all_languages }
|
|
},
|
|
avatar_link_expiration_seconds: {
|
|
description: "Cache duration for avatar image API responses",
|
|
default: 24.hours.to_i,
|
|
env_alias: "OPENPROJECT_AVATAR__LINK__EXPIRY__SECONDS"
|
|
},
|
|
# Allow users with the required permissions to create backups via the web interface or API.
|
|
backup_enabled: {
|
|
description: "Enable application backups through the UI",
|
|
default: true
|
|
},
|
|
backup_daily_limit: {
|
|
description: "Maximum number of application backups allowed per day",
|
|
default: 3
|
|
},
|
|
backup_initial_waiting_period: {
|
|
description: "Wait time before newly created backup tokens are usable",
|
|
default: 24.hours,
|
|
format: :integer
|
|
},
|
|
backup_include_attachments: {
|
|
description: "Allow inclusion of attachments in application backups",
|
|
default: true
|
|
},
|
|
backup_attachment_size_max_sum_mb: {
|
|
description: "Maximum limit of attachment size to include into application backups",
|
|
default: 1024
|
|
},
|
|
blacklisted_routes: {
|
|
description: "Blocked routes to prevent access to certain modules or pages",
|
|
default: [],
|
|
writable: false # used in initializer
|
|
},
|
|
bcc_recipients: {
|
|
default: true
|
|
},
|
|
boards_demo_data_available: {
|
|
description: "Internal setting determining availability of demo seed data",
|
|
default: false
|
|
},
|
|
brute_force_block_minutes: {
|
|
description: "Number of minutes to block users after presumed brute force attack",
|
|
default: 30
|
|
},
|
|
brute_force_block_after_failed_logins: {
|
|
description: "Number of login attempts per user before assuming brute force attack",
|
|
default: 20
|
|
},
|
|
cache_expires_in_seconds: {
|
|
description: "Expiration time for memcache entries, empty for no expiration be default",
|
|
format: :integer,
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
cache_formatted_text: {
|
|
default: true
|
|
},
|
|
# use dalli defaults for memcache
|
|
cache_memcache_server: {
|
|
description: "The memcache server host and IP",
|
|
format: :string,
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
cache_redis_url: {
|
|
description: "URL to the redis cache server",
|
|
format: :string,
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
cache_namespace: {
|
|
format: :string,
|
|
description: "Namespace for cache keys, useful when multiple applications use a single memcache server",
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
total_percent_complete_mode: {
|
|
description: "Mode in which the total % Complete for work packages in a hierarchy is calculated",
|
|
default: "work_weighted_average",
|
|
allowed: %w[work_weighted_average simple_average]
|
|
},
|
|
commit_fix_keywords: {
|
|
description: "Keywords to look for in commit for fixing work packages",
|
|
default: "fixes,closes"
|
|
},
|
|
commit_fix_status_id: {
|
|
description: "Assigned status when fixing keyword is found",
|
|
format: :integer,
|
|
default: nil,
|
|
allowed: -> { Status.pluck(:id) + [nil] }
|
|
},
|
|
commit_logs_encoding: {
|
|
description: "Encoding used to convert commit logs to UTF-8",
|
|
default: "UTF-8"
|
|
},
|
|
commit_logtime_activity_id: {
|
|
description: :setting_commit_logtime_activity_id,
|
|
format: :integer,
|
|
default: nil,
|
|
allowed: -> { TimeEntryActivity.pluck(:id) + [nil] }
|
|
},
|
|
commit_logtime_enabled: {
|
|
description: "Allow logging time through commit message",
|
|
default: false
|
|
},
|
|
commit_ref_keywords: {
|
|
description: "Keywords used in commits for referencing work packages",
|
|
default: "refs,references,IssueID"
|
|
},
|
|
consent_decline_mail: {
|
|
format: :string,
|
|
default: 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.
|
|
consent_time: {
|
|
default: nil,
|
|
format: :datetime
|
|
},
|
|
# Additional info about what the user is consenting to (optional).
|
|
consent_info: {
|
|
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: false
|
|
},
|
|
cross_project_work_package_relations: {
|
|
default: true
|
|
},
|
|
database_cipher_key: {
|
|
description: "Encryption key for repository credentials",
|
|
format: :string,
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
date_format: {
|
|
format: :string,
|
|
default: 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
|
|
},
|
|
days_per_month: {
|
|
description: "This will define what is considered a “month” when displaying duration in a more natural way " \
|
|
"(for example, if a month is 20 days, 60 days would be 3 months.",
|
|
default: 20,
|
|
format: :integer
|
|
},
|
|
default_auto_hide_popups: {
|
|
description: "Whether to automatically hide success notifications by default",
|
|
default: true
|
|
},
|
|
# user configuration
|
|
default_comment_sort_order: {
|
|
description: "Default sort order for activities",
|
|
default: "asc"
|
|
},
|
|
disable_keyboard_shortcuts: {
|
|
description: "Whether keyboard short cuts should be disabled (e.g. for better screen reader support)",
|
|
default: false
|
|
},
|
|
default_language: {
|
|
default: "en",
|
|
allowed: -> { Redmine::I18n.all_languages }
|
|
},
|
|
default_projects_modules: {
|
|
default: -> {
|
|
base_modules = %w[calendar board_view work_package_tracking gantt news costs wiki]
|
|
if Setting.real_time_text_collaboration_enabled?
|
|
base_modules + %w[documents]
|
|
else
|
|
base_modules
|
|
end
|
|
},
|
|
allowed: -> { OpenProject::AccessControl.available_project_modules.map(&:to_s) }
|
|
},
|
|
default_projects_public: {
|
|
default: false
|
|
},
|
|
demo_projects_available: {
|
|
default: false
|
|
},
|
|
demo_view_of_type_work_packages_table_seeded: {
|
|
default: false
|
|
},
|
|
demo_view_of_type_team_planner_seeded: {
|
|
default: false
|
|
},
|
|
demo_view_of_type_gantt_seeded: {
|
|
default: false
|
|
},
|
|
development_highlight_enabled: {
|
|
description: "Enable highlighting of development environment",
|
|
default: -> { Rails.env.development? },
|
|
format: :boolean
|
|
},
|
|
diff_max_lines_displayed: {
|
|
default: 1500
|
|
},
|
|
direct_uploads: {
|
|
description: "Enable direct uploads to AWS S3. Only applicable with enabled Fog / AWS S3 configuration",
|
|
default: true,
|
|
writable: false
|
|
},
|
|
disable_browser_cache: {
|
|
description: "Prevent browser from caching any logged-in responses for security reasons",
|
|
default: true,
|
|
writable: false
|
|
},
|
|
# allow to disable default modules
|
|
disabled_modules: {
|
|
description: "A list of module names to prevent access to in the application",
|
|
default: [],
|
|
allowed: -> { OpenProject::AccessControl.available_project_modules.map(&:to_s) },
|
|
writable: false # setting stored in global variable
|
|
},
|
|
disable_password_choice: {
|
|
description: "If enabled a user's password cannot be set to an arbitrary value, but can only be randomized.",
|
|
default: false
|
|
},
|
|
disable_password_login: {
|
|
description: "Disable internal logins and instead only allow SSO through OmniAuth.",
|
|
default: false
|
|
},
|
|
display_subprojects_work_packages: {
|
|
default: true
|
|
},
|
|
drop_old_sessions_on_logout: {
|
|
description: "Destroy all sessions for current_user on logout",
|
|
default: true
|
|
},
|
|
drop_old_sessions_on_login: {
|
|
description: "Destroy all sessions for current_user on login",
|
|
default: false
|
|
},
|
|
duration_format: {
|
|
description: "Format for displaying durations",
|
|
default: "hours_only",
|
|
allowed: %w[days_and_hours hours_only]
|
|
},
|
|
edition: {
|
|
format: :string,
|
|
default: "standard",
|
|
description: "OpenProject edition mode",
|
|
writable: false,
|
|
allowed: %w[standard bim]
|
|
},
|
|
ee_manager_visible: {
|
|
description: "Show the Enterprise configuration page",
|
|
default: true,
|
|
writable: false
|
|
},
|
|
ee_hide_banners: {
|
|
description: "Hide the Enterprise enterprise banners",
|
|
default: false
|
|
},
|
|
enable_internal_assets_server: {
|
|
description: "Serve assets through the Rails internal asset server",
|
|
default: false,
|
|
writable: false
|
|
},
|
|
# email configuration
|
|
email_delivery_configuration: {
|
|
default: "inapp",
|
|
allowed: %w[inapp legacy],
|
|
writable: false,
|
|
env_alias: "EMAIL_DELIVERY_CONFIGURATION"
|
|
},
|
|
email_delivery_method: {
|
|
format: :symbol,
|
|
default: nil,
|
|
env_alias: "EMAIL_DELIVERY_METHOD"
|
|
},
|
|
emails_salutation: {
|
|
allowed: %w[firstname name],
|
|
default: :firstname
|
|
},
|
|
emails_footer: {
|
|
default: {
|
|
"en" => ""
|
|
}
|
|
},
|
|
emails_header: {
|
|
default: {
|
|
"en" => ""
|
|
}
|
|
},
|
|
# use email address as login, hide login in registration form
|
|
email_login: {
|
|
default: false
|
|
},
|
|
enabled_projects_columns: {
|
|
default: %w[favorited name project_status public created_at latest_activity_at required_disk_space],
|
|
allowed: -> { ProjectQuery.new.available_selects.map { |s| s.attribute.to_s } }
|
|
},
|
|
enabled_scm: {
|
|
default: %w[subversion git]
|
|
},
|
|
# Allow connections for trial creation and booking
|
|
enterprise_trial_creation_host: {
|
|
description: "Host for EE trial service",
|
|
default: "https://start.openproject.com",
|
|
writable: false
|
|
},
|
|
enterprise_chargebee_site: {
|
|
description: "Site name for EE trial service",
|
|
default: "openproject-enterprise",
|
|
writable: false
|
|
},
|
|
enterprise_plan: {
|
|
description: "Default EE selected plan",
|
|
default: "enterprise-on-premises---basic---euro---1-year",
|
|
writable: false
|
|
},
|
|
feeds_enabled: {
|
|
default: true
|
|
},
|
|
feeds_limit: {
|
|
default: 15
|
|
},
|
|
# Maximum size of files that can be displayed
|
|
# inline through the file viewer (in KB)
|
|
file_max_size_displayed: {
|
|
default: 512
|
|
},
|
|
first_week_of_year: {
|
|
default: nil,
|
|
format: :integer,
|
|
allowed: [1, 4]
|
|
},
|
|
fog: {
|
|
description: "Configure fog, e.g. when using an S3 uploader",
|
|
default: {}
|
|
},
|
|
fog_download_url_expires_in: {
|
|
description: "Expiration time in seconds of created shared presigned URLs",
|
|
default: 21600 # 6h by default as 6 hours is max in S3 when using IAM roles
|
|
},
|
|
# Additional / overridden help links
|
|
force_help_link: {
|
|
description: "You can set a custom URL for the help button in application header menu.",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
force_formatting_help_link: {
|
|
description: "You can set a custom URL for the help button in the WYSIWYG editor.",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
forced_single_page_size: {
|
|
description: "Forced page size for manually sorted work package views",
|
|
default: 250
|
|
},
|
|
good_job_queues: {
|
|
description: "",
|
|
format: :string,
|
|
writable: false,
|
|
default: "*"
|
|
},
|
|
good_job_max_threads: {
|
|
description: "",
|
|
format: :integer,
|
|
writable: false,
|
|
default: 20
|
|
},
|
|
good_job_max_cache: {
|
|
description: "",
|
|
format: :integer,
|
|
writable: false,
|
|
default: 10_000
|
|
},
|
|
good_job_enable_cron: {
|
|
description: "",
|
|
format: :boolean,
|
|
writable: false,
|
|
default: true
|
|
},
|
|
good_job_cleanup_preserved_jobs_before_seconds_ago: {
|
|
description: "",
|
|
format: :integer,
|
|
writable: false,
|
|
default: 7.days
|
|
},
|
|
good_job_engine_basic_auth: {
|
|
description: "Allow basic authentication for GoodJob web interface by setting a password",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
hashed_token_pepper: {
|
|
description: "Pepper used for HMAC-SHA256 hashing of hashed tokens (e.g. API tokens). " \
|
|
"Auto-initialized on first use. " \
|
|
"Changing this invalidates all existing hashed tokens.",
|
|
format: :string,
|
|
default: -> { SecureRandom.hex(32) },
|
|
persist_on_first_read: true
|
|
},
|
|
host_name: {
|
|
format: :string,
|
|
default: -> { "#{ENV.fetch('HOST', 'localhost')}:#{ENV.fetch('PORT', 3000)}" },
|
|
default_by_env: {
|
|
# We do not want to set a localhost host name in production
|
|
production: nil
|
|
}
|
|
},
|
|
additional_host_names: {
|
|
description: "Additional allowed host names for the application.",
|
|
default: []
|
|
},
|
|
real_time_text_collaboration_enabled: {
|
|
description: "Enable real-time collaborative editing of text fields using BlockNoteJS and Hocuspocus server.",
|
|
default: -> {
|
|
Setting.collaborative_editing_hocuspocus_url.present? &&
|
|
Setting.collaborative_editing_hocuspocus_secret.present?
|
|
}
|
|
},
|
|
collaborative_editing_hocuspocus_url: {
|
|
format: :string,
|
|
default: nil,
|
|
description: "The URL of the hocuspocus server used by BlockNoteJS editor to enable collaborative editing.",
|
|
default_by_env: {
|
|
development: "wss://hocuspocus.local"
|
|
}
|
|
},
|
|
collaborative_editing_hocuspocus_secret: {
|
|
format: :string,
|
|
default: nil,
|
|
default_by_env: {
|
|
development: "secret12345"
|
|
},
|
|
description: "The secret used for generating access tokens to access documents on hocuspocus server."
|
|
},
|
|
hours_per_day: {
|
|
description: "This will define what is considered a “day” when displaying duration in a more natural way " \
|
|
"(for example, if a day is 8 hours, 32 hours would be 4 days).",
|
|
default: 8,
|
|
format: :integer
|
|
},
|
|
# Health check configuration
|
|
health_checks_authentication_password: {
|
|
description: "Add an authentication challenge for the /health_check endpoint",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
## Maximum number of minutes that jobs have not yet run after their designated 'run_at' time
|
|
health_checks_jobs_never_ran_minutes_ago: {
|
|
description: "Set threshold of outstanding background jobs to fail health check",
|
|
format: :integer,
|
|
default: 5
|
|
},
|
|
## Maximum number of unprocessed requests in puma's backlog.
|
|
health_checks_backlog_threshold: {
|
|
description: "Set threshold of outstanding HTTP requests to fail health check",
|
|
format: :integer,
|
|
default: 20
|
|
},
|
|
# Default gravatar image, set to something other than 404
|
|
# to ensure a default is returned
|
|
gravatar_fallback_image: {
|
|
description: "Set default gravatar image fallback",
|
|
default: "404"
|
|
},
|
|
hidden_menu_items: {
|
|
description: "Hide menu items in the menu sidebar for each main menu (such as Administration and Projects).",
|
|
default: {},
|
|
writable: false # cached in global variable
|
|
},
|
|
impressum_link: {
|
|
description: "Impressum link to be set, hidden by default",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
installation_type: {
|
|
default: "manual",
|
|
writable: false
|
|
},
|
|
installation_uuid: {
|
|
format: :string,
|
|
default: -> { SecureRandom.uuid },
|
|
persist_on_first_read: true,
|
|
default_by_env: {
|
|
test: "test_uuid"
|
|
}
|
|
},
|
|
internal_password_confirmation: {
|
|
description: "Require password confirmations for certain administrative actions",
|
|
default: true
|
|
},
|
|
invitation_expiration_days: {
|
|
default: 7
|
|
},
|
|
journal_aggregation_time_minutes: {
|
|
default: 5
|
|
},
|
|
ldap_force_no_page: {
|
|
description: "Force LDAP to respond as a single page, in case paged responses do not work with your server.",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
ldap_groups_disable_sync_job: {
|
|
description: "Deactivate regular synchronization job for groups in case scheduled as a separate cronjob",
|
|
default: false
|
|
},
|
|
ldap_users_disable_sync_job: {
|
|
description: "Deactivate user attributes synchronization from LDAP",
|
|
default: false
|
|
},
|
|
ldap_users_sync_status: {
|
|
description: "Enable user status (locked/unlocked) synchronization from LDAP",
|
|
format: :boolean,
|
|
default: false
|
|
},
|
|
log_level: {
|
|
description: "Set the OpenProject logger level",
|
|
default: Rails.env.development? ? "debug" : "info",
|
|
allowed: %w[debug info warn error fatal],
|
|
writable: false
|
|
},
|
|
log_requesting_user: {
|
|
default: false
|
|
},
|
|
lograge_enabled: {
|
|
description: "Use lograge formatter for outputting logs",
|
|
default: true,
|
|
format: :boolean,
|
|
writable: false
|
|
},
|
|
lograge_formatter: {
|
|
description: "Lograge formatter to use for outputting logs",
|
|
default: "key_value",
|
|
format: :string,
|
|
writable: false
|
|
},
|
|
login_required: {
|
|
default: true
|
|
},
|
|
lookbook_enabled: {
|
|
description: "Enable the Lookbook component documentation tool. Discouraged for production environments.",
|
|
default: -> { Rails.env.development? },
|
|
format: :boolean
|
|
},
|
|
lost_password: {
|
|
description: "Activate or deactivate lost password form",
|
|
default: true
|
|
},
|
|
mail_from: {
|
|
default: "openproject@example.net"
|
|
},
|
|
mail_handler_api_key: {
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
mail_handler_body_delimiters: {
|
|
default: ""
|
|
},
|
|
mail_handler_body_delimiter_regex: {
|
|
default: ""
|
|
},
|
|
mail_handler_ignore_filenames: {
|
|
default: "signature.asc"
|
|
},
|
|
mail_suffix_separators: {
|
|
default: "+"
|
|
},
|
|
main_content_language: {
|
|
default: "english",
|
|
description: "Main content language for PostgreSQL full text features",
|
|
writable: false,
|
|
allowed: %w[danish dutch english finnish french german hungarian
|
|
italian norwegian portuguese romanian russian simple spanish swedish turkish]
|
|
},
|
|
mcp_tool_response_format: {
|
|
default: :full,
|
|
format: :symbol,
|
|
allowed: -> { McpTools::Base::RESPONSE_FORMATS },
|
|
description: "How to format responses for MCP tools. Using values other than full may improve language model performance."
|
|
},
|
|
migration_check_on_exceptions: {
|
|
description: "Check for missing migrations in internal errors",
|
|
default: true,
|
|
writable: false
|
|
},
|
|
# Role given to a non-admin user who creates a project
|
|
new_project_user_role_id: {
|
|
format: :integer,
|
|
default: nil,
|
|
allowed: -> { Role.pluck(:id) }
|
|
},
|
|
new_project_send_confirmation_email: {
|
|
format: :boolean,
|
|
default: false
|
|
},
|
|
new_project_notification_text: {
|
|
format: :string,
|
|
default: ""
|
|
},
|
|
notifications_hidden: {
|
|
default: false
|
|
},
|
|
notifications_polling_interval: {
|
|
format: :integer,
|
|
default: 60000
|
|
},
|
|
oauth_allow_remapping_of_existing_users: {
|
|
description: "When set to false, prevent users from other identity providers to take over accounts " \
|
|
"that exist in OpenProject.",
|
|
format: :boolean,
|
|
default: true
|
|
},
|
|
omniauth_direct_login_provider: {
|
|
description: "Clicking on login sends a login request to the specified OmniAuth provider.",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
override_bcrypt_cost_factor: {
|
|
description: "Set a custom BCrypt cost factor for deriving a user's bcrypt hash.",
|
|
format: :string,
|
|
default: nil,
|
|
writable: false # this changes a global variable and must therefore not be writable at runtime
|
|
},
|
|
onboarding_enabled: {
|
|
description: "Enable or disable onboarding guided tour for new users",
|
|
default: true
|
|
},
|
|
password_active_rules: {
|
|
default: %w[lowercase uppercase numeric special],
|
|
default_by_env: {
|
|
test: []
|
|
},
|
|
allowed: %w[lowercase uppercase numeric special]
|
|
},
|
|
password_count_former_banned: {
|
|
default: 0
|
|
},
|
|
password_days_valid: {
|
|
default: 0
|
|
},
|
|
password_min_length: {
|
|
default: 10,
|
|
format: :integer,
|
|
allowed: -> { 1..Setting::PASSWORD_MAX_LENGTH }
|
|
},
|
|
# TODO: turn into array of ints
|
|
# Requires a migration to be written
|
|
# replace Setting#per_page_options_array
|
|
per_page_options: {
|
|
default: "20, 100"
|
|
},
|
|
percent_complete_on_status_closed: {
|
|
description: "Describes how % complete should change when setting a work package status to a closed one",
|
|
default: "no_change",
|
|
allowed: %w[no_change set_100p]
|
|
},
|
|
plain_text_mail: {
|
|
default: false
|
|
},
|
|
project_gantt_query: {
|
|
default: nil,
|
|
format: :string
|
|
},
|
|
rails_asset_host: {
|
|
description: "Custom asset hostname for serving assets (e.g., Cloudfront)",
|
|
format: :string,
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
rails_cache_store: {
|
|
description: "Set cache store implementation to use with OpenProject",
|
|
format: :symbol,
|
|
default: :file_store,
|
|
writable: false,
|
|
allowed: %i[file_store memcache redis]
|
|
},
|
|
rails_relative_url_root: {
|
|
description: "Set a URL prefix / base path to run OpenProject under, e.g., host.tld/openproject",
|
|
default: "",
|
|
writable: false
|
|
},
|
|
show_work_package_attachments: {
|
|
description: "Show work package attachments by default.",
|
|
format: :boolean,
|
|
default: true,
|
|
writable: true
|
|
},
|
|
https: {
|
|
description: "Set assumed connection security for the Rails processes",
|
|
format: :boolean,
|
|
default: -> { Rails.env.production? },
|
|
writable: false
|
|
},
|
|
hsts: {
|
|
description: "Allow disabling of HSTS headers and http -> https redirects",
|
|
format: :boolean,
|
|
default: true,
|
|
writable: false
|
|
},
|
|
home_url: {
|
|
description: "Override default link when clicking on the top menu logo (Homescreen by default).",
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
httpx_connect_timeout: {
|
|
description: "",
|
|
format: :float,
|
|
writable: false,
|
|
allowed: (0..),
|
|
default: 3
|
|
},
|
|
httpx_operation_timeout: {
|
|
description: "",
|
|
format: :float,
|
|
writable: false,
|
|
allowed: (0..),
|
|
default: 10
|
|
},
|
|
httpx_request_timeout: {
|
|
description: "",
|
|
format: :float,
|
|
writable: false,
|
|
allowed: (0..),
|
|
default: 10
|
|
},
|
|
httpx_read_timeout: {
|
|
description: "",
|
|
format: :float,
|
|
writable: false,
|
|
allowed: (0..),
|
|
default: 3
|
|
},
|
|
httpx_write_timeout: {
|
|
description: "",
|
|
format: :float,
|
|
writable: false,
|
|
allowed: (0..),
|
|
default: 3
|
|
},
|
|
httpx_keep_alive_timeout: {
|
|
description: "",
|
|
format: :float,
|
|
writable: false,
|
|
allowed: (0..),
|
|
default: 20
|
|
},
|
|
opentelemetry_enabled: {
|
|
description: "Enable OpenTelemetry metrics",
|
|
default: false
|
|
},
|
|
rate_limiting: {
|
|
default: {},
|
|
description: "Configure rate limiting for various endpoint rules. See configuration documentation for details."
|
|
},
|
|
registration_footer: {
|
|
default: {
|
|
"en" => ""
|
|
}
|
|
},
|
|
remote_storage_upload_host: {
|
|
format: :string,
|
|
default: nil,
|
|
writable: false,
|
|
description: "Host the frontend uses to upload files to, which has to be added to the CSP."
|
|
},
|
|
remote_storage_download_host: {
|
|
format: :string,
|
|
default: nil,
|
|
writable: false,
|
|
description: "Host the frontend uses to download files, which has to be added to the CSP."
|
|
},
|
|
# Content Security Policy
|
|
csp_img_src: {
|
|
format: :array,
|
|
default: %w(* data: blob:),
|
|
writable: false,
|
|
description: "Allowed sources for the CSP img-src directive."
|
|
},
|
|
report_incoming_email_errors: {
|
|
description: "Respond to incoming mails with error details",
|
|
default: true
|
|
},
|
|
repositories_automatic_managed_vendor: {
|
|
default: 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
|
|
repositories_encodings: {
|
|
default: nil,
|
|
format: :string
|
|
},
|
|
repository_checkout_data: {
|
|
default: {
|
|
"git" => { "enabled" => 0 },
|
|
"subversion" => { "enabled" => 0 }
|
|
}
|
|
},
|
|
repository_log_display_limit: {
|
|
default: 100
|
|
},
|
|
repository_storage_cache_minutes: {
|
|
default: 720
|
|
},
|
|
repository_truncate_at: {
|
|
default: 500
|
|
},
|
|
scm: {
|
|
format: :hash,
|
|
default: {},
|
|
writable: false
|
|
},
|
|
scm_git_command: {
|
|
format: :string,
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
scm_local_checkout_path: {
|
|
default: "repositories", # relative to OpenProject directory
|
|
writable: false
|
|
},
|
|
scm_subversion_command: {
|
|
format: :string,
|
|
default: nil,
|
|
writable: false
|
|
},
|
|
# Display update / security badge, enabled by default
|
|
security_badge_displayed: {
|
|
default: true
|
|
},
|
|
security_badge_url: {
|
|
description: "URL of the update check badge",
|
|
default: "https://releases.openproject.com/v1/check.svg",
|
|
writable: false
|
|
},
|
|
seed_admin_user_locked: {
|
|
description: "Lock the created admin user after seeding, so it can not be used for logging in. " \
|
|
"If set to true, an admin user has to be created manually or through an SSO provider.",
|
|
default: false,
|
|
writable: false
|
|
},
|
|
seed_admin_user_password: {
|
|
description: 'Password to set for the initially created admin user (Login remains "admin").',
|
|
default: "admin",
|
|
writable: false
|
|
},
|
|
seed_admin_user_mail: {
|
|
description: "E-mail to set for the initially created admin user.",
|
|
default: "admin@example.net",
|
|
writable: false
|
|
},
|
|
seed_admin_user_name: {
|
|
description: "Name to set for the initially created admin user.",
|
|
default: "OpenProject Admin",
|
|
writable: false
|
|
},
|
|
seed_admin_user_password_reset: {
|
|
description: "Whether to force a password reset for the initially created admin user.",
|
|
default: true,
|
|
writable: false
|
|
},
|
|
seed_ldap: {
|
|
description: "Provide an LDAP connection and sync settings through ENV",
|
|
writable: false,
|
|
default: nil,
|
|
format: :hash,
|
|
string_values: true
|
|
},
|
|
seed_design: {
|
|
description: "Seed enterprise-edition theme colors and logos through ENV",
|
|
writable: false,
|
|
default: nil,
|
|
format: :hash,
|
|
string_values: true
|
|
},
|
|
seed_enterprise_token: {
|
|
description: "Seed enterprise-edition token through ENV",
|
|
writable: false,
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
self_registration: {
|
|
default: 2,
|
|
format: :integer
|
|
},
|
|
sendmail_arguments: {
|
|
description: "Arguments to call sendmail with in case it is configured as outgoing email setup",
|
|
format: :string,
|
|
writable: false,
|
|
default: "-i"
|
|
},
|
|
sendmail_location: {
|
|
description: "Location of sendmail to call if it is configured as outgoing email setup",
|
|
format: :string,
|
|
writable: false,
|
|
default: "/usr/sbin/sendmail"
|
|
},
|
|
# Allow separate error reporting for frontend errors
|
|
appsignal_frontend_key: {
|
|
format: :string,
|
|
default: nil,
|
|
description: "Appsignal API key for JavaScript error reporting"
|
|
},
|
|
session_cookie_name: {
|
|
description: "Set session cookie name",
|
|
default: "_open_project_session"
|
|
},
|
|
session_ttl_enabled: {
|
|
default: false
|
|
},
|
|
session_ttl: {
|
|
default: 120
|
|
},
|
|
show_community_links: {
|
|
description: "Enable or disable links to OpenProject community instances",
|
|
default: true
|
|
},
|
|
show_product_version: {
|
|
description: "Show product version information in the administration section",
|
|
default: true
|
|
},
|
|
show_pending_migrations_warning: {
|
|
description: "Enable or disable warning bar in case of pending migrations",
|
|
default: true,
|
|
writable: false
|
|
},
|
|
show_setting_mismatch_warning: {
|
|
description: "Show mismatched protocol/hostname warning. In cases where they must differ this can be disabled",
|
|
default: true
|
|
},
|
|
# Render storage information
|
|
show_storage_information: {
|
|
description: "Show available and taken storage information under administration / info",
|
|
default: true
|
|
},
|
|
show_warning_bars: {
|
|
description: "Render warning bars (pending migrations, deprecation, unsupported browsers)",
|
|
# Hide warning bars by default in tests as they might overlay other elements
|
|
default: -> { !Rails.env.test? }
|
|
},
|
|
smtp_authentication: {
|
|
format: :string,
|
|
default: "plain",
|
|
env_alias: "SMTP_AUTHENTICATION"
|
|
},
|
|
smtp_enable_starttls_auto: {
|
|
format: :boolean,
|
|
default: false,
|
|
env_alias: "SMTP_ENABLE_STARTTLS_AUTO"
|
|
},
|
|
smtp_openssl_verify_mode: {
|
|
description: "Globally set verify mode for OpenSSL. Careful: Setting to none will disable any SSL verification!",
|
|
format: :string,
|
|
default: "peer",
|
|
allowed: %w[none peer client_once fail_if_no_peer_cert],
|
|
writable: false
|
|
},
|
|
smtp_ssl: {
|
|
format: :boolean,
|
|
default: false,
|
|
env_alias: "SMTP_SSL"
|
|
},
|
|
smtp_address: {
|
|
format: :string,
|
|
default: "",
|
|
env_alias: "SMTP_ADDRESS"
|
|
},
|
|
smtp_domain: {
|
|
format: :string,
|
|
default: "your.domain.com",
|
|
env_alias: "SMTP_DOMAIN"
|
|
},
|
|
smtp_user_name: {
|
|
format: :string,
|
|
default: "",
|
|
env_alias: "SMTP_USER_NAME"
|
|
},
|
|
smtp_port: {
|
|
format: :integer,
|
|
default: 587,
|
|
env_alias: "SMTP_PORT"
|
|
},
|
|
smtp_password: {
|
|
format: :string,
|
|
default: "",
|
|
env_alias: "SMTP_PASSWORD"
|
|
},
|
|
smtp_timeout: {
|
|
format: :integer,
|
|
default: 5
|
|
},
|
|
software_name: {
|
|
description: "Override software application name",
|
|
default: "OpenProject"
|
|
},
|
|
software_url: {
|
|
description: "Override software application URL",
|
|
default: "https://www.openproject.org/"
|
|
},
|
|
sql_slow_query_threshold: {
|
|
description: "Time limit in ms after which queries will be logged as slow queries",
|
|
default: 2000,
|
|
writable: false
|
|
},
|
|
ssrf_protection_ip_allowlist: {
|
|
description: "
|
|
Connections to certain IP addresses (such as private ranges) are blocked to prevent SSRF attacks.
|
|
Use this setting to explicitly allow given IP addresses which would otherwise be blocked.
|
|
Takes a comma or space separated list of IPv4 and IPv6 addresses (including masks for ranges),
|
|
e.g. `192.168.255.255/16`.
|
|
|
|
Here is a list of blocked IP ranges as defined by the ssrf_filter gem used.
|
|
See [1] for the latest state in case this has changed.
|
|
|
|
0.0.0.0/8 # Current network (only valid as source address)
|
|
10.0.0.0/8 # Private network
|
|
100.64.0.0/10 # Shared Address Space
|
|
127.0.0.0/8 # Loopback
|
|
169.254.0.0/16 # Link-local
|
|
172.16.0.0/12 # Private network
|
|
192.0.0.0/24 # IETF Protocol Assignments
|
|
192.0.2.0/24 # TEST-NET-1, documentation and examples
|
|
192.88.99.0/24 # IPv6 to IPv4 relay (includes 2002::/16)
|
|
192.168.0.0/16 # Private network
|
|
198.18.0.0/15 # Network benchmark tests
|
|
198.51.100.0/24 # TEST-NET-2, documentation and examples
|
|
203.0.113.0/24 # TEST-NET-3, documentation and examples
|
|
224.0.0.0/4 # IP multicast (former Class D network)
|
|
240.0.0.0/4 # Reserved (former Class E network)
|
|
255.255.255.255 # Broadcast
|
|
|
|
::1/128 # Loopback
|
|
64:ff9b::/96 # IPv4/IPv6 translation (RFC 6052)
|
|
100::/64 # Discard prefix (RFC 6666)
|
|
2001::/32 # Teredo tunneling
|
|
2001:10::/28 # Deprecated (previously ORCHID)
|
|
2001:20::/28 # ORCHIDv2
|
|
2001:db8::/32 # Addresses used in documentation and example source code
|
|
2002::/16 # 6to4
|
|
fc00::/7 # Unique local address
|
|
fe80::/10 # Link-local address
|
|
ff00::/8 # Multicast
|
|
|
|
[1] https://github.com/arkadiyt/ssrf_filter/blob/main/lib/ssrf_filter/ssrf_filter.rb#L28-L58
|
|
".squish,
|
|
format: :string,
|
|
default: "",
|
|
env_alias: "SSRF_PROTECTION_IP_ALLOWLIST",
|
|
writable: false
|
|
},
|
|
start_of_week: {
|
|
default: nil,
|
|
format: :integer,
|
|
allowed: [1, 6, 7]
|
|
},
|
|
statsd: {
|
|
description: "enable statsd metrics (currently puma only) by configuring host",
|
|
default: {
|
|
"host" => nil,
|
|
"port" => 8125
|
|
},
|
|
writable: false
|
|
},
|
|
metrics: {
|
|
description: "
|
|
Publish a reduced set of puma metrics on a separate port for Prometheus consumption,
|
|
providing autoscaling hints
|
|
".squish,
|
|
default: {
|
|
"enabled" => false,
|
|
"port" => 9394
|
|
},
|
|
writable: false
|
|
},
|
|
sys_api_enabled: {
|
|
description: "Enable internal system API for setting up managed repositories",
|
|
default: false
|
|
},
|
|
sys_api_key: {
|
|
description: "Internal system API key for setting up managed repositories",
|
|
default: nil,
|
|
format: :string
|
|
},
|
|
time_format: {
|
|
format: :string,
|
|
default: nil,
|
|
allowed: [
|
|
"%H:%M",
|
|
"%I:%M %p"
|
|
].freeze
|
|
},
|
|
user_default_timezone: {
|
|
default: nil,
|
|
format: :string,
|
|
allowed: ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.canonical_identifier }.sort.uniq + [nil]
|
|
},
|
|
users_deletable_by_admins: {
|
|
default: false
|
|
},
|
|
user_default_theme: {
|
|
default: "light",
|
|
format: :string,
|
|
allowed: -> do
|
|
UserPreferences::Schema.schema.dig("definitions", "UserPreferences", "properties", "theme", "enum")
|
|
end
|
|
},
|
|
users_deletable_by_self: {
|
|
default: false
|
|
},
|
|
user_format: {
|
|
default: :firstname_lastname,
|
|
allowed: -> { User::USER_FORMATS_STRUCTURE.keys }
|
|
},
|
|
web: {
|
|
description: "Web worker count and threads configuration",
|
|
default: {
|
|
"workers" => 2,
|
|
"timeout" => Rails.env.production? ? 120 : 0,
|
|
"wait_timeout" => 30,
|
|
"min_threads" => 4,
|
|
"max_threads" => 16,
|
|
"term_on_timeout" => 1
|
|
},
|
|
writable: false
|
|
},
|
|
welcome_text: {
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
welcome_title: {
|
|
format: :string,
|
|
default: nil
|
|
},
|
|
welcome_on_homescreen: {
|
|
default: false
|
|
},
|
|
work_package_done_ratio: {
|
|
default: "field",
|
|
allowed: %w[field status]
|
|
},
|
|
work_packages_projects_export_limit: {
|
|
default: 500
|
|
},
|
|
work_packages_bulk_request_limit: {
|
|
default: 10
|
|
},
|
|
work_packages_identifier: {
|
|
description: "Defines how work packages are identified in the UI (e.g. in links and titles). " \
|
|
"The 'classic' option uses the work package numerical ID, " \
|
|
"while 'semantic' uses the project identifier and the work package ID separated by a dash " \
|
|
"(e.g. 'PROJA-123').",
|
|
format: :string,
|
|
allowed: -> { Setting::WorkPackageIdentifier::ALLOWED_VALUES },
|
|
default: "classic"
|
|
},
|
|
work_package_list_default_highlighted_attributes: {
|
|
default: ["status", "priority", "due_date"],
|
|
allowed: -> {
|
|
Query.available_columns(nil).select(&:highlightable).map(&:name).map(&:to_s)
|
|
}
|
|
},
|
|
work_package_list_default_highlighting_mode: {
|
|
format: :string,
|
|
default: -> { "inline" },
|
|
allowed: -> { Query::QUERY_HIGHLIGHTING_MODES.map(&:to_s) }
|
|
},
|
|
work_package_list_default_columns: {
|
|
default: %w[id subject type status assigned_to priority],
|
|
allowed: -> { Query.new.displayable_columns.map { |c| c.name.to_s } }
|
|
},
|
|
work_package_startdate_is_adddate: {
|
|
default: false
|
|
},
|
|
working_days: {
|
|
description: "Set working days of the week (Array of 1 to 7, where 1=Monday, 7=Sunday)",
|
|
format: :array,
|
|
allowed: Array(1..7),
|
|
default: Array(1..5) # Sat, Sun being non-working days,
|
|
},
|
|
youtube_channel: {
|
|
description: "Link to YouTube channel in help menu",
|
|
default: "https://www.youtube.com/c/OpenProjectCommunity"
|
|
},
|
|
capture_external_links: {
|
|
description: "Redirect external links through a warning page before leaving the application",
|
|
default: false,
|
|
writable: -> { EnterpriseToken.allows_to?(:capture_external_links) }
|
|
},
|
|
capture_external_links_require_login: {
|
|
description: "Require users to be logged in before being able to navigate to external links",
|
|
default: false,
|
|
writable: -> { EnterpriseToken.allows_to?(:capture_external_links) }
|
|
}
|
|
}.freeze
|
|
|
|
attr_accessor :name,
|
|
:format,
|
|
:env_alias,
|
|
:string_values,
|
|
:persist_on_first_read
|
|
|
|
attr_writer :value,
|
|
:description,
|
|
:allowed
|
|
|
|
def initialize(name, # rubocop:disable Metrics/AbcSize
|
|
default:,
|
|
default_by_env: {},
|
|
description: nil,
|
|
format: nil,
|
|
writable: true,
|
|
allowed: nil,
|
|
env_alias: nil,
|
|
string_values: false,
|
|
persist_on_first_read: false)
|
|
self.name = name.to_s
|
|
self.value = derive_default default_by_env.fetch(Rails.env.to_sym, default)
|
|
self.format = format ? format.to_sym : deduce_format(value)
|
|
self.writable = writable
|
|
self.allowed = allowed
|
|
self.env_alias = env_alias
|
|
self.description = description.presence || :"setting_#{name}"
|
|
self.string_values = string_values
|
|
self.persist_on_first_read = persist_on_first_read
|
|
|
|
if persist_on_first_read && !writable
|
|
raise ArgumentError, "Settings using persist_on_first_read need to be writable"
|
|
end
|
|
|
|
if persist_on_first_read && default.nil?
|
|
raise ArgumentError, "Settings using persist_on_first_read need to have a default value"
|
|
end
|
|
end
|
|
|
|
def env_name
|
|
self.class.env_name(self)
|
|
end
|
|
|
|
def possible_env_names
|
|
self.class.possible_env_names(self)
|
|
end
|
|
|
|
def derive_default(default)
|
|
@default = default.is_a?(Hash) ? default.deep_stringify_keys : default
|
|
@default.freeze
|
|
@default.dup
|
|
end
|
|
|
|
def default
|
|
cast(@default)
|
|
end
|
|
|
|
def value
|
|
unless (override = resolve_value_override).nil?
|
|
return cast(override)
|
|
end
|
|
|
|
cast(@value)
|
|
end
|
|
|
|
def description
|
|
if @description.is_a?(Symbol)
|
|
I18n.t(@description, default: nil)
|
|
else
|
|
@description
|
|
end
|
|
end
|
|
|
|
def serialized?
|
|
%i[array hash].include?(format)
|
|
end
|
|
|
|
def writable?
|
|
return false if value_override?
|
|
|
|
if writable.respond_to?(:call)
|
|
writable.call
|
|
else
|
|
!!writable
|
|
end
|
|
end
|
|
|
|
def persist_on_first_read?
|
|
persist_on_first_read
|
|
end
|
|
|
|
def unprefixed_env_var_name_allowed?
|
|
# Configuration values could be overridden with unprefixed env var
|
|
# names before being harmonized (PR#10296). Using unprefixed en var
|
|
# is deprecated and will be removed in 13.0.
|
|
# Configuration are recognized by not being writable.
|
|
!writable
|
|
end
|
|
|
|
def override_value(other_value)
|
|
self.value = coerce(other_value)
|
|
if valid_for?(value)
|
|
self.writable = false
|
|
else
|
|
raise ArgumentError, "Value for #{name} must be one of #{allowed.join(', ')} but is #{value}"
|
|
end
|
|
end
|
|
|
|
def valid_for?(value)
|
|
return true if allowed.nil?
|
|
|
|
# TODO: it would make sense to also check the type of the value (e.g. boolean).
|
|
# But as using e.g. 0 for a boolean is quite common, that would break.
|
|
if format == :array
|
|
(value - allowed).empty?
|
|
else
|
|
allowed.include?(value)
|
|
end
|
|
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] default The default value the setting has if not overridden.
|
|
# @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 [nil] description A human-readable description of this setting.
|
|
# @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.
|
|
# @param [nil] env_alias Alternative for the default env name to also look up. E.g. with the alias set to
|
|
# `OPENPROJECT_2FA` for a definition with the name `two_factor_authentication`, the value is fetched
|
|
# from the ENV OPENPROJECT_2FA as well.
|
|
# @param [TrueClass|FalseClass] disallow_override Disables the usual possibility of overriding the value
|
|
# from ENV or configuration file.
|
|
def add(name,
|
|
default:,
|
|
default_by_env: {},
|
|
format: nil,
|
|
description: nil,
|
|
writable: true,
|
|
allowed: nil,
|
|
env_alias: nil,
|
|
string_values: false,
|
|
persist_on_first_read: false,
|
|
disallow_override: false)
|
|
name = name.to_sym
|
|
return if exists?(name)
|
|
|
|
definition = new(name,
|
|
format:,
|
|
description:,
|
|
default:,
|
|
default_by_env:,
|
|
writable:,
|
|
allowed:,
|
|
env_alias:,
|
|
string_values:,
|
|
persist_on_first_read:)
|
|
override_value(definition) unless disallow_override
|
|
all[name] = definition
|
|
end
|
|
|
|
def add_all
|
|
Settings::Definition::DEFINITIONS.each do |setting_name, setting_options|
|
|
Settings::Definition.add(setting_name, **setting_options)
|
|
end
|
|
end
|
|
|
|
def [](name)
|
|
name = name.to_sym
|
|
if exists?(name)
|
|
all[name]
|
|
else
|
|
h = DEFINITIONS[name]
|
|
add(name, **h) if h.present?
|
|
end
|
|
end
|
|
|
|
def exists?(name)
|
|
all.key?(name.to_sym)
|
|
end
|
|
|
|
def all
|
|
@all ||= {}
|
|
end
|
|
|
|
# Registers a value override block for a setting. The block is called
|
|
# whenever the setting's value or writability is evaluated.
|
|
#
|
|
# If the block returns a non-nil value, that value is used as the setting's
|
|
# value and the setting becomes non-writable. If the block returns nil,
|
|
# no override is applied.
|
|
#
|
|
# To override a setting with nil, return a callable: +-> { nil }+
|
|
#
|
|
# @param name [Symbol] The setting name to override.
|
|
# @yield A block that returns the override value, or nil to skip.
|
|
#
|
|
# @example Force a setting to true when a condition is met
|
|
# Settings::Definition.add_value_override(:capture_external_links) do
|
|
# true if MyPlugin.active?
|
|
# end
|
|
def add_value_override(name, &block)
|
|
(value_overrides[name.to_sym] ||= []) << block
|
|
end
|
|
|
|
def value_overrides
|
|
@value_overrides ||= {}
|
|
end
|
|
|
|
def clear_value_overrides(name = nil)
|
|
if name
|
|
value_overrides.delete(name.to_sym)
|
|
else
|
|
@value_overrides = {}
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def file_config
|
|
@file_config ||= begin
|
|
filename = Rails.root.join("config/configuration.yml")
|
|
|
|
file_config = {}
|
|
|
|
if File.file?(filename)
|
|
file_config = load_yaml(ERB.new(File.read(filename)).result)
|
|
|
|
if file_config.is_a? Hash
|
|
file_config
|
|
else
|
|
warn "#{filename} is not a valid OpenProject configuration file, ignoring."
|
|
end
|
|
end
|
|
|
|
file_config
|
|
end
|
|
end
|
|
|
|
# Replace values for which an entry in the config file or as an environment variable exists.
|
|
def override_value(definition)
|
|
override_value_from_file(definition)
|
|
override_value_from_env(definition)
|
|
end
|
|
|
|
def override_value_from_file(definition)
|
|
envs = ["default", Rails.env]
|
|
envs.delete("default") if Rails.env.test? # The test setup should govern the configuration
|
|
envs.each do |env|
|
|
next unless (env_config = file_config[env])
|
|
next unless env_config.has_key?(definition.name)
|
|
|
|
definition.override_value(env_config[definition.name])
|
|
end
|
|
end
|
|
|
|
# Replace 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_value_from_env(definition)
|
|
override_config_values(definition)
|
|
merge_hash_config(definition) if definition.format == :hash
|
|
end
|
|
|
|
def override_config_values(definition)
|
|
find_env_var_override(definition) do |env_var_name, env_var_value|
|
|
value = extract_value_from_env(env_var_name, env_var_value)
|
|
definition.override_value(value)
|
|
end
|
|
end
|
|
|
|
def merge_hash_config(definition)
|
|
merged_hash = {}
|
|
each_env_var_hash_override(definition) do |env_var_name, env_var_value, env_var_hash_part|
|
|
value =
|
|
if definition.string_values
|
|
path_to_hash(*hash_path(env_var_hash_part), env_var_value)
|
|
else
|
|
extract_hash_from_env(env_var_name, env_var_value, env_var_hash_part)
|
|
end
|
|
|
|
merged_hash.deep_merge!(value)
|
|
end
|
|
return if merged_hash.empty?
|
|
|
|
definition.override_value(merged_hash)
|
|
end
|
|
|
|
def extract_hash_from_env(env_var_name, env_var_value, env_var_hash_part)
|
|
value = extract_value_from_env(env_var_name, env_var_value)
|
|
path_to_hash(*hash_path(env_var_hash_part), value)
|
|
end
|
|
|
|
# takes the hash part of an env variable and turn it into a path.
|
|
#
|
|
# e.g. hash_path('KEY_SUB__KEY_SUB__SUB__KEY') => ['key', 'sub_key', 'sub_sub_key']
|
|
def hash_path(env_var_hash_part)
|
|
env_var_hash_part
|
|
.scan(/(?:[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
|
|
|
|
def find_env_var_override(definition)
|
|
found_env_name = possible_env_names(definition).find { |name| ENV.key?(name) }
|
|
return unless found_env_name
|
|
|
|
if found_env_name == env_name_unprefixed(definition)
|
|
Rails.logger.warn(
|
|
"Using unprefixed environment variables is deprecated. " \
|
|
"Please use #{env_name(definition)} instead of #{env_name_unprefixed(definition)}"
|
|
)
|
|
end
|
|
yield found_env_name, ENV.fetch(found_env_name)
|
|
end
|
|
|
|
def each_env_var_hash_override(definition)
|
|
hash_override_matcher =
|
|
if definition.env_alias
|
|
/^(?:#{env_name(definition)}|#{env_name_nested(definition)}|#{env_name_alias(definition)})_(.+)/i
|
|
else
|
|
/^(?:#{env_name(definition)}|#{env_name_nested(definition)})_(.+)/i
|
|
end
|
|
ENV.each do |env_var_name, env_var_value|
|
|
env_var_name.match(hash_override_matcher) do |m|
|
|
yield env_var_name, env_var_value, m[1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def possible_env_names(definition)
|
|
[
|
|
env_name_nested(definition),
|
|
env_name(definition),
|
|
env_name_unprefixed(definition),
|
|
env_name_alias(definition)
|
|
].compact
|
|
end
|
|
|
|
def env_name_nested(definition)
|
|
"#{ENV_PREFIX}#{definition.name.upcase.gsub('_', '__')}"
|
|
end
|
|
|
|
def env_name(definition)
|
|
"#{ENV_PREFIX}#{definition.name.upcase}"
|
|
end
|
|
|
|
def env_name_unprefixed(definition)
|
|
definition.name.upcase if definition.unprefixed_env_var_name_allowed?
|
|
end
|
|
|
|
def env_name_alias(definition)
|
|
return unless definition.env_alias
|
|
|
|
definition.env_alias.upcase
|
|
end
|
|
|
|
public :possible_env_names, :env_name
|
|
|
|
##
|
|
# Extract the configuration value from the given environment variable
|
|
# using YAML.
|
|
#
|
|
# @param env_var_name [String] The environment variable name.
|
|
# @param env_var_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_from_env(env_var_name, env_var_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 env_var_value if env_var_value == ""
|
|
|
|
parsed = load_yaml(env_var_value)
|
|
|
|
if parsed.is_a?(String)
|
|
env_var_value
|
|
else
|
|
parsed
|
|
end
|
|
rescue StandardError => e
|
|
raise ArgumentError, "Configuration value for environment variable '#{env_var_name}' is invalid: #{e.message}"
|
|
end
|
|
|
|
def load_yaml(source)
|
|
YAML::safe_load(source, permitted_classes: [Symbol, Date])
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
attr_accessor :serialized,
|
|
:writable
|
|
|
|
def value_override?
|
|
!resolve_value_override.nil?
|
|
end
|
|
|
|
def resolve_value_override
|
|
self.class.value_overrides[name.to_sym]&.each do |block|
|
|
result = block.call
|
|
return result unless result.nil?
|
|
end
|
|
nil
|
|
end
|
|
|
|
def cast(value)
|
|
return nil if value.nil?
|
|
|
|
value = value.call if value.respond_to?(:call)
|
|
|
|
case format
|
|
when :integer
|
|
value.to_i
|
|
when :float
|
|
value.to_f
|
|
when :boolean
|
|
AR_BOOLEAN_TYPE.cast(value)
|
|
when :symbol
|
|
value.to_sym
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
def coerce(value)
|
|
case format
|
|
when :hash
|
|
(self.value || {}).deep_merge value.deep_stringify_keys
|
|
when :array
|
|
value.is_a?(String) ? value.split : Array(value)
|
|
when :datetime
|
|
value.is_a?(DateTime) ? value : DateTime.parse(value.to_s)
|
|
else
|
|
value
|
|
end
|
|
end
|
|
end
|
|
end
|
|
# rubocop:enable Metrics/CollectionLiteralLength
|