Configuration entries can be loaded from files.

Co-authored-by: Sofi <sofi+git@mailbox.org>
This commit is contained in:
Éloi Rivard 2023-06-15 18:29:12 +02:00
parent f254cd94f1
commit a9d9d43152
5 changed files with 85 additions and 35 deletions

View file

@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_, The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_,
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_. and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_.
Added
*****
- Configuration entries can be loaded from files if the entry key has a *_FILE* suffix
and the entry value is the path to the file. :issue:`134` :pr:`134`
Removed Removed
******* *******

View file

@ -2,7 +2,6 @@ import datetime
import os import os
from logging.config import dictConfig from logging.config import dictConfig
import toml
from flask import Flask from flask import Flask
from flask import g from flask import g
from flask import session from flask import session
@ -15,34 +14,6 @@ from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect() csrf = CSRFProtect()
def setup_config(app, config=None, validate=True):
import canaille.app.configuration
from canaille.oidc.installation import install
app.config.from_mapping(
{
"SESSION_COOKIE_NAME": "canaille",
"OAUTH2_REFRESH_TOKEN_GENERATOR": True,
"OAUTH2_ACCESS_TOKEN_GENERATOR": "canaille.oidc.oauth.generate_access_token",
}
)
if config:
app.config.from_mapping(config)
elif "CONFIG" in os.environ:
app.config.from_mapping(toml.load(os.environ.get("CONFIG")))
else:
raise Exception(
"No configuration file found. "
"Either create conf/config.toml or set the 'CONFIG' variable environment."
)
if app.debug: # pragma: no cover
install(app.config)
if validate:
canaille.app.configuration.validate(app.config)
def setup_backend(app, backend): def setup_backend(app, backend):
from .backends.ldap.backend import Backend from .backends.ldap.backend import Backend
@ -182,14 +153,15 @@ def setup_flask(app):
def create_app(config=None, validate=True, backend=None): def create_app(config=None, validate=True, backend=None):
from .oidc.oauth import setup_oauth
from .app.i18n import setup_i18n
from .app.configuration import setup_config
app = Flask(__name__) app = Flask(__name__)
setup_config(app, config, validate) setup_config(app, config, validate)
sentry_sdk = setup_sentry(app) sentry_sdk = setup_sentry(app)
try: try:
from .oidc.oauth import setup_oauth
from .app.i18n import setup_i18n
setup_logging(app) setup_logging(app)
setup_backend(app, backend) setup_backend(app, backend)
setup_oauth(app) setup_oauth(app)

View file

@ -1,6 +1,9 @@
import os import os
import smtplib import smtplib
import socket import socket
from collections.abc import Mapping
import toml
ROOT = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(os.path.abspath(__file__))
@ -9,6 +12,58 @@ class ConfigurationException(Exception):
pass pass
def parse_file_keys(config):
"""
Replaces configuration entries with the '_FILE' suffix with
the matching file content.
"""
SUFFIX = "_FILE"
new_config = {}
for key, value in config.items():
if isinstance(value, Mapping):
new_config[key] = parse_file_keys(value)
elif isinstance(key, str) and key.endswith(SUFFIX) and isinstance(value, str):
with open(value) as f:
value = f.read().rstrip("\n")
root_key = key[: -len(SUFFIX)]
new_config[root_key] = value
else:
new_config[key] = value
return new_config
def setup_config(app, config=None, validate_config=True):
from canaille.oidc.installation import install
app.config.from_mapping(
{
"SESSION_COOKIE_NAME": "canaille",
"OAUTH2_REFRESH_TOKEN_GENERATOR": True,
"OAUTH2_ACCESS_TOKEN_GENERATOR": "canaille.oidc.oauth.generate_access_token",
}
)
if config:
app.config.from_mapping(parse_file_keys(config))
elif "CONFIG" in os.environ:
app.config.from_mapping(parse_file_keys(toml.load(os.environ.get("CONFIG"))))
else:
raise Exception(
"No configuration file found. "
"Either create conf/config.toml or set the 'CONFIG' variable environment."
)
if app.debug: # pragma: no cover
install(app.config)
if validate_config:
validate(app.config)
def validate(config, validate_remote=False): def validate(config, validate_remote=False):
validate_keypair(config) validate_keypair(config)
validate_theme(config) validate_theme(config)

View file

@ -6,14 +6,18 @@ Here are the different options you can have in your configuration file.
.. contents:: .. contents::
:local: :local:
.. note ::
Any configuration entry can be suffixed by *_FILE* and point to the path of
a file that contains the actual value. For instance you could have
``SECRET_KEY_FILE = "/path/to/secret.txt"`` instead of ``SECRET_KEY = "very-secret"``
Sections Sections
======== ========
Miscellaneous Miscellaneous
------------- -------------
Canaille is based on Flask, so any `flask configuration <https://flask.palletsprojects.com/en/1.1.x/config/#builtin-configuration-values>`_ option will be usable with canaille: Canaille is based on Flask, so any `flask configuration <https://flask.palletsprojects.com/en/2.3.x/config/#builtin-configuration-values>`_ option will be usable with canaille:
:SECRET_KEY: :SECRET_KEY:
**Required.** The Flask secret key. You should set a random string here. **Required.** The Flask secret key. You should set a random string here.

View file

@ -7,6 +7,19 @@ from canaille.app.configuration import validate
from flask_webtest import TestApp from flask_webtest import TestApp
def test_configuration_file_suffix(tmp_path, backend, configuration):
file_path = os.path.join(tmp_path, "secret.txt")
with open(file_path, "w") as fd:
fd.write("very-secret")
del configuration["SECRET_KEY"]
configuration["SECRET_KEY_FILE"] = file_path
app = create_app(configuration)
assert "SECRET_KEY_FILE" not in app.config
assert app.config["SECRET_KEY"] == "very-secret"
def test_smtp_connection_remote_smtp_unreachable(testclient, backend, configuration): def test_smtp_connection_remote_smtp_unreachable(testclient, backend, configuration):
configuration["SMTP"]["HOST"] = "smtp://invalid-smtp.com" configuration["SMTP"]["HOST"] = "smtp://invalid-smtp.com"
with pytest.raises( with pytest.raises(