-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(matrix_chat): add automatic registering of bot user
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
1 parent
c94fb2d
commit d9343cd
Showing
2 changed files
with
220 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
218 changes: 218 additions & 0 deletions
218
grouprise/features/matrix_chat/management/commands/matrix_register_grouprise_bot.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |