diff --git a/doc/source/index_cli_reference.rst b/doc/source/index_cli_reference.rst index 51ae6d7d..d64ba248 100644 --- a/doc/source/index_cli_reference.rst +++ b/doc/source/index_cli_reference.rst @@ -15,3 +15,4 @@ Command Line Reference synadm.cli.matrix synadm.cli.regtok synadm.cli.notice + synadm.cli.raw diff --git a/doc/source/synadm.cli.raw.rst b/doc/source/synadm.cli.raw.rst new file mode 100644 index 00000000..b386fdd2 --- /dev/null +++ b/doc/source/synadm.cli.raw.rst @@ -0,0 +1,6 @@ +Raw +==== + +.. click:: synadm.cli.raw:raw_request_cmd + :prog: synadm raw + :nested: full diff --git a/synadm/api.py b/synadm/api.py index 2535e122..0abf3662 100644 --- a/synadm/api.py +++ b/synadm/api.py @@ -1370,3 +1370,17 @@ def notice_send(self, receivers, content_plain, content_html, paginate, else: data["user_id"] = receivers return [self.query("post", "v1/send_server_notice", data=data)] + + def raw_request(self, endpoint, method, data): + data_dict = {} + if method != "get": + self.log.debug("The data we are trying to parse and submit:") + self.log.debug(data) + try: # user provided json might be crap + data_dict = json.loads(data) + except Exception as error: + self.log.error("loading data: %s: %s", + type(error).__name__, error) + return None + + return self.query(method, endpoint, data=data_dict) diff --git a/synadm/cli/__init__.py b/synadm/cli/__init__.py index 8346743b..a758c997 100644 --- a/synadm/cli/__init__.py +++ b/synadm/cli/__init__.py @@ -505,4 +505,4 @@ def version(helper): # Import additional commands -from synadm.cli import room, user, media, group, history, matrix, regtok, notice # noqa: F401, E402, E501 +from synadm.cli import room, user, media, group, history, matrix, regtok, notice, raw # noqa: F401, E402, E501 diff --git a/synadm/cli/_common.py b/synadm/cli/_common.py new file mode 100644 index 00000000..5440fec3 --- /dev/null +++ b/synadm/cli/_common.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# synadm +# Copyright (C) 2020-2023 Johannes Tiefenbacher +# +# synadm is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# synadm is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"Common CLI options, option groups, helpers and utilities." + +import click + +from click_option_group import MutuallyExclusiveOptionGroup + + +def common_opts_raw_command(function): + return click.argument( + "endpoint", type=str + )( + click.option( + "--method", "-m", + type=click.Choice(["get", "post", "put", "delete"]), + help="The HTTP method used for the request.", + default="get", show_default=True + )(function) + ) + + +data_group_raw_command = MutuallyExclusiveOptionGroup( + "Data", + help="" +) + + +def data_opts_raw_command(function): + return data_group_raw_command.option( + "--data", "-d", type=str, default='{}', show_default=True, + help="""The JSON string sent in the body of post, put and delete + requests - provided as a string. Make sure to escape it from shell + interpretation by using single quotes. E.g '{"key1": "value1", + "key2": 123}'""" + )( + data_group_raw_command.option( + "--data-file", "-f", type=click.File("rt"), + show_default=True, + help="""Read JSON data from file. To read from stdin use "-" as the + filename argument.""" + )(function) + ) diff --git a/synadm/cli/matrix.py b/synadm/cli/matrix.py index 5a64ca6b..c233b7b2 100644 --- a/synadm/cli/matrix.py +++ b/synadm/cli/matrix.py @@ -21,6 +21,7 @@ import click from synadm import cli +from synadm.cli._common import common_opts_raw_command, data_opts_raw_command from click_option_group import optgroup, MutuallyExclusiveOptionGroup @@ -72,28 +73,8 @@ def login_cmd(helper, user_id, password): @matrix.command(name="raw") -@click.argument( - "endpoint", type=str) -@click.option( - "--method", "-m", type=click.Choice(["get", "post", "put", "delete"]), - help="""The HTTP method used for the request.""", - default="get", show_default=True) -@optgroup.group( - "Data input", - cls=MutuallyExclusiveOptionGroup, - help="") -@optgroup.option( - "--data", "-d", type=str, default='{}', show_default=True, - help="""The JSON string sent in the body of post, put and delete requests - - provided as a string. Make sure to escape it from shell interpretation by - using single quotes. E.g '{"key1": "value1", "key2": 123}' - """) -@optgroup.option( - "--data-file", "-f", type=click.File("rt"), - show_default=True, - help="""Read JSON data from file. To read from stdin use "-" as the - filename argument. - """) +@common_opts_raw_command +@data_opts_raw_command @optgroup.group( "Matrix token", cls=MutuallyExclusiveOptionGroup, @@ -101,43 +82,47 @@ def login_cmd(helper, user_id, password): @optgroup.option( "--token", "-t", type=str, envvar='MTOKEN', show_default=True, help="""Token used for Matrix authentication instead of the configured - admin user's token. If --token (and --prompt) option is missing, the token - is read from environment variable $MTOKEN instead. To make sure a user's - token does not show up in system logs, don't provide it on the shell - directly but set $MTOKEN with shell command `read MTOKEN`.""") + admin user's token. If ``--token`` (and ``--prompt``) option is missing, + the token is read from environment variable ``$MTOKEN`` instead. To make + sure a user's token does not show up in system logs, don't provide it on + the shell directly but set ``$MTOKEN`` with shell command ``read + MTOKEN``.""") @optgroup.option( "--prompt", "-p", is_flag=True, show_default=True, help="""Prompt for the token used for Matrix authentication. This option always overrides $MTOKEN.""") @click.pass_obj def raw_request_cmd(helper, endpoint, method, data, data_file, token, prompt): - """ Execute a raw request to the Matrix API. + """ Execute a custom request to the Matrix API. The endpoint argument is the part of the URL _after_ the configured base - URL and Matrix path (see `synadm config`). A simple get request would e.g - look like this: `synadm matrix raw client/versions` + URL (actually "Synapse base URL") and "Matrix API path" (see ``synadm + config``). A get request could look like this: ``synadm matrix raw + client/versions`` URL encoding must be handled at this point. Consider + enabling debug outputs via synadm's global flag ``-vv`` - Use either --token or --prompt to provide a user's token and execute Matrix - commands on their behalf. Respect the privacy of others! Be responsible! + Use either ``--token`` or ``--prompt`` to provide a user's token and + execute Matrix commands on their behalf. Respect the privacy of others! + Act responsible! + \b The precedence rules for token reading are: - 1. Interactive input using --prompt; 2. Set on CLI via --token string; - 3. Read from environment variable $MTOKEN; 4. Preconfigured admin token - set in synadm's config file. + 1. Interactive input using ``--prompt``; + 2. Set on CLI via ``--token`` + 3. Read from environment variable ``$MTOKEN``; + 4. Preconfigured admin token set via ``synadm config``. + + Caution: Passing secrets as CLI arguments or via environment variables is + not considered secure. Know what you are doing! """ if prompt: token = click.prompt("Matrix token", type=str) if data_file: - raw_request = helper.matrix_api.raw_request( - endpoint, - method, - data_file.read(), - token=token - ) - else: - raw_request = helper.matrix_api.raw_request(endpoint, method, data, - token=token) + data = data_file.read() + + raw_request = helper.matrix_api.raw_request(endpoint, method, data, + token=token) if helper.no_confirm: if raw_request is None: diff --git a/synadm/cli/raw.py b/synadm/cli/raw.py new file mode 100644 index 00000000..0f1501e2 --- /dev/null +++ b/synadm/cli/raw.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# synadm +# Copyright (C) 2020-2023 Johannes Tiefenbacher +# +# synadm is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# synadm is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""This module holds the `raw` command only.""" + +import click + +from synadm import cli +from synadm.cli._common import common_opts_raw_command, data_opts_raw_command + + +@cli.root.command(name="raw") +@common_opts_raw_command +@data_opts_raw_command +@click.pass_obj +def raw_request_cmd(helper, endpoint, method, data, data_file): + """ Issue a custom request to the Synapse Admin API. + + The endpoint argument is the part of the URL _after_ the configured + "Synapse base URL" and "Synapse Admin API path" (see ``synadm config``). + A get request to the "Query User Account API" would look like this: + ``synadm raw v2/users/%40testuser%3Aexample.org``. URL encoding must be + handled at this point. Consider enabling debug outputs via synadm's global + flag ``-vv`` + """ + if data_file: + data = data_file.read() + + raw_request = helper.api.raw_request(endpoint, method, data) + + if helper.no_confirm: + if raw_request is None: + raise SystemExit(1) + helper.output(raw_request) + else: + if raw_request is None: + click.echo("The Admin API's response was empty or JSON data " + "could not be loaded.") + raise SystemExit(1) + else: + helper.output(raw_request)