mirror of
https://github.com/aljazceru/CTFd.git
synced 2026-01-31 20:04:28 +01:00
Add a better fallback for environment variables through config interpolation (#1574)
* Make config.ini properly supercede environment variables * Add ability for all config.ini values to fallback to environment variables. * Closes #1573 Config Behavior should be as follows: 1. Key exists in `config.ini` and is set to a value. That value becomes the app config. 2. Key exists in `config.ini` but is set to empty string. An envvar the same name is looked up. The envvar's value is used as the app config. 3. If the envvar is not found, fall back to the default specified value in `config.py` 4. If there is no default, use None or an empty string
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
# CTFd Configuration File
|
||||
#
|
||||
# Use this file to configure aspects of how CTFd behaves. Additional attributes can be specified for
|
||||
# plugins and other additional behavior.
|
||||
#
|
||||
# If a configuration item is specified but left empty, CTFd will do the following:
|
||||
#
|
||||
# 1. Look for an environment variable under the same name and use that value if found
|
||||
# 2. Use a default value specified in it's own internal configuration
|
||||
# 3. Use a null value (i.e. None) or empty string for the configuration value
|
||||
|
||||
|
||||
[server]
|
||||
# SECRET_KEY:
|
||||
# The secret value used to creation sessions and sign strings. This should be set to a random string. In the
|
||||
|
||||
142
CTFd/config.py
142
CTFd/config.py
@@ -3,6 +3,33 @@ import os
|
||||
from distutils.util import strtobool
|
||||
|
||||
|
||||
class EnvInterpolation(configparser.BasicInterpolation):
|
||||
"""Interpolation which expands environment variables in values."""
|
||||
|
||||
def before_get(self, parser, section, option, value, defaults):
|
||||
value = super().before_get(parser, section, option, value, defaults)
|
||||
envvar = os.getenv(option)
|
||||
if value == "" and envvar:
|
||||
return process_string_var(envvar)
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def process_string_var(value):
|
||||
if value == "":
|
||||
return None
|
||||
|
||||
if value.isdigit():
|
||||
return int(value)
|
||||
elif value.replace(".", "", 1).isdigit():
|
||||
return float(value)
|
||||
|
||||
try:
|
||||
return bool(strtobool(value))
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
def process_boolean_str(value):
|
||||
if type(value) is bool:
|
||||
return value
|
||||
@@ -44,7 +71,7 @@ def gen_secret_key():
|
||||
return key
|
||||
|
||||
|
||||
config_ini = configparser.ConfigParser()
|
||||
config_ini = configparser.ConfigParser(interpolation=EnvInterpolation())
|
||||
config_ini.optionxform = str # Makes the key value case-insensitive
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.ini")
|
||||
config_ini.read(path)
|
||||
@@ -52,16 +79,13 @@ config_ini.read(path)
|
||||
|
||||
# fmt: off
|
||||
class ServerConfig(object):
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY") \
|
||||
or empty_str_cast(config_ini["server"]["SECRET_KEY"]) \
|
||||
SECRET_KEY: str = empty_str_cast(config_ini["server"]["SECRET_KEY"]) \
|
||||
or gen_secret_key()
|
||||
|
||||
DATABASE_URL: str = os.getenv("DATABASE_URL") \
|
||||
or empty_str_cast(config_ini["server"]["DATABASE_URL"]) \
|
||||
DATABASE_URL: str = empty_str_cast(config_ini["server"]["DATABASE_URL"]) \
|
||||
or f"sqlite:///{os.path.dirname(os.path.abspath(__file__))}/ctfd.db"
|
||||
|
||||
REDIS_URL: str = os.getenv("REDIS_URL") \
|
||||
or empty_str_cast(config_ini["server"]["REDIS_URL"])
|
||||
REDIS_URL: str = empty_str_cast(config_ini["server"]["REDIS_URL"])
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = DATABASE_URL
|
||||
CACHE_REDIS_URL = REDIS_URL
|
||||
@@ -76,16 +100,13 @@ class ServerConfig(object):
|
||||
CACHE_THRESHOLD: int = 0
|
||||
|
||||
# === SECURITY ===
|
||||
SESSION_COOKIE_HTTPONLY: bool = process_boolean_str(os.getenv("SESSION_COOKIE_HTTPONLY")) \
|
||||
or config_ini["security"].getboolean("SESSION_COOKIE_HTTPONLY") \
|
||||
SESSION_COOKIE_HTTPONLY: bool = config_ini["security"].getboolean("SESSION_COOKIE_HTTPONLY") \
|
||||
or True
|
||||
|
||||
SESSION_COOKIE_SAMESITE: str = os.getenv("SESSION_COOKIE_SAMESITE") \
|
||||
or empty_str_cast(config_ini["security"]["SESSION_COOKIE_SAMESITE"]) \
|
||||
SESSION_COOKIE_SAMESITE: str = empty_str_cast(config_ini["security"]["SESSION_COOKIE_SAMESITE"]) \
|
||||
or "Lax"
|
||||
|
||||
PERMANENT_SESSION_LIFETIME: int = int(os.getenv("PERMANENT_SESSION_LIFETIME", 0)) \
|
||||
or config_ini["security"].getint("PERMANENT_SESSION_LIFETIME") \
|
||||
PERMANENT_SESSION_LIFETIME: int = config_ini["security"].getint("PERMANENT_SESSION_LIFETIME") \
|
||||
or 604800
|
||||
|
||||
"""
|
||||
@@ -110,114 +131,85 @@ class ServerConfig(object):
|
||||
]
|
||||
|
||||
# === EMAIL ===
|
||||
MAILFROM_ADDR: str = os.getenv("MAILFROM_ADDR") \
|
||||
or config_ini["email"]["MAILFROM_ADDR"] \
|
||||
MAILFROM_ADDR: str = config_ini["email"]["MAILFROM_ADDR"] \
|
||||
or "noreply@ctfd.io"
|
||||
|
||||
MAIL_SERVER: str = os.getenv("MAIL_SERVER") \
|
||||
or empty_str_cast(config_ini["email"]["MAIL_SERVER"])
|
||||
MAIL_SERVER: str = empty_str_cast(config_ini["email"]["MAIL_SERVER"])
|
||||
|
||||
MAIL_PORT: str = os.getenv("MAIL_PORT") \
|
||||
or empty_str_cast(config_ini["email"]["MAIL_PORT"])
|
||||
MAIL_PORT: int = empty_str_cast(config_ini["email"]["MAIL_PORT"])
|
||||
|
||||
MAIL_USEAUTH: bool = process_boolean_str(os.getenv("MAIL_USEAUTH")) \
|
||||
or process_boolean_str(config_ini["email"]["MAIL_USEAUTH"])
|
||||
MAIL_USEAUTH: bool = process_boolean_str(config_ini["email"]["MAIL_USEAUTH"])
|
||||
|
||||
MAIL_USERNAME: str = os.getenv("MAIL_USERNAME") \
|
||||
or empty_str_cast(config_ini["email"]["MAIL_USERNAME"])
|
||||
MAIL_USERNAME: str = empty_str_cast(config_ini["email"]["MAIL_USERNAME"])
|
||||
|
||||
MAIL_PASSWORD: str = os.getenv("MAIL_PASSWORD") \
|
||||
or empty_str_cast(config_ini["email"]["MAIL_PASSWORD"])
|
||||
MAIL_PASSWORD: str = empty_str_cast(config_ini["email"]["MAIL_PASSWORD"])
|
||||
|
||||
MAIL_TLS: bool = process_boolean_str(os.getenv("MAIL_TLS")) \
|
||||
or process_boolean_str(config_ini["email"]["MAIL_TLS"])
|
||||
MAIL_TLS: bool = process_boolean_str(config_ini["email"]["MAIL_TLS"])
|
||||
|
||||
MAIL_SSL: bool = process_boolean_str(os.getenv("MAIL_SSL")) \
|
||||
or process_boolean_str(config_ini["email"]["MAIL_SSL"])
|
||||
MAIL_SSL: bool = process_boolean_str(config_ini["email"]["MAIL_SSL"])
|
||||
|
||||
MAILGUN_API_KEY: str = os.getenv("MAILGUN_API_KEY") \
|
||||
or empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
|
||||
MAILGUN_API_KEY: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
|
||||
|
||||
MAILGUN_BASE_URL: str = os.getenv("MAILGUN_BASE_URL") \
|
||||
or empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
|
||||
MAILGUN_BASE_URL: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
|
||||
|
||||
# === LOGS ===
|
||||
LOG_FOLDER: str = os.getenv("LOG_FOLDER") \
|
||||
or empty_str_cast(config_ini["logs"]["LOG_FOLDER"]) \
|
||||
LOG_FOLDER: str = empty_str_cast(config_ini["logs"]["LOG_FOLDER"]) \
|
||||
or os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
|
||||
|
||||
# === UPLOADS ===
|
||||
UPLOAD_PROVIDER: str = os.getenv("UPLOAD_PROVIDER") \
|
||||
or empty_str_cast(config_ini["uploads"]["UPLOAD_PROVIDER"]) \
|
||||
UPLOAD_PROVIDER: str = empty_str_cast(config_ini["uploads"]["UPLOAD_PROVIDER"]) \
|
||||
or "filesystem"
|
||||
|
||||
UPLOAD_FOLDER: str = os.getenv("UPLOAD_FOLDER") \
|
||||
or empty_str_cast(config_ini["uploads"]["UPLOAD_FOLDER"]) \
|
||||
UPLOAD_FOLDER: str = empty_str_cast(config_ini["uploads"]["UPLOAD_FOLDER"]) \
|
||||
or os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads")
|
||||
|
||||
if UPLOAD_PROVIDER == "s3":
|
||||
AWS_ACCESS_KEY_ID: str = os.getenv("AWS_ACCESS_KEY_ID") \
|
||||
or empty_str_cast(config_ini["uploads"]["AWS_ACCESS_KEY_ID"])
|
||||
AWS_ACCESS_KEY_ID: str = empty_str_cast(config_ini["uploads"]["AWS_ACCESS_KEY_ID"])
|
||||
|
||||
AWS_SECRET_ACCESS_KEY: str = os.getenv("AWS_SECRET_ACCESS_KEY") \
|
||||
or empty_str_cast(config_ini["uploads"]["AWS_SECRET_ACCESS_KEY"])
|
||||
AWS_SECRET_ACCESS_KEY: str = empty_str_cast(config_ini["uploads"]["AWS_SECRET_ACCESS_KEY"])
|
||||
|
||||
AWS_S3_BUCKET: str = os.getenv("AWS_S3_BUCKET") \
|
||||
or empty_str_cast(config_ini["uploads"]["AWS_S3_BUCKET"])
|
||||
AWS_S3_BUCKET: str = empty_str_cast(config_ini["uploads"]["AWS_S3_BUCKET"])
|
||||
|
||||
AWS_S3_ENDPOINT_URL: str = os.getenv("AWS_S3_ENDPOINT_URL") \
|
||||
or empty_str_cast(config_ini["uploads"]["AWS_S3_ENDPOINT_URL"])
|
||||
AWS_S3_ENDPOINT_URL: str = empty_str_cast(config_ini["uploads"]["AWS_S3_ENDPOINT_URL"])
|
||||
|
||||
# === OPTIONAL ===
|
||||
REVERSE_PROXY: bool = process_boolean_str(os.getenv("REVERSE_PROXY")) \
|
||||
or empty_str_cast(config_ini["optional"]["REVERSE_PROXY"]) \
|
||||
REVERSE_PROXY: bool = empty_str_cast(config_ini["optional"]["REVERSE_PROXY"]) \
|
||||
or False
|
||||
|
||||
TEMPLATES_AUTO_RELOAD: bool = process_boolean_str(os.getenv("TEMPLATES_AUTO_RELOAD")) \
|
||||
or empty_str_cast(config_ini["optional"]["TEMPLATES_AUTO_RELOAD"]) \
|
||||
TEMPLATES_AUTO_RELOAD: bool = empty_str_cast(config_ini["optional"]["TEMPLATES_AUTO_RELOAD"]) \
|
||||
or True
|
||||
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS: bool = process_boolean_str(os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS")) \
|
||||
or empty_str_cast(config_ini["optional"]["SQLALCHEMY_TRACK_MODIFICATIONS"]) \
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS: bool = empty_str_cast(config_ini["optional"]["SQLALCHEMY_TRACK_MODIFICATIONS"]) \
|
||||
or False
|
||||
|
||||
SWAGGER_UI: bool = os.getenv("SWAGGER_UI") \
|
||||
or empty_str_cast(config_ini["optional"]["SWAGGER_UI"]) \
|
||||
SWAGGER_UI: bool = empty_str_cast(config_ini["optional"]["SWAGGER_UI"]) \
|
||||
or False
|
||||
|
||||
SWAGGER_UI_ENDPOINT: str = "/" if SWAGGER_UI else None
|
||||
|
||||
UPDATE_CHECK: bool = process_boolean_str(os.getenv("UPDATE_CHECK")) \
|
||||
or empty_str_cast(config_ini["optional"]["UPDATE_CHECK"]) \
|
||||
UPDATE_CHECK: bool = empty_str_cast(config_ini["optional"]["UPDATE_CHECK"]) \
|
||||
or True
|
||||
|
||||
APPLICATION_ROOT: str = os.getenv("APPLICATION_ROOT") \
|
||||
or empty_str_cast(config_ini["optional"]["APPLICATION_ROOT"]) \
|
||||
APPLICATION_ROOT: str = empty_str_cast(config_ini["optional"]["APPLICATION_ROOT"]) \
|
||||
or "/"
|
||||
|
||||
SERVER_SENT_EVENTS: bool = process_boolean_str(os.getenv("SERVER_SENT_EVENTS")) \
|
||||
or empty_str_cast(config_ini["optional"]["SERVER_SENT_EVENTS"]) \
|
||||
SERVER_SENT_EVENTS: bool = empty_str_cast(config_ini["optional"]["SERVER_SENT_EVENTS"]) \
|
||||
or True
|
||||
|
||||
HTML_SANITIZATION: bool = process_boolean_str(os.getenv("HTML_SANITIZATION")) \
|
||||
or empty_str_cast(config_ini["optional"]["HTML_SANITIZATION"]) \
|
||||
HTML_SANITIZATION: bool = empty_str_cast(config_ini["optional"]["HTML_SANITIZATION"]) \
|
||||
or False
|
||||
|
||||
if DATABASE_URL.startswith("sqlite") is False:
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
"max_overflow": int(os.getenv("SQLALCHEMY_MAX_OVERFLOW", 0))
|
||||
or int(empty_str_cast(config_ini["optional"]["SQLALCHEMY_MAX_OVERFLOW"], default=0)) # noqa: E131
|
||||
"max_overflow": int(empty_str_cast(config_ini["optional"]["SQLALCHEMY_MAX_OVERFLOW"], default=0)) # noqa: E131
|
||||
or 20, # noqa: E131
|
||||
"pool_pre_ping": process_boolean_str(os.getenv("SQLALCHEMY_POOL_PRE_PING"))
|
||||
or empty_str_cast(config_ini["optional"]["SQLALCHEMY_POOL_PRE_PING"]) # noqa: E131
|
||||
"pool_pre_ping": empty_str_cast(config_ini["optional"]["SQLALCHEMY_POOL_PRE_PING"]) # noqa: E131
|
||||
or True, # noqa: E131
|
||||
}
|
||||
|
||||
# === OAUTH ===
|
||||
OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID") \
|
||||
or empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_ID"])
|
||||
OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET") \
|
||||
or empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_SECRET"])
|
||||
OAUTH_CLIENT_ID: str = empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_ID"])
|
||||
OAUTH_CLIENT_SECRET: str = empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_SECRET"])
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -238,10 +230,4 @@ class TestingConfig(ServerConfig):
|
||||
# Actually initialize ServerConfig to allow us to add more attributes on
|
||||
Config = ServerConfig()
|
||||
for k, v in config_ini.items("extra"):
|
||||
# Cast numeric values to their appropriate type
|
||||
if v.isdigit():
|
||||
setattr(Config, k, int(v))
|
||||
elif v.replace(".", "", 1).isdigit():
|
||||
setattr(Config, k, float(v))
|
||||
else:
|
||||
setattr(Config, k, v)
|
||||
setattr(Config, k, process_string_var(v))
|
||||
|
||||
Reference in New Issue
Block a user