diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd987ed..dd3156c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## untagged +- Enable invite-only chatmail relays with invite tokens + that can override disabled account creation + ([#600](https://github.com/chatmail/relay/pull/600)) + - Expire push notification tokens after 90 days ([#583](https://github.com/chatmail/relay/pull/583)) diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py index 0cac3ea4..bab53bbd 100644 --- a/chatmaild/src/chatmaild/config.py +++ b/chatmaild/src/chatmaild/config.py @@ -31,6 +31,7 @@ def __init__(self, inipath, params): self.username_min_length = int(params["username_min_length"]) self.username_max_length = int(params["username_max_length"]) self.password_min_length = int(params["password_min_length"]) + self.invite_token = params.get("invite_token", "") self.passthrough_senders = params["passthrough_senders"].split() self.passthrough_recipients = params["passthrough_recipients"].split() self.filtermail_smtp_port = int(params["filtermail_smtp_port"]) diff --git a/chatmaild/src/chatmaild/doveauth.py b/chatmaild/src/chatmaild/doveauth.py index e6292a36..7127ef58 100644 --- a/chatmaild/src/chatmaild/doveauth.py +++ b/chatmaild/src/chatmaild/doveauth.py @@ -24,10 +24,16 @@ def encrypt_password(password: str): def is_allowed_to_create(config: Config, user, cleartext_password) -> bool: """Return True if user and password are admissable.""" if os.path.exists(NOCREATE_FILE): - logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") - return False + if not config.invite_token or config.invite_token not in cleartext_password: + logging.warning( + f"blocked account creation because {NOCREATE_FILE!r} exists." + ) + return False - if len(cleartext_password) < config.password_min_length: + if ( + len(cleartext_password.replace(config.invite_token, "")) + < config.password_min_length + ): logging.warning( "Password needs to be at least %s characters long", config.password_min_length, diff --git a/chatmaild/src/chatmaild/newemail.py b/chatmaild/src/chatmaild/newemail.py index fbf976af..6ca39cba 100644 --- a/chatmaild/src/chatmaild/newemail.py +++ b/chatmaild/src/chatmaild/newemail.py @@ -3,6 +3,7 @@ """CGI script for creating new accounts.""" import json +import os import random import secrets import string @@ -20,7 +21,11 @@ def create_newemail_dict(config: Config): secrets.choice(ALPHANUMERIC_PUNCT) for _ in range(config.password_min_length + 3) ) - return dict(email=f"{user}@{config.mail_domain}", password=f"{password}") + redirect_uri = os.getenv("REQUEST_URI", "/new") + invite_token = "" if redirect_uri == "/new" else redirect_uri[5:] + return dict( + email=f"{user}@{config.mail_domain}", password=f"{invite_token}{password}" + ) def print_new_account(): diff --git a/chatmaild/src/chatmaild/tests/test_doveauth.py b/chatmaild/src/chatmaild/tests/test_doveauth.py index 078bec42..4feb44cf 100644 --- a/chatmaild/src/chatmaild/tests/test_doveauth.py +++ b/chatmaild/src/chatmaild/tests/test_doveauth.py @@ -64,12 +64,34 @@ def test_dont_overwrite_password_on_wrong_login(dictproxy): assert res["password"] == res2["password"] -def test_nocreate_file(monkeypatch, tmpdir, dictproxy): - p = tmpdir.join("nocreate") - p.write("") - monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p)) - dictproxy.lookup_passdb("newuser12@chat.example.org", "zequ0Aimuchoodaechik") - assert not dictproxy.lookup_userdb("newuser12@chat.example.org") +@pytest.mark.parametrize( + ["nocreate_file", "account", "invite_token", "password"], + [ + (False, True, "asdf", "asdfasdmaimfelsgwerw"), + (False, True, "asdf", "z9873240187420913798"), + (False, True, "", "dsaiujfw9fjiwf9w"), + (True, True, "asdf", "asdfmosadkdkfwdofkw"), + (True, False, "asdf", "z9873240187420913798"), + (True, False, "", "dsaiujfw9fjiwf9w"), + ], +) +def test_nocreate_file( + monkeypatch, + tmpdir, + dictproxy, + example_config, + nocreate_file: bool, + account: bool, + invite_token: str, + password: str, +): + if nocreate_file: + p = tmpdir.join("nocreate") + p.write("") + monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p)) + example_config.invite_token = invite_token + dictproxy.lookup_passdb("newuser12@chat.example.org", password) + assert bool(dictproxy.lookup_userdb("newuser12@chat.example.org")) == account def test_handle_dovecot_request(dictproxy): diff --git a/cmdeploy/src/cmdeploy/__init__.py b/cmdeploy/src/cmdeploy/__init__.py index 20558d06..e35260ba 100644 --- a/cmdeploy/src/cmdeploy/__init__.py +++ b/cmdeploy/src/cmdeploy/__init__.py @@ -346,7 +346,7 @@ def _install_dovecot_package(package: str, arch: str): src=url, dest=deb_filename, sha256sum=sha256, - cache_time=9999999999999, # never redownload the package + cache_time=60 * 60 * 24 * 365 * 10, # never redownload the package ) apt.deb(name=f"Install dovecot-{package}", src=deb_filename) @@ -709,9 +709,10 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None: packages="postfix", ) - _install_dovecot_package("core", host.get_fact(facts.server.Arch)) - _install_dovecot_package("imapd", host.get_fact(facts.server.Arch)) - _install_dovecot_package("lmtpd", host.get_fact(facts.server.Arch)) + if not "dovecot.service" in host.get_fact(SystemdEnabled): + _install_dovecot_package("core", host.get_fact(facts.server.Arch)) + _install_dovecot_package("imapd", host.get_fact(facts.server.Arch)) + _install_dovecot_package("lmtpd", host.get_fact(facts.server.Arch)) apt.packages( name="Install nginx", diff --git a/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 b/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 index 8d27394c..644b35b8 100644 --- a/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 +++ b/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 @@ -84,12 +84,13 @@ http { if ($request_method = GET) { # Redirect to Delta Chat, # which will in turn do a POST request. - return 301 dcaccount:https://{{ config.domain_name }}/new; + return 301 dcaccount:https://{{ config.domain_name }}$request_uri; } fastcgi_pass unix:/run/fcgiwrap.socket; include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py; + fastcgi_param QUERY_STRING $query_string; } # Old URL for compatibility with e.g. printed QR codes. @@ -100,7 +101,7 @@ http { # Redirects are only for browsers. location /cgi-bin/newemail.py { if ($request_method = GET) { - return 301 dcaccount:https://{{ config.domain_name }}/new; + return 301 dcaccount:https://{{ config.domain_name }}$request_uri; } fastcgi_pass unix:/run/fcgiwrap.socket;