diff --git a/canaille/app/configuration.py b/canaille/app/configuration.py index 01d6bde1..9dd62d37 100644 --- a/canaille/app/configuration.py +++ b/canaille/app/configuration.py @@ -173,6 +173,7 @@ def setup_config(app, config=None, test_config=True, env_file=".env", env_prefix def validate(config, validate_remote=False): validate_keypair(config.get("CANAILLE_OIDC")) validate_theme(config["CANAILLE"]) + validate_admin_email(config["CANAILLE"]) if not validate_remote: return @@ -234,3 +235,10 @@ def validate_theme(config): os.path.join(ROOT, "themes", config["THEME"]) ): raise ConfigurationException(f'Cannot find theme \'{config["THEME"]}\'') + + +def validate_admin_email(config): + if config["ENABLE_PASSWORD_COMPROMISSION_CHECK"] and config["ADMIN_EMAIL"] is None: + raise ConfigurationException( + "You must set an administration email if you want to check if users' passwords are compromised." + ) diff --git a/canaille/config.sample.toml b/canaille/config.sample.toml index 3d3cb4bb..c3acc051 100644 --- a/canaille/config.sample.toml +++ b/canaille/config.sample.toml @@ -42,7 +42,7 @@ SECRET_KEY = "change me before you go in production" # By default, this is true if SMTP is configured, else this is false. # If explicitly set to true and SMTP is disabled, the email field # will be read-only. -# EMAIL_CONFIRMATION = +# EMAIL_CONFIRMATION = True # If ENABLE_REGISTRATION is true, then users can freely create an account # at this instance. If email verification is available, users must confirm @@ -74,6 +74,28 @@ SECRET_KEY = "change me before you go in production" # - if this is a string, it is passed to the python fileConfig method # https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig +# Minimum length for user password. +# It is possible not to set a minimum, by entering None or 0. +# MIN_PASSWORD_LENGTH = 8 + +# Maximum length for user password. +# There is a technical limit with passlib used by sql database of 4096 +# characters. If the value entered is 0 or None, or greater than 4096, +# then 4096 will be retained. +# MAX_PASSWORD_LENGTH = 1000 + +# Administration email contact. +# In certain special cases (example : questioning about password +# corruption), it is necessary to provide an administration contact +# email. +# ADMIN_EMAIL = "admin@mydomain.tld" + +# If :py:data:`True`, Canaille will check for password compromise on HIBP +# every time a new password is register. +# (https://haveibeenpwned.com/) +# ENABLE_PASSWORD_COMPROMISSION_CHECK = False + + # [CANAILLE_SQL] # The SQL database connection string # Details on https://docs.sqlalchemy.org/en/20/core/engines.html diff --git a/canaille/core/configuration.py b/canaille/core/configuration.py index 0384ce71..79557f0f 100644 --- a/canaille/core/configuration.py +++ b/canaille/core/configuration.py @@ -313,7 +313,7 @@ class CoreSettings(BaseModel): then 4096 will be retained. """ - ADMIN_EMAIL: str = None + ADMIN_EMAIL: str | None = None """Administration email contact. In certain special cases (example : questioning about password @@ -322,6 +322,8 @@ class CoreSettings(BaseModel): """ ENABLE_PASSWORD_COMPROMISSION_CHECK: bool = False - """Enable to check for password compromise on HIBP. - + """If :py:data:`True`, Canaille will check for password compromise on HIBP + every time a new password is register. + + (https://haveibeenpwned.com/) """ diff --git a/demo/conf-docker/canaille-ldap.toml b/demo/conf-docker/canaille-ldap.toml index 970522d8..56c7eb78 100644 --- a/demo/conf-docker/canaille-ldap.toml +++ b/demo/conf-docker/canaille-ldap.toml @@ -7,6 +7,7 @@ LOGO = "/static/img/canaille-head.webp" FAVICON = "/static/img/canaille-c.webp" EMAIL_CONFIRMATION = false ENABLE_REGISTRATION = true +ADMIN_EMAIL = "admin@mydomain.tld" [CANAILLE_LDAP] URI = "ldap://ldap:389" diff --git a/demo/conf-docker/canaille-memory.toml b/demo/conf-docker/canaille-memory.toml index 55242258..80a3d845 100644 --- a/demo/conf-docker/canaille-memory.toml +++ b/demo/conf-docker/canaille-memory.toml @@ -7,6 +7,7 @@ LOGO = "/static/img/canaille-head.webp" FAVICON = "/static/img/canaille-c.webp" EMAIL_CONFIRMATION = false ENABLE_REGISTRATION = true +ADMIN_EMAIL = "admin@mydomain.tld" [CANAILLE.ACL.DEFAULT] PERMISSIONS = ["edit_self", "use_oidc"] diff --git a/demo/conf-docker/canaille-sql.toml b/demo/conf-docker/canaille-sql.toml index 98d6392c..20375918 100644 --- a/demo/conf-docker/canaille-sql.toml +++ b/demo/conf-docker/canaille-sql.toml @@ -7,6 +7,7 @@ LOGO = "/static/img/canaille-head.webp" FAVICON = "/static/img/canaille-c.webp" EMAIL_CONFIRMATION = false ENABLE_REGISTRATION = true +ADMIN_EMAIL = "admin@mydomain.tld" [CANAILLE_SQL] DATABASE_URI = "sqlite:///demo.sqlite" diff --git a/demo/conf/canaille-ldap.toml b/demo/conf/canaille-ldap.toml index 93043d9b..d15d070c 100644 --- a/demo/conf/canaille-ldap.toml +++ b/demo/conf/canaille-ldap.toml @@ -6,6 +6,7 @@ LOGO = "/static/img/canaille-head.webp" FAVICON = "/static/img/canaille-c.webp" EMAIL_CONFIRMATION = false ENABLE_REGISTRATION = true +ADMIN_EMAIL = "admin@mydomain.tld" [CANAILLE.LOGGING] version = 1 diff --git a/demo/conf/canaille-memory.toml b/demo/conf/canaille-memory.toml index 301449ba..6abbc4c8 100644 --- a/demo/conf/canaille-memory.toml +++ b/demo/conf/canaille-memory.toml @@ -6,6 +6,7 @@ LOGO = "/static/img/canaille-head.webp" FAVICON = "/static/img/canaille-c.webp" EMAIL_CONFIRMATION = false ENABLE_REGISTRATION = true +ADMIN_EMAIL = "admin@mydomain.tld" [CANAILLE.LOGGING] version = 1 diff --git a/demo/conf/canaille-sql.toml b/demo/conf/canaille-sql.toml index 1560162c..9eb47510 100644 --- a/demo/conf/canaille-sql.toml +++ b/demo/conf/canaille-sql.toml @@ -6,6 +6,7 @@ LOGO = "/static/img/canaille-head.webp" FAVICON = "/static/img/canaille-c.webp" EMAIL_CONFIRMATION = false ENABLE_REGISTRATION = true +ADMIN_EMAIL = "admin@mydomain.tld" [CANAILLE.LOGGING] version = 1 diff --git a/tests/app/test_configuration.py b/tests/app/test_configuration.py index 36c43b9b..1d7516a1 100644 --- a/tests/app/test_configuration.py +++ b/tests/app/test_configuration.py @@ -203,3 +203,29 @@ def test_invalid_theme(configuration, backend): config_obj = settings_factory(configuration) config_dict = config_obj.model_dump() validate(config_dict, validate_remote=False) + + +def test_enable_password_compromission_check_with_and_without_admin_email( + configuration, backend +): + configuration["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = False + configuration["CANAILLE"]["ADMIN_EMAIL"] = None + config_obj = settings_factory(configuration) + config_dict = config_obj.model_dump() + validate(config_dict, validate_remote=False) + + configuration["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = True + configuration["CANAILLE"]["ADMIN_EMAIL"] = "admin_default_mail@mymail.com" + config_obj = settings_factory(configuration) + config_dict = config_obj.model_dump() + validate(config_dict, validate_remote=False) + + with pytest.raises( + ConfigurationException, + match=r"You must set an administration email if you want to check if users' passwords are compromised.", + ): + configuration["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = True + configuration["CANAILLE"]["ADMIN_EMAIL"] = None + config_obj = settings_factory(configuration) + config_dict = config_obj.model_dump() + validate(config_dict, validate_remote=False)