From 8c3e3a3255d7cd5846d85911d98bcd732dbd0d06 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 19 Nov 2021 15:41:14 +0200 Subject: [PATCH] Improve config comments and errors with mbc auth --- maubot/cli/commands/auth.py | 2 +- maubot/config.py | 5 ++- maubot/example-config.yaml | 15 +++++--- maubot/management/api/client_auth.py | 52 +++++++++++++++------------- maubot/management/api/responses.py | 7 ++++ 5 files changed, 50 insertions(+), 31 deletions(-) diff --git a/maubot/cli/commands/auth.py b/maubot/cli/commands/auth.py index d8cb3cb..c537cf1 100644 --- a/maubot/cli/commands/auth.py +++ b/maubot/cli/commands/auth.py @@ -32,7 +32,7 @@ enc = functools.partial(quote, safe="") friendly_errors = { "server_not_found": "Registration target server not found.\n\n" "To log in or register through maubot, you must add the server to the\n" - "registration_secrets section in the config. If you only want to log in,\n" + "homeservers section in the config. If you only want to log in,\n" "leave the `secret` field empty." } diff --git a/maubot/config.py b/maubot/config.py index eb5072b..92f5aa9 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -55,7 +55,10 @@ class Config(BaseFileConfig): base["server.unshared_secret"] = self._new_token() else: base["server.unshared_secret"] = shared_secret - copy("registration_secrets") + if "registration_secrets" in self: + base["homeservers"] = self["registration_secrets"] + else: + copy("homeservers") copy("admins") for username, password in base["admins"].items(): if password and not bcrypt_regex.match(password): diff --git a/maubot/example-config.yaml b/maubot/example-config.yaml index 8f3e288..0f82e12 100644 --- a/maubot/example-config.yaml +++ b/maubot/example-config.yaml @@ -42,13 +42,18 @@ server: # Set to "generate" to generate and save a new token at startup. unshared_secret: generate -# Shared registration secrets to allow registering new users from the management UI -registration_secrets: - example.com: +# Known homeservers. This is required for the `mbc auth` command and also allows +# more convenient access from the management UI. This is not required to create +# clients in the management UI, since you can also just type the homeserver URL +# into the box there. +homeservers: + matrix.org: # Client-server API URL - url: https://example.com + url: https://matrix-client.matrix.org # registration_shared_secret from synapse config - secret: synapse_shared_registration_secret + # You can leave this empty if you don't have access to the homeserver. + # When this is empty, `mbc auth --register` won't work, but `mbc auth` (login) will. + secret: null # List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password # to prevent normal login. Root is a special user that can't have a password and will always exist. diff --git a/maubot/management/api/client_auth.py b/maubot/management/api/client_auth.py index 36c4d9a..a3a5878 100644 --- a/maubot/management/api/client_auth.py +++ b/maubot/management/api/client_auth.py @@ -1,5 +1,5 @@ # maubot - A plugin-based Matrix bot system. -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2021 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -22,30 +22,36 @@ import string import hmac from aiohttp import web -from mautrix.api import HTTPAPI, Path, SynapseAdminPath, Method +from mautrix.api import SynapseAdminPath, Method from mautrix.errors import MatrixRequestError +from mautrix.client import ClientAPI +from mautrix.types import LoginType from .base import routes, get_config, get_loop from .responses import resp -def registration_secrets() -> Dict[str, Dict[str, str]]: - return get_config()["registration_secrets"] +def known_homeservers() -> Dict[str, Dict[str, str]]: + return get_config()["homeservers"] @routes.get("/client/auth/servers") -async def get_registerable_servers(_: web.Request) -> web.Response: - return web.json_response({key: value["url"] for key, value in registration_secrets().items()}) +async def get_known_servers(_: web.Request) -> web.Response: + return web.json_response({key: value["url"] for key, value in known_homeservers().items()}) -AuthRequestInfo = NamedTuple("AuthRequestInfo", api=HTTPAPI, secret=str, username=str, - password=str, user_type=str) +class AuthRequestInfo(NamedTuple): + client: ClientAPI + secret: str + username: str + password: str + user_type: str async def read_client_auth_request(request: web.Request) -> Tuple[Optional[AuthRequestInfo], Optional[web.Response]]: server_name = request.match_info.get("server", None) - server = registration_secrets().get(server_name, None) + server = known_homeservers().get(server_name, None) if not server: return None, resp.server_not_found try: @@ -59,10 +65,10 @@ async def read_client_auth_request(request: web.Request) -> Tuple[Optional[AuthR return None, resp.username_or_password_missing try: base_url = server["url"] - secret = server["secret"] except KeyError: return None, resp.invalid_server - api = HTTPAPI(base_url, "", loop=get_loop()) + secret = server.get("secret") + api = ClientAPI(base_url=base_url, loop=get_loop()) user_type = body.get("user_type", "bot") return AuthRequestInfo(api, secret, username, password, user_type), None @@ -88,9 +94,12 @@ async def register(request: web.Request) -> web.Response: info, err = await read_client_auth_request(request) if err is not None: return err - api, secret, username, password, user_type = info + client: ClientAPI + client, secret, username, password, user_type = info + if not secret: + return resp.registration_secret_not_found path = SynapseAdminPath.v1.register - res = await api.request(Method.GET, path) + res = await client.api.request(Method.GET, path) content = { "nonce": res["nonce"], "username": username, @@ -100,7 +109,7 @@ async def register(request: web.Request) -> web.Response: "user_type": user_type, } try: - return web.json_response(await api.request(Method.POST, path, content=content)) + return web.json_response(await client.api.request(Method.POST, path, content=content)) except MatrixRequestError as e: return web.json_response({ "errcode": e.errcode, @@ -114,18 +123,13 @@ async def login(request: web.Request) -> web.Response: info, err = await read_client_auth_request(request) if err is not None: return err - api, _, username, password, _ = info device_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) + client = info.client try: - return web.json_response(await api.request(Method.POST, Path.login, content={ - "type": "m.login.password", - "identifier": { - "type": "m.id.user", - "user": username, - }, - "password": password, - "device_id": f"maubot_{device_id}", - })) + res = await client.login(identifier=info.username, login_type=LoginType.PASSWORD, + password=info.password, device_id=f"maubot_{device_id}", + initial_device_display_name="Maubot", store_access_token=False) + return web.json_response(res.serialize()) except MatrixRequestError as e: return web.json_response({ "errcode": e.errcode, diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index b30d49d..a4e76d8 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -180,6 +180,13 @@ class _Response: "errcode": "server_not_found", }, status=HTTPStatus.NOT_FOUND) + @property + def registration_secret_not_found(self) -> web.Response: + return web.json_response({ + "error": "Config does not have a registration secret for that server", + "errcode": "registration_secret_not_found", + }, status=HTTPStatus.NOT_FOUND) + @property def plugin_has_no_database(self) -> web.Response: return web.json_response({