diff --git a/CHANGELOG.md b/CHANGELOG.md index 13dc2a49..8650e6f4 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)) + - dovecot: keep mailbox index only in memory to avoid unnecessary disc usage ([#632](https://github.com/chatmail/relay/pull/632)) diff --git a/README.md b/README.md index 9f60d113..263e5ce7 100644 --- a/README.md +++ b/README.md @@ -284,8 +284,23 @@ Fresh chatmail addresses have a mailbox directory that contains: will typically be empty unless the user of that address hasn't been online for a while. +## Restrict address creation -## Emergency Commands to disable automatic address creation +### Only allow new addresses with an invite token + +To restrict address creation for anyone who doesn't have the invite link/QR code: + +1. Use the `invite_token` option to add + one or more tokens of your choice to `chatmail.ini`: + `invite_token = s3cr3t privil3g3` + - (recommendation: choose 9 or more letters, or it will be easily bruteforced) +2. Run `scripts/cmdeploy run` +3. Distribute a `dcaccount` invite link/QR code + (like the one on your web page) + with one of your invite tokens added at the end, + for example: `dcaccount:https://example.org/new?s3cr3t` + +### Emergency Command to disable automatic address creation If you need to stop address creation, e.g. because some script is wildly creating addresses, diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py index ae5f4423..68d11f75 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.www_folder = params.get("www_folder", "") diff --git a/chatmaild/src/chatmaild/doveauth.py b/chatmaild/src/chatmaild/doveauth.py index e6292a36..e2543b7d 100644 --- a/chatmaild/src/chatmaild/doveauth.py +++ b/chatmaild/src/chatmaild/doveauth.py @@ -26,8 +26,19 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool: if os.path.exists(NOCREATE_FILE): logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") return False + password_length = len(cleartext_password) + if config.invite_token: + for inv_token in config.invite_token.split(): + if cleartext_password.startswith(inv_token): + password_length = len(cleartext_password) - len(inv_token) + break + else: + logging.warning( + "blocked account creation because password didn't contain invite token(s)." + ) + return False - if len(cleartext_password) < config.password_min_length: + if password_length < 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..3f689e5d 100644 --- a/chatmaild/src/chatmaild/tests/test_doveauth.py +++ b/chatmaild/src/chatmaild/tests/test_doveauth.py @@ -64,12 +64,38 @@ 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, False, "asdf", "z9873240187420913798"), + (False, True, "", "dsaiujfw9fjiwf9w"), + (False, False, "asdf", "z987324018742asdf0913798"), + (False, True, "as df", "asj0wiefkj0ofkeefok"), + (False, True, "as df", "dfj0wiefkj0ofkeefok"), + (False, False, "as df", "j0wiefkj0ofas dfkeefok"), + (True, False, "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/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; diff --git a/www/src/index.md b/www/src/index.md index e167c740..0031d28e 100644 --- a/www/src/index.md +++ b/www/src/index.md @@ -11,6 +11,7 @@ for Delta Chat users. For details how it avoids storing personal information please see our [privacy policy](privacy.html). {% endif %} +{% if not config.invite_token %} Get a {{config.mail_domain}} chat profile If you are viewing this page on a different device @@ -23,6 +24,10 @@ you can also **scan this QR code** with Delta Chat: 🐣 **Choose** your Avatar and Name 💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee) +{% else %} +**To join this instance, you need an invite link or QR code - +ask the admin for an invite.** +{% endif %} {% if config.mail_domain != "nine.testrun.org" %}