Compare commits

...
11 Commits
8 changed files with 79 additions and 11 deletions
+4
View File
@@ -2,6 +2,10 @@
## untagged ## 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 - dovecot: keep mailbox index only in memory to avoid unnecessary disc usage
([#632](https://github.com/chatmail/relay/pull/632)) ([#632](https://github.com/chatmail/relay/pull/632))
+16 -1
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 will typically be empty unless the user of that address hasn't been online
for a while. 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, If you need to stop address creation,
e.g. because some script is wildly creating addresses, e.g. because some script is wildly creating addresses,
+1
View File
@@ -31,6 +31,7 @@ class Config:
self.username_min_length = int(params["username_min_length"]) self.username_min_length = int(params["username_min_length"])
self.username_max_length = int(params["username_max_length"]) self.username_max_length = int(params["username_max_length"])
self.password_min_length = int(params["password_min_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_senders = params["passthrough_senders"].split()
self.passthrough_recipients = params["passthrough_recipients"].split() self.passthrough_recipients = params["passthrough_recipients"].split()
self.www_folder = params.get("www_folder", "") self.www_folder = params.get("www_folder", "")
+12 -1
View File
@@ -26,8 +26,19 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
if os.path.exists(NOCREATE_FILE): if os.path.exists(NOCREATE_FILE):
logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.")
return False 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( logging.warning(
"Password needs to be at least %s characters long", "Password needs to be at least %s characters long",
config.password_min_length, config.password_min_length,
+6 -1
View File
@@ -3,6 +3,7 @@
"""CGI script for creating new accounts.""" """CGI script for creating new accounts."""
import json import json
import os
import random import random
import secrets import secrets
import string import string
@@ -20,7 +21,11 @@ def create_newemail_dict(config: Config):
secrets.choice(ALPHANUMERIC_PUNCT) secrets.choice(ALPHANUMERIC_PUNCT)
for _ in range(config.password_min_length + 3) 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(): def print_new_account():
+29 -3
View File
@@ -64,12 +64,38 @@ def test_dont_overwrite_password_on_wrong_login(dictproxy):
assert res["password"] == res2["password"] assert res["password"] == res2["password"]
def test_nocreate_file(monkeypatch, tmpdir, dictproxy): @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 = tmpdir.join("nocreate")
p.write("") p.write("")
monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p)) monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p))
dictproxy.lookup_passdb("newuser12@chat.example.org", "zequ0Aimuchoodaechik") example_config.invite_token = invite_token
assert not dictproxy.lookup_userdb("newuser12@chat.example.org") dictproxy.lookup_passdb("newuser12@chat.example.org", password)
assert bool(dictproxy.lookup_userdb("newuser12@chat.example.org")) == account
def test_handle_dovecot_request(dictproxy): def test_handle_dovecot_request(dictproxy):
+3 -2
View File
@@ -84,12 +84,13 @@ http {
if ($request_method = GET) { if ($request_method = GET) {
# Redirect to Delta Chat, # Redirect to Delta Chat,
# which will in turn do a POST request. # 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; fastcgi_pass unix:/run/fcgiwrap.socket;
include /etc/nginx/fastcgi_params; include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py; 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. # Old URL for compatibility with e.g. printed QR codes.
@@ -100,7 +101,7 @@ http {
# Redirects are only for browsers. # Redirects are only for browsers.
location /cgi-bin/newemail.py { location /cgi-bin/newemail.py {
if ($request_method = GET) { 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; fastcgi_pass unix:/run/fcgiwrap.socket;
+5
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). please see our [privacy policy](privacy.html).
{% endif %} {% 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> <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 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 🐣 **Choose** your Avatar and Name
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee) 💬 **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" %} {% if config.mail_domain != "nine.testrun.org" %}
<div class="experimental">Note: this is only a temporary development chatmail service</div> <div class="experimental">Note: this is only a temporary development chatmail service</div>