Improve config comments and errors with mbc auth

This commit is contained in:
Tulir Asokan 2021-11-19 15:41:14 +02:00
parent 61711e8329
commit 8c3e3a3255
5 changed files with 50 additions and 31 deletions

View file

@ -32,7 +32,7 @@ enc = functools.partial(quote, safe="")
friendly_errors = { friendly_errors = {
"server_not_found": "Registration target server not found.\n\n" "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" "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." "leave the `secret` field empty."
} }

View file

@ -55,7 +55,10 @@ class Config(BaseFileConfig):
base["server.unshared_secret"] = self._new_token() base["server.unshared_secret"] = self._new_token()
else: else:
base["server.unshared_secret"] = shared_secret base["server.unshared_secret"] = shared_secret
copy("registration_secrets") if "registration_secrets" in self:
base["homeservers"] = self["registration_secrets"]
else:
copy("homeservers")
copy("admins") copy("admins")
for username, password in base["admins"].items(): for username, password in base["admins"].items():
if password and not bcrypt_regex.match(password): if password and not bcrypt_regex.match(password):

View file

@ -42,13 +42,18 @@ server:
# Set to "generate" to generate and save a new token at startup. # Set to "generate" to generate and save a new token at startup.
unshared_secret: generate unshared_secret: generate
# Shared registration secrets to allow registering new users from the management UI # Known homeservers. This is required for the `mbc auth` command and also allows
registration_secrets: # more convenient access from the management UI. This is not required to create
example.com: # 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 # Client-server API URL
url: https://example.com url: https://matrix-client.matrix.org
# registration_shared_secret from synapse config # 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 # 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. # to prevent normal login. Root is a special user that can't have a password and will always exist.

View file

@ -1,5 +1,5 @@
# maubot - A plugin-based Matrix bot system. # 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 # 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 # it under the terms of the GNU Affero General Public License as published by
@ -22,30 +22,36 @@ import string
import hmac import hmac
from aiohttp import web 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.errors import MatrixRequestError
from mautrix.client import ClientAPI
from mautrix.types import LoginType
from .base import routes, get_config, get_loop from .base import routes, get_config, get_loop
from .responses import resp from .responses import resp
def registration_secrets() -> Dict[str, Dict[str, str]]: def known_homeservers() -> Dict[str, Dict[str, str]]:
return get_config()["registration_secrets"] return get_config()["homeservers"]
@routes.get("/client/auth/servers") @routes.get("/client/auth/servers")
async def get_registerable_servers(_: web.Request) -> web.Response: async def get_known_servers(_: web.Request) -> web.Response:
return web.json_response({key: value["url"] for key, value in registration_secrets().items()}) return web.json_response({key: value["url"] for key, value in known_homeservers().items()})
AuthRequestInfo = NamedTuple("AuthRequestInfo", api=HTTPAPI, secret=str, username=str, class AuthRequestInfo(NamedTuple):
password=str, user_type=str) client: ClientAPI
secret: str
username: str
password: str
user_type: str
async def read_client_auth_request(request: web.Request) -> Tuple[Optional[AuthRequestInfo], async def read_client_auth_request(request: web.Request) -> Tuple[Optional[AuthRequestInfo],
Optional[web.Response]]: Optional[web.Response]]:
server_name = request.match_info.get("server", None) 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: if not server:
return None, resp.server_not_found return None, resp.server_not_found
try: try:
@ -59,10 +65,10 @@ async def read_client_auth_request(request: web.Request) -> Tuple[Optional[AuthR
return None, resp.username_or_password_missing return None, resp.username_or_password_missing
try: try:
base_url = server["url"] base_url = server["url"]
secret = server["secret"]
except KeyError: except KeyError:
return None, resp.invalid_server 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") user_type = body.get("user_type", "bot")
return AuthRequestInfo(api, secret, username, password, user_type), None 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) info, err = await read_client_auth_request(request)
if err is not None: if err is not None:
return err 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 path = SynapseAdminPath.v1.register
res = await api.request(Method.GET, path) res = await client.api.request(Method.GET, path)
content = { content = {
"nonce": res["nonce"], "nonce": res["nonce"],
"username": username, "username": username,
@ -100,7 +109,7 @@ async def register(request: web.Request) -> web.Response:
"user_type": user_type, "user_type": user_type,
} }
try: 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: except MatrixRequestError as e:
return web.json_response({ return web.json_response({
"errcode": e.errcode, "errcode": e.errcode,
@ -114,18 +123,13 @@ async def login(request: web.Request) -> web.Response:
info, err = await read_client_auth_request(request) info, err = await read_client_auth_request(request)
if err is not None: if err is not None:
return err return err
api, _, username, password, _ = info
device_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) device_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
client = info.client
try: try:
return web.json_response(await api.request(Method.POST, Path.login, content={ res = await client.login(identifier=info.username, login_type=LoginType.PASSWORD,
"type": "m.login.password", password=info.password, device_id=f"maubot_{device_id}",
"identifier": { initial_device_display_name="Maubot", store_access_token=False)
"type": "m.id.user", return web.json_response(res.serialize())
"user": username,
},
"password": password,
"device_id": f"maubot_{device_id}",
}))
except MatrixRequestError as e: except MatrixRequestError as e:
return web.json_response({ return web.json_response({
"errcode": e.errcode, "errcode": e.errcode,

View file

@ -180,6 +180,13 @@ class _Response:
"errcode": "server_not_found", "errcode": "server_not_found",
}, status=HTTPStatus.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 @property
def plugin_has_no_database(self) -> web.Response: def plugin_has_no_database(self) -> web.Response:
return web.json_response({ return web.json_response({