Skip to content

Commit

Permalink
feat(matrix_chat): add automatic registering of bot user
Browse files Browse the repository at this point in the history
Sadly the registration of a new user requires dirty interaction with the
matrix-synapse server (reconfiguration and restart).
See matrix-org/synapse#5323
  • Loading branch information
sumpfralle committed May 16, 2021
1 parent c94fb2d commit d9343cd
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 0 deletions.
2 changes: 2 additions & 0 deletions debian/grouprise-matrix.postinst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ if [ "$1" = "configure" ]; then
configure_grouprise_matrix
configure_grouprise_element
configure_grouprise
# register the privileged bot account
GROUPRISE_USER=root grouprisectl matrix_register_grouprise_bot
fi

set +eu
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import atexit
import hashlib
import hmac
import os
import subprocess
import random
import string
import sys
import tempfile
import time
import urllib.error

from django.core.management.base import BaseCommand

from grouprise.features.matrix_chat.settings import MATRIX_SETTINGS
from grouprise.features.matrix_chat.matrix_admin import MatrixAdmin


DEFAULT_MATRIX_CONFIG_LOCATIONS = [
"/etc/matrix-synapse/homeserver.yaml",
"/etc/matrix-synapse/conf.d/",
]


def _get_random_string(length):
return "".join(random.choice(string.ascii_letters) for _ in range(length))


def _restart_matrix_server(wait_seconds=None):
try:
subprocess.call(["service", "matrix-synapse", "restart"])
except FileNotFoundError:
subprocess.call(["systemctl", "restart", "matrix-synapse"])
if wait_seconds:
time.sleep(wait_seconds)


def _get_matrix_config_key(key, config_locations):
get_config_key_args = ["/usr/share/matrix-synapse/get-config-key"]
for location in config_locations:
get_config_key_args.extend(["--config", location])
get_config_key_args.append(key)
try:
token = subprocess.check_output(get_config_key_args, stderr=subprocess.PIPE)
except OSError:
return None
except subprocess.CalledProcessError:
# the key did not exist or any other failure
return None
if token:
# remove newline (emitted by "get-config-key")
token = token.strip()
if token:
return token.decode()
else:
return None


class Command(BaseCommand):
args = ""
help = "Register a grouprise bot with local admin privileges on the matrix server"

def add_arguments(self, parser):
parser.add_argument(
"--bot-username",
type=str,
default=MATRIX_SETTINGS.BOT_USERNAME,
help="local Matrix account username to be used for the grouprise bot",
)
parser.add_argument(
"--matrix-api-url",
type=str,
default=MATRIX_SETTINGS.ADMIN_API_URL,
)
parser.add_argument(
"--matrix-config-location",
type=str,
action="append",
dest="matrix_config_locations",
help=(
"A matrix-synapse configuration file or directory. This argument may be specified "
"multiple times."
),
)

def request_admin_user_via_api(self, api, username, registration_token):
"""generate a user with admin privileges """
# get a registration nonce
register_api_path = "_synapse/admin/v1/register"
nonce = api.request_get(register_api_path)["nonce"]
password = "".join(random.choice(string.ascii_letters) for _ in range(64))
mac = hmac.new(key=registration_token.encode("utf8"), digestmod=hashlib.sha1)
mac.update(nonce.encode("utf8"))
mac.update(b"\x00")
mac.update(username.encode("utf8"))
mac.update(b"\x00")
mac.update(password.encode("utf8"))
mac.update(b"\x00")
mac.update(b"admin")
response = api.request_post(
register_api_path,
{
"nonce": nonce,
"username": username,
"password": password,
"mac": mac.hexdigest(),
"admin": True,
"user_type": None,
},
)
return response["access_token"]

def register_admin_account(self, bot_username, matrix_api_url, config_locations):
"""register an admin account and return its access token
This process is quite complicated, since we need to enable a registration token in advance
and restart the matrix server. This is not fun :(
A proper solution is discussed in this issue:
https://github.com/matrix-org/synapse/issues/5323
"""
# retrieve an existing registration token (maybe one is configured)
registration_token = _get_matrix_config_key(
"registration_shared_secret", config_locations
)
if not registration_token:
synapse_config_directories = [
config_location
for config_location in config_locations
if os.path.isdir(config_location)
]
if synapse_config_directories:
config_directory = synapse_config_directories[0]
else:
config_directory = "/etc/matrix-synapse/conf.d"
# generate a new configuration file with a registration token and restart the server
handle, temp_registration_token_filename = tempfile.mkstemp(
suffix=".yaml",
prefix="zzz-grouprise-matrix-temporary-registration-key-",
dir=config_directory,
)

def _cleanup_matrix_registration_configuration():
try:
os.unlink(temp_registration_token_filename)
except OSError:
pass
_restart_matrix_server()

atexit.register(_cleanup_matrix_registration_configuration)
registration_token = _get_random_string(64)
os.write(
handle, b"registration_shared_secret: " + registration_token.encode()
)
os.close(handle)
# sadly the matrix-synapse user needs read access to that file
os.chmod(temp_registration_token_filename, 0o644)
# wait for the restart of the matrix server
_restart_matrix_server(10)
else:
temp_registration_token_filename = None
# generate an admin token
api = MatrixAdmin(None, matrix_api_url)
try:
bot_access_token = self.request_admin_user_via_api(
api, bot_username, registration_token
)
except urllib.error.HTTPError as exc:
error_message = "Failed to create grouprise bot account ('{}'). ".format(
bot_username
)
if exc.code == 400:
error_message += "Maybe it already exists?"
else:
error_message += "API response: {}".format(exc)
self.stderr.write(self.style.ERROR(error_message))
sys.exit(10)
if temp_registration_token_filename:
# remove the temporary configuration file and restart matrix again
_cleanup_matrix_registration_configuration()
atexit.unregister(_cleanup_matrix_registration_configuration)
return bot_access_token

def verify_configured_bot(self, matrix_api_url):
api = MatrixAdmin(MATRIX_SETTINGS.BOT_ACCESS_TOKEN, matrix_api_url)
try:
user_id_for_token = api.request_get("_matrix/client/r0/account/whoami")[
"user_id"
]
except urllib.error.HTTPError as exc:
if exc.code == 401:
return False
return user_id_for_token == "@{}:{}".format(
MATRIX_SETTINGS.BOT_USERNAME, MATRIX_SETTINGS.DOMAIN
)

def handle(self, *args, **options):
if (
options["bot_username"] == MATRIX_SETTINGS.BOT_USERNAME
) and self.verify_configured_bot(options["matrix_api_url"]):
self.stdout.write(
self.style.NOTICE(
"The requested bot username ({}) is already configured properly.".format(
options["bot_username"]
)
)
)
else:
# we need to create a new user
config_locations = options["matrix_config_locations"]
if not config_locations:
config_locations = DEFAULT_MATRIX_CONFIG_LOCATIONS
access_token = self.register_admin_account(
options["bot_username"],
options["matrix_api_url"],
config_locations,
)
print(access_token)

0 comments on commit d9343cd

Please sign in to comment.