mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-27 10:44:33 +00:00
matrixsynapse: Add token based registration verification
- Allow setting registration verification to token based registration verification. - Configure the server with registration secret. Use the registration secret to register an admin account for FreedomBox's use. Store the access token provided during registration for future use. - Use Admin API and the access token to create a registration verification token. Show list of all registration tokens on app page. Tests: - On a fresh installation, setup succeeds, public registration is disabled. Enabling public registration sets verification to be disabled by default. Registration tokens are not shown in status. - Without the patch, install the app and enable public registration. Apply the patches. After update registration verification will show as disabled. - Setting verification method to registration token works. freedombox-registration-secret.yaml file is created. This file has 0o600 permissions and is owned by matrix-synapse:nogroup. freedombox-admin-access-token.txt file is created. This file has 0o600 permissions and is owned by root:root. List of registration tokens are shown in status section. Registration with Element app works with the token listed. - Disabling registration verification works. Registration tokens are not shown in status section. Registration with Element app works without verification. - Disable app. Try to update the verification configuration to use tokens. An error should be thrown that configuration can't be updated when app is disabled. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
485107604f
commit
92aff3e63c
@ -10,6 +10,17 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from plinth.modules.coturn.forms import turn_uris_validator
|
from plinth.modules.coturn.forms import turn_uris_validator
|
||||||
from plinth.utils import format_lazy
|
from plinth.utils import format_lazy
|
||||||
|
|
||||||
|
_registration_verification_choices = [
|
||||||
|
('disabled',
|
||||||
|
_('Disabled. This could lead to adversaries registering many spam '
|
||||||
|
'accounts on your server with automated scripts.')),
|
||||||
|
('token',
|
||||||
|
_('Require users creating a new account to use a registration token. A '
|
||||||
|
'token will be created automatically. Pass this token to your '
|
||||||
|
'potential new users. They will be asked for the token during '
|
||||||
|
'registration. (recommended)')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class MatrixSynapseForm(forms.Form):
|
class MatrixSynapseForm(forms.Form):
|
||||||
enable_public_registration = forms.BooleanField(
|
enable_public_registration = forms.BooleanField(
|
||||||
@ -18,6 +29,11 @@ class MatrixSynapseForm(forms.Form):
|
|||||||
'can register a new account on your Matrix server. Disable this '
|
'can register a new account on your Matrix server. Disable this '
|
||||||
'if you only want existing users to be able to use it.'))
|
'if you only want existing users to be able to use it.'))
|
||||||
|
|
||||||
|
registration_verification = forms.ChoiceField(
|
||||||
|
label=_('Verification method for registration'),
|
||||||
|
choices=_registration_verification_choices, required=True,
|
||||||
|
widget=forms.RadioSelect)
|
||||||
|
|
||||||
enable_managed_turn = forms.BooleanField(
|
enable_managed_turn = forms.BooleanField(
|
||||||
label=_('Automatically manage audio/video call setup'), required=False,
|
label=_('Automatically manage audio/video call setup'), required=False,
|
||||||
help_text=format_lazy(
|
help_text=format_lazy(
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Configure Matrix-Synapse server."""
|
"""Configure Matrix-Synapse server."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Optional
|
import shutil
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from plinth import action_utils
|
from plinth import action_utils, utils
|
||||||
from plinth.actions import privileged
|
from plinth.actions import privileged
|
||||||
|
|
||||||
CONF_DIR = "/etc/matrix-synapse/conf.d/"
|
CONF_DIR = "/etc/matrix-synapse/conf.d/"
|
||||||
@ -18,8 +22,12 @@ SERVER_NAME_PATH = CONF_DIR + 'server_name.yaml'
|
|||||||
STATIC_CONF_PATH = CONF_DIR + 'freedombox-static.yaml'
|
STATIC_CONF_PATH = CONF_DIR + 'freedombox-static.yaml'
|
||||||
LISTENERS_CONF_PATH = CONF_DIR + 'freedombox-listeners.yaml'
|
LISTENERS_CONF_PATH = CONF_DIR + 'freedombox-listeners.yaml'
|
||||||
REGISTRATION_CONF_PATH = CONF_DIR + 'freedombox-registration.yaml'
|
REGISTRATION_CONF_PATH = CONF_DIR + 'freedombox-registration.yaml'
|
||||||
|
REGISTRATION_SECRET_PATH = CONF_DIR + 'freedombox-registration-secret.yaml'
|
||||||
|
ADMIN_ACCESS_TOKEN_PATH = CONF_DIR + 'freedombox-admin-access-token.txt'
|
||||||
TURN_CONF_PATH = CONF_DIR + 'freedombox-turn.yaml'
|
TURN_CONF_PATH = CONF_DIR + 'freedombox-turn.yaml'
|
||||||
OVERRIDDEN_TURN_CONF_PATH = CONF_DIR + 'turn.yaml'
|
OVERRIDDEN_TURN_CONF_PATH = CONF_DIR + 'turn.yaml'
|
||||||
|
FREEDOMBOX_ADMIN_USERNAME = 'freedombox-admin'
|
||||||
|
ADMIN_API_BASE = 'http://localhost:8008/_synapse/admin/v1/'
|
||||||
|
|
||||||
STATIC_CONFIG = {
|
STATIC_CONFIG = {
|
||||||
'max_upload_size':
|
'max_upload_size':
|
||||||
@ -81,6 +89,8 @@ def get_config():
|
|||||||
|
|
||||||
if config.get('enable_registration_without_verification'):
|
if config.get('enable_registration_without_verification'):
|
||||||
registration_verification = 'disabled'
|
registration_verification = 'disabled'
|
||||||
|
elif config.get('registration_requires_token'):
|
||||||
|
registration_verification = 'token'
|
||||||
else:
|
else:
|
||||||
registration_verification = None
|
registration_verification = None
|
||||||
|
|
||||||
@ -94,9 +104,14 @@ def get_config():
|
|||||||
def set_config(public_registration: bool,
|
def set_config(public_registration: bool,
|
||||||
registration_verification: Optional[str] = None):
|
registration_verification: Optional[str] = None):
|
||||||
"""Enable/disable public user registration."""
|
"""Enable/disable public user registration."""
|
||||||
|
if registration_verification == 'token':
|
||||||
|
_create_registration_token()
|
||||||
|
|
||||||
config = {'enable_registration': public_registration}
|
config = {'enable_registration': public_registration}
|
||||||
if public_registration and registration_verification in ('disabled', None):
|
if public_registration and registration_verification in ('disabled', None):
|
||||||
config['enable_registration_without_verification'] = True
|
config['enable_registration_without_verification'] = True
|
||||||
|
elif registration_verification == 'token':
|
||||||
|
config['registration_requires_token'] = True
|
||||||
|
|
||||||
with open(REGISTRATION_CONF_PATH, 'w', encoding='utf-8') as reg_conf_file:
|
with open(REGISTRATION_CONF_PATH, 'w', encoding='utf-8') as reg_conf_file:
|
||||||
yaml.dump(config, reg_conf_file)
|
yaml.dump(config, reg_conf_file)
|
||||||
@ -155,3 +170,172 @@ def fix_public_registrations():
|
|||||||
and config['registration_verification'] is None):
|
and config['registration_verification'] is None):
|
||||||
set_config(public_registration=True,
|
set_config(public_registration=True,
|
||||||
registration_verification='disabled')
|
registration_verification='disabled')
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_server_is_running():
|
||||||
|
"""Raise an exception if matrix-synapse server is not running."""
|
||||||
|
if not action_utils.service_is_running('matrix-synapse'):
|
||||||
|
raise ProcessLookupError
|
||||||
|
|
||||||
|
|
||||||
|
def _get_registration_shared_secret() -> str:
|
||||||
|
"""Ensure that server is set to use a shared secret for registration."""
|
||||||
|
secret_file = pathlib.Path(REGISTRATION_SECRET_PATH)
|
||||||
|
if secret_file.exists():
|
||||||
|
with secret_file.open() as file_handle:
|
||||||
|
return yaml.safe_load(file_handle)['registration_shared_secret']
|
||||||
|
|
||||||
|
secret = utils.generate_password()
|
||||||
|
# Ensure that file is readable by matrix-synapse user and root.
|
||||||
|
previous_umask = os.umask(0o077)
|
||||||
|
try:
|
||||||
|
secret_file.write_text(f'registration_shared_secret: "{secret}"\n')
|
||||||
|
finally:
|
||||||
|
os.umask(previous_umask)
|
||||||
|
|
||||||
|
shutil.chown(secret_file, 'matrix-synapse', 'nogroup')
|
||||||
|
action_utils.service_try_restart('matrix-synapse')
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_api_error(response):
|
||||||
|
"""Raise exception if the response of API call was an error."""
|
||||||
|
if response.get('errcode'):
|
||||||
|
raise ConnectionError(response['errcode'], response['error'])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_access_token() -> str:
|
||||||
|
"""Create a new freedombox-admin account and return its access token.
|
||||||
|
|
||||||
|
For user registration Admin API, see:
|
||||||
|
https://matrix-org.github.io/synapse/latest/admin_api/register_api.html
|
||||||
|
"""
|
||||||
|
access_token_path = pathlib.Path(ADMIN_ACCESS_TOKEN_PATH)
|
||||||
|
if access_token_path.exists():
|
||||||
|
return access_token_path.read_text().strip()
|
||||||
|
|
||||||
|
shared_secret = _get_registration_shared_secret()
|
||||||
|
nonce = _get_nonce()
|
||||||
|
username = FREEDOMBOX_ADMIN_USERNAME
|
||||||
|
# No need to store password, we will use the access token.
|
||||||
|
password = utils.generate_password()
|
||||||
|
mac = _generate_mac(shared_secret, nonce, username, password, True)
|
||||||
|
data = {
|
||||||
|
'nonce': nonce,
|
||||||
|
'username': username,
|
||||||
|
'displayname': 'FreedomBox Admin',
|
||||||
|
'password': password,
|
||||||
|
'admin': True,
|
||||||
|
'mac': mac
|
||||||
|
}
|
||||||
|
request = requests.post(ADMIN_API_BASE + 'register', json=data)
|
||||||
|
response = request.json()
|
||||||
|
_parse_api_error(response)
|
||||||
|
|
||||||
|
# Ensure that file is only readable by root user.
|
||||||
|
previous_umask = os.umask(0o077)
|
||||||
|
try:
|
||||||
|
access_token_path.write_text(response['access_token'])
|
||||||
|
finally:
|
||||||
|
os.umask(previous_umask)
|
||||||
|
|
||||||
|
return response['access_token']
|
||||||
|
|
||||||
|
|
||||||
|
@privileged
|
||||||
|
def list_registration_tokens() -> List[Dict[str, Optional[Union[str, int]]]]:
|
||||||
|
"""Return the current list of registration tokens."""
|
||||||
|
if not action_utils.service_is_running('matrix-synapse'):
|
||||||
|
return []
|
||||||
|
|
||||||
|
access_token = _get_access_token()
|
||||||
|
return _list_registration_tokens(access_token)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_headers(access_token: str):
|
||||||
|
"""Return the common HTTP headers needed for synapse admin API.
|
||||||
|
|
||||||
|
For details on authorization to the Admin API, see:
|
||||||
|
https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html
|
||||||
|
"""
|
||||||
|
return {'Authorization': f'Bearer {access_token}'}
|
||||||
|
|
||||||
|
|
||||||
|
def _list_registration_tokens(
|
||||||
|
access_token: str) -> List[Dict[str, Optional[Union[str, int]]]]:
|
||||||
|
"""Use Admin API to fetch the list of registration tokens.
|
||||||
|
|
||||||
|
For details on registration tokens API, see:
|
||||||
|
https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/registration_tokens.html
|
||||||
|
"""
|
||||||
|
request = requests.get(ADMIN_API_BASE + 'registration_tokens',
|
||||||
|
headers=_get_headers(access_token))
|
||||||
|
response = request.json()
|
||||||
|
_parse_api_error(response)
|
||||||
|
return response['registration_tokens']
|
||||||
|
|
||||||
|
|
||||||
|
def _create_registration_token():
|
||||||
|
"""Make sure that at least one registration token is created.
|
||||||
|
|
||||||
|
For details on registration tokens API, see:
|
||||||
|
https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/registration_tokens.html
|
||||||
|
"""
|
||||||
|
_ensure_server_is_running()
|
||||||
|
|
||||||
|
access_token = _get_access_token()
|
||||||
|
tokens = _list_registration_tokens(access_token)
|
||||||
|
for token in tokens:
|
||||||
|
if token['uses_allowed'] is None and token['expiry_time'] is None:
|
||||||
|
# A token with unlimited uses and for unlimited time already
|
||||||
|
# exists.
|
||||||
|
return
|
||||||
|
|
||||||
|
if not tokens:
|
||||||
|
# API can generate a token for us but it includes special chars which
|
||||||
|
# is awkward to deal with for users.
|
||||||
|
token = utils.generate_password(size=12)
|
||||||
|
request = requests.post(ADMIN_API_BASE + 'registration_tokens/new',
|
||||||
|
headers=_get_headers(access_token),
|
||||||
|
json={'token': token})
|
||||||
|
response = request.json()
|
||||||
|
_parse_api_error(response)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nonce() -> str:
|
||||||
|
"""Make a preliminary registration request to get a nonce.
|
||||||
|
|
||||||
|
Nonce must be returned to the server during actual registration request. It
|
||||||
|
is used in computation of MAC and prevents replay attacks.
|
||||||
|
https://matrix-org.github.io/synapse/latest/admin_api/register_api.html
|
||||||
|
"""
|
||||||
|
request = requests.get(ADMIN_API_BASE + 'register')
|
||||||
|
response = request.json()
|
||||||
|
_parse_api_error(response)
|
||||||
|
return response['nonce']
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_mac(shared_secret: str, nonce: str, user: str, password: str,
|
||||||
|
admin: bool = False, user_type=None) -> str:
|
||||||
|
"""Generate MAC using HMAC-SHA1 algorithm.
|
||||||
|
|
||||||
|
For information on how to encode the data for MAC computation, see:
|
||||||
|
https://matrix-org.github.io/synapse/latest/admin_api/register_api.html
|
||||||
|
"""
|
||||||
|
mac = hmac.new(
|
||||||
|
key=shared_secret.encode('utf8'),
|
||||||
|
digestmod=hashlib.sha1,
|
||||||
|
)
|
||||||
|
|
||||||
|
mac.update(nonce.encode('utf8'))
|
||||||
|
mac.update(b'\x00')
|
||||||
|
mac.update(user.encode('utf8'))
|
||||||
|
mac.update(b'\x00')
|
||||||
|
mac.update(password.encode('utf8'))
|
||||||
|
mac.update(b'\x00')
|
||||||
|
mac.update(b'admin' if admin else b'notadmin')
|
||||||
|
if user_type:
|
||||||
|
mac.update(b'\x00')
|
||||||
|
mac.update(user_type.encode('utf8'))
|
||||||
|
|
||||||
|
return mac.hexdigest()
|
||||||
|
|||||||
@ -24,6 +24,50 @@
|
|||||||
enabled.
|
enabled.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
{% if config.public_registration and config.registration_verification == 'token' and registration_tokens %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
New users must use one of the following tokens for verification during
|
||||||
|
account registration:
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<div class="table-responsive table-registration-tokens">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Registration Token" %}</th>
|
||||||
|
<th>{% trans "Uses Allowed" %}</th>
|
||||||
|
<th>{% trans "Pending Registrations" %}</th>
|
||||||
|
<th>{% trans "Completed Registrations" %}</th>
|
||||||
|
<th>{% trans "Expiry Time" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for token in registration_tokens %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ token.token }}</td>
|
||||||
|
<td>
|
||||||
|
{% if token.uses_allowed is None %}
|
||||||
|
{% trans "Unlimited" %}
|
||||||
|
{% else %}
|
||||||
|
{{ token.uses_allowed }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ token.pending }}</td>
|
||||||
|
<td>{{ token.completed }}</td>
|
||||||
|
<td>
|
||||||
|
{% if token.expiry_time %}
|
||||||
|
{{ token.expiry_time|date:"DATETIME_FORMAT" }}
|
||||||
|
{% else %}
|
||||||
|
{% trans "None" %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if certificate_status != "valid" %}
|
{% if certificate_status != "valid" %}
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Views for the Matrix Synapse module."""
|
"""Views for the Matrix Synapse module."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
@ -61,21 +63,33 @@ class MatrixSynapseAppView(AppView):
|
|||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
context['domain_name'] = matrixsynapse.get_configured_domain_name()
|
context['domain_name'] = matrixsynapse.get_configured_domain_name()
|
||||||
context['certificate_status'] = matrixsynapse.get_certificate_status()
|
context['certificate_status'] = matrixsynapse.get_certificate_status()
|
||||||
|
context['config'] = privileged.get_config()
|
||||||
|
tokens = privileged.list_registration_tokens()
|
||||||
|
for token in tokens:
|
||||||
|
if token['expiry_time']:
|
||||||
|
date = datetime.datetime.utcfromtimestamp(
|
||||||
|
token['expiry_time'] / 1000)
|
||||||
|
token['expiry_time'] = date
|
||||||
|
|
||||||
|
context['registration_tokens'] = tokens
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
"""Return the values to fill in the form."""
|
"""Return the values to fill in the form."""
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
config, managed = get_turn_configuration()
|
turn_config, managed = get_turn_configuration()
|
||||||
|
config = privileged.get_config()
|
||||||
initial.update({
|
initial.update({
|
||||||
'enable_public_registration':
|
'enable_public_registration':
|
||||||
privileged.get_config()['public_registration'],
|
config['public_registration'],
|
||||||
|
'registration_verification':
|
||||||
|
config['registration_verification'] or 'disabled',
|
||||||
'enable_managed_turn':
|
'enable_managed_turn':
|
||||||
managed,
|
managed,
|
||||||
'turn_uris':
|
'turn_uris':
|
||||||
'\n'.join(config.uris),
|
'\n'.join(turn_config.uris),
|
||||||
'shared_secret':
|
'shared_secret':
|
||||||
config.shared_secret
|
turn_config.shared_secret
|
||||||
})
|
})
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
@ -107,10 +121,21 @@ class MatrixSynapseAppView(AppView):
|
|||||||
return old_config[prop] != new_config[prop]
|
return old_config[prop] != new_config[prop]
|
||||||
|
|
||||||
is_changed = False
|
is_changed = False
|
||||||
if changed('enable_public_registration'):
|
if (changed('enable_public_registration')
|
||||||
|
or changed('registration_verification')):
|
||||||
|
try:
|
||||||
privileged.set_config(
|
privileged.set_config(
|
||||||
public_registration=new_config['enable_public_registration'])
|
public_registration=new_config[
|
||||||
|
'enable_public_registration'],
|
||||||
|
registration_verification=new_config[
|
||||||
|
'registration_verification'])
|
||||||
is_changed = True
|
is_changed = True
|
||||||
|
except ProcessLookupError:
|
||||||
|
# Matrix Synapse server is not running
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
_('Registration configuration cannot be updated when app '
|
||||||
|
'is disabled.'))
|
||||||
|
|
||||||
if changed('enable_managed_turn') or changed('turn_uris') or \
|
if changed('enable_managed_turn') or changed('turn_uris') or \
|
||||||
changed('shared_secret'):
|
changed('shared_secret'):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user