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 = {
"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."
}

View file

@ -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):

View file

@ -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.

View file

@ -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,

View file

@ -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({