Compare commits

...

11 Commits

Author SHA1 Message Date
missytake
4a7b425958 www: if invite_token is set, don't show the QR code & register button 2025-09-12 09:35:12 +02:00
missytake
cb87c85c03 doc: add recommendation on token length 2025-09-12 09:35:11 +02:00
missytake
5b4eb1701e CI: cmdeploy fmt 2025-09-12 09:32:48 +02:00
missytake
f578704069 doc: document invite tokens 2025-09-12 09:32:48 +02:00
missytake
7319977527 doveauth: allow more than one invite token 2025-09-12 09:32:48 +02:00
missytake
7dcd109bec doveauth: invite token doesn't overwrite nocreate file, must be at beginning of password 2025-09-12 09:32:48 +02:00
missytake
6940175b06 add changelog 2025-09-12 09:32:47 +02:00
missytake
de139bde18 nginx: pass on invite tokens even for GET requests 2025-09-12 09:32:15 +02:00
missytake
a92c9ff275 tests: ensure valid invite token in password overrides nocreate file 2025-09-12 09:32:15 +02:00
missytake
1afdab7b20 fix lint 2025-09-12 09:32:15 +02:00
missytake
56cbd6f35b doveauth: add invite_token to override nocreate file 2025-09-12 09:32:15 +02:00
8 changed files with 79 additions and 11 deletions

View File

@@ -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))

View File

@@ -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,

View File

@@ -31,6 +31,7 @@ class Config:
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", "")

View File

@@ -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,

View File

@@ -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():

View File

@@ -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):

View File

@@ -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;

View File

@@ -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 %}
<a class="cta-button" href="DCACCOUNT:https://{{ config.mail_domain }}/new">Get a {{config.mail_domain}} chat profile</a>
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" %}
<div class="experimental">Note: this is only a temporary development chatmail service</div>