discourse/app/models/global_setting.rb
Sam Saffron 57a3d4e0d2
FEATURE: whitelist theme repo mode (experimental)
In some restricted setups all JS payloads need tight control.

This setting bans admins from making changes to JS on the site and
requires all themes be whitelisted to be used.

There are edge cases we still need to work through in this mode
hence this is still not supported in production and experimental.

Use an example like this to enable:

`DISCOURSE_WHITELISTED_THEME_REPOS="https://repo.com/repo.git,https://repo.com/repo2.git"`

By default this feature is not enabled and no changes are made.

One exception is that default theme id was missing a security check
this was added for correctness.
2020-06-03 13:19:57 +10:00

345 lines
8.6 KiB
Ruby

# frozen_string_literal: true
class GlobalSetting
def self.register(key, default)
define_singleton_method(key) do
provider.lookup(key, default)
end
end
VALID_SECRET_KEY ||= /^[0-9a-f]{128}$/
# this is named SECRET_TOKEN as opposed to SECRET_KEY_BASE
# for legacy reasons
REDIS_SECRET_KEY ||= 'SECRET_TOKEN'
REDIS_VALIDATE_SECONDS ||= 30
# In Rails secret_key_base is used to encrypt the cookie store
# the cookie store contains session data
# Discourse also uses this secret key to digest user auth tokens
# This method will
# - use existing token if already set in ENV or discourse.conf
# - generate a token on the fly if needed and cache in redis
# - enforce rules about token format falling back to redis if needed
def self.safe_secret_key_base
if @safe_secret_key_base && @token_in_redis && (@token_last_validated + REDIS_VALIDATE_SECONDS) < Time.now
@token_last_validated = Time.now
token = Discourse.redis.without_namespace.get(REDIS_SECRET_KEY)
if token.nil?
Discourse.redis.without_namespace.set(REDIS_SECRET_KEY, @safe_secret_key_base)
end
end
@safe_secret_key_base ||= begin
token = secret_key_base
if token.blank? || token !~ VALID_SECRET_KEY
@token_in_redis = true
@token_last_validated = Time.now
token = Discourse.redis.without_namespace.get(REDIS_SECRET_KEY)
unless token && token =~ VALID_SECRET_KEY
token = SecureRandom.hex(64)
Discourse.redis.without_namespace.set(REDIS_SECRET_KEY, token)
end
end
if !secret_key_base.blank? && token != secret_key_base
STDERR.puts "WARNING: DISCOURSE_SECRET_KEY_BASE is invalid, it was re-generated"
end
token
end
rescue Redis::CommandError => e
@safe_secret_key_base = SecureRandom.hex(64) if e.message =~ /READONLY/
end
def self.load_defaults
default_provider = FileProvider.from(File.expand_path('../../../config/discourse_defaults.conf', __FILE__))
default_provider.keys.concat(@provider.keys).uniq.each do |key|
default = default_provider.lookup(key, nil)
instance_variable_set("@#{key}_cache", nil)
define_singleton_method(key) do
val = instance_variable_get("@#{key}_cache")
unless val.nil?
val == :missing ? nil : val
else
val = provider.lookup(key, default)
if val.nil?
val = :missing
end
instance_variable_set("@#{key}_cache", val)
val == :missing ? nil : val
end
end
end
end
def self.skip_db=(v)
@skip_db = v
end
def self.skip_db?
@skip_db
end
def self.skip_redis=(v)
@skip_redis = v
end
def self.skip_redis?
@skip_redis
end
def self.use_s3?
(@use_s3 ||=
begin
s3_bucket &&
s3_region && (
s3_use_iam_profile || (s3_access_key_id && s3_secret_access_key)
) ? :true : :false
end) == :true
end
def self.s3_bucket_name
@s3_bucket_name ||= s3_bucket.downcase.split("/")[0]
end
# for testing
def self.reset_s3_cache!
@use_s3 = nil
end
def self.database_config
hash = { "adapter" => "postgresql" }
%w{
pool
connect_timeout
timeout
socket
host
backup_host
port
backup_port
username
password
replica_host
replica_port
}.each do |s|
if val = self.public_send("db_#{s}")
hash[s] = val
end
end
if hash["replica_host"]
if !ENV["ACTIVE_RECORD_RAILS_FAILOVER"]
hash["adapter"] = "postgresql_fallback"
end
end
hostnames = [ hostname ]
hostnames << backup_hostname if backup_hostname.present?
hostnames << URI.parse(cdn_url).host if cdn_url.present?
hostnames << cdn_origin_hostname if cdn_origin_hostname.present?
hash["host_names"] = hostnames
hash["database"] = db_name
hash["prepared_statements"] = !!self.db_prepared_statements
hash["idle_timeout"] = connection_reaper_age if connection_reaper_age.present?
hash["reaping_frequency"] = connection_reaper_interval if connection_reaper_interval.present?
{ "production" => hash }
end
# For testing purposes
def self.reset_redis_config!
@config = nil
@message_bus_config = nil
end
def self.redis_config
@config ||=
begin
c = {}
c[:host] = redis_host if redis_host
c[:port] = redis_port if redis_port
if redis_slave_host && redis_slave_port
if ENV["REDIS_RAILS_FAILOVER"]
c[:replica_host] = redis_slave_host
c[:replica_port] = redis_slave_port
c[:connector] = RailsFailover::Redis::Connector
else
c[:slave_host] = redis_slave_host
c[:slave_port] = redis_slave_port
c[:connector] = DiscourseRedis::Connector
end
end
c[:password] = redis_password if redis_password.present?
c[:db] = redis_db if redis_db != 0
c[:db] = 1 if Rails.env == "test"
c[:id] = nil if redis_skip_client_commands
c[:ssl] = true if redis_use_ssl
c.freeze
end
end
def self.message_bus_redis_config
return redis_config unless message_bus_redis_enabled
@message_bus_config ||=
begin
c = {}
c[:host] = message_bus_redis_host if message_bus_redis_host
c[:port] = message_bus_redis_port if message_bus_redis_port
if message_bus_redis_slave_host && message_bus_redis_slave_port
if ENV["REDIS_RAILS_FAILOVER"]
c[:replica_host] = redis_slave_host
c[:replica_port] = redis_slave_port
c[:connector] = RailsFailover::Redis::Connector
else
c[:slave_host] = message_bus_redis_slave_host
c[:slave_port] = message_bus_redis_slave_port
c[:connector] = DiscourseRedis::Connector
end
end
c[:password] = message_bus_redis_password if message_bus_redis_password.present?
c[:db] = message_bus_redis_db if message_bus_redis_db != 0
c[:db] = 1 if Rails.env == "test"
c[:id] = nil if message_bus_redis_skip_client_commands
c[:ssl] = true if redis_use_ssl
c.freeze
end
end
# test only
def self.reset_whitelisted_theme_ids!
@whitelisted_theme_ids = nil
end
def self.whitelisted_theme_ids
return nil if whitelisted_theme_repos.blank?
@whitelisted_theme_ids ||= begin
urls = whitelisted_theme_repos.split(",").map(&:strip)
Theme
.joins(:remote_theme)
.where('remote_themes.remote_url in (?)', urls)
.pluck(:id)
end
end
def self.add_default(name, default)
unless self.respond_to? name
define_singleton_method(name) do
default
end
end
end
class BaseProvider
def self.coerce(setting)
return setting == "true" if setting == "true" || setting == "false"
return $1.to_i if setting.to_s.strip =~ /^([0-9]+)$/
setting
end
def resolve(current, default)
BaseProvider.coerce(
if current.present?
current
else
default.present? ? default : nil
end
)
end
end
class FileProvider < BaseProvider
attr_reader :data
def self.from(file)
if File.exists?(file)
parse(file)
end
end
def initialize(file)
@file = file
@data = {}
end
def read
ERB.new(File.read(@file)).result().split("\n").each do |line|
if line =~ /^\s*([a-z_]+[a-z0-9_]*)\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|[^#]*)/
@data[$1.strip.to_sym] = ($4 || $3 || $2).strip
end
end
end
def lookup(key, default)
var = @data[key]
resolve(var, var.nil? ? default : "")
end
def keys
@data.keys
end
def self.parse(file)
provider = self.new(file)
provider.read
provider
end
private_class_method :parse
end
class EnvProvider < BaseProvider
def lookup(key, default)
var = ENV["DISCOURSE_" + key.to_s.upcase]
resolve(var , var.nil? ? default : nil)
end
def keys
ENV.keys.select { |k| k =~ /^DISCOURSE_/ }.map { |k| k[10..-1].downcase.to_sym }
end
end
class BlankProvider < BaseProvider
def lookup(key, default)
if key == :redis_port
return ENV["DISCOURSE_REDIS_PORT"] if ENV["DISCOURSE_REDIS_PORT"]
end
default
end
def keys
[]
end
end
class << self
attr_accessor :provider
end
def self.configure!
if Rails.env == "test"
@provider = BlankProvider.new
else
@provider =
FileProvider.from(File.expand_path('../../../config/discourse.conf', __FILE__)) ||
EnvProvider.new
end
end
end