From 555b71f2483222d4bc4efcf73872f83c2015232e Mon Sep 17 00:00:00 2001 From: Amanda Hager Lopes de Andrade Katz Date: Mon, 16 Oct 2023 16:46:37 -0300 Subject: [PATCH] Add get_admin_access_token to the charm so interaction with Secret and Peer Data occurs only there --- src-docs/charm.py.md | 29 +- src-docs/mjolnir.py.md | 26 +- src-docs/secret_storage.py.md | 71 --- src-docs/workload.md | 517 ------------------- src/charm.py | 84 ++- src/mjolnir.py | 53 +- src/secret_storage.py | 111 ---- src/synapse/workload.py | 11 +- tests/unit/test_mjolnir.py | 137 +---- tests/unit/test_promote_user_admin_action.py | 8 +- 10 files changed, 168 insertions(+), 879 deletions(-) delete mode 100644 src-docs/secret_storage.py.md delete mode 100644 src-docs/workload.md delete mode 100644 src/secret_storage.py diff --git a/src-docs/charm.py.md b/src-docs/charm.py.md index bd40ec87..d40ab37c 100644 --- a/src-docs/charm.py.md +++ b/src-docs/charm.py.md @@ -5,6 +5,12 @@ # module `charm.py` Charm for Synapse on kubernetes. +**Global Variables** +--------------- +- **JUJU_HAS_SECRETS** +- **PEER_RELATION_NAME** +- **SECRET_ID** +- **SECRET_KEY** --- @@ -12,7 +18,7 @@ Charm for Synapse on kubernetes. ## class `SynapseCharm` Charm the service. - + ### function `__init__` @@ -69,7 +75,7 @@ Unit that this execution is responsible for. --- - + ### function `change_config` @@ -81,7 +87,24 @@ Change configuration. --- - + + +### function `get_admin_access_token` + +```python +get_admin_access_token() → Optional[str] +``` + +Get admin access token. + + + +**Returns:** + admin access token or None if fails. + +--- + + ### function `replan_nginx` diff --git a/src-docs/mjolnir.py.md b/src-docs/mjolnir.py.md index 3690d6f5..525b22fd 100644 --- a/src-docs/mjolnir.py.md +++ b/src-docs/mjolnir.py.md @@ -18,7 +18,7 @@ A class representing the Mjolnir plugin for Synapse application. Mjolnir is a moderation tool for Matrix to be used to protect your server from malicious invites, spam messages etc. See https://github.com/matrix-org/mjolnir/ for more details about it. - + ### function `__init__` @@ -46,12 +46,12 @@ Shortcut for more simple access the model. --- - + ### function `enable_mjolnir` ```python -enable_mjolnir() → None +enable_mjolnir(admin_access_token: str) → None ``` Enable mjolnir service. @@ -68,27 +68,33 @@ The required steps to enable Mjolnir are: - Override Mjolnir user rate limit. - Finally, add Mjolnir pebble layer. + + +**Args:** + + - `admin_access_token`: not empty admin access token. + --- - + ### function `get_membership_room_id` ```python -get_membership_room_id() → Optional[str] +get_membership_room_id(admin_access_token: str) → Optional[str] ``` Check if membership room exists. -**Returns:** - The room id or None if is not found. +**Args:** + + - `admin_access_token`: not empty admin access token. -**Raises:** - - - `AdminAccessTokenNotFoundError`: if there is not admin access token. +**Returns:** + The room id or None if is not found. diff --git a/src-docs/secret_storage.py.md b/src-docs/secret_storage.py.md deleted file mode 100644 index d57dd451..00000000 --- a/src-docs/secret_storage.py.md +++ /dev/null @@ -1,71 +0,0 @@ - - - - -# module `secret_storage.py` -Helper module used to manage interactions with Synapse secrets. - -**Global Variables** ---------------- -- **JUJU_HAS_SECRETS** -- **PEER_RELATION_NAME** -- **SECRET_ID** -- **SECRET_KEY** - ---- - - - -## function `get_admin_access_token` - -```python -get_admin_access_token(charm: CharmBase) → str -``` - -Get admin access token. - - - -**Args:** - - - `charm`: The charm object. - - - -**Returns:** - admin access token. - - - -**Raises:** - - - `AdminAccessTokenNotFoundError`: if admin access token is not found. - - ---- - -## class `AdminAccessTokenNotFoundError` -Exception raised when there is not admin access token. - -Attrs: msg (str): Explanation of the error. - - - -### function `__init__` - -```python -__init__(msg: str) -``` - -Initialize a new instance of the AdminAccessTokenNotFoundError exception. - - - -**Args:** - - - `msg` (str): Explanation of the error. - - - - - diff --git a/src-docs/workload.md b/src-docs/workload.md deleted file mode 100644 index 6f4a77e3..00000000 --- a/src-docs/workload.md +++ /dev/null @@ -1,517 +0,0 @@ - - - - -# module `workload` -Helper module used to manage interactions with Synapse. - -**Global Variables** ---------------- -- **CHECK_ALIVE_NAME** -- **CHECK_MJOLNIR_READY_NAME** -- **CHECK_NGINX_READY_NAME** -- **CHECK_READY_NAME** -- **COMMAND_MIGRATE_CONFIG** -- **SYNAPSE_CONFIG_DIR** -- **MJOLNIR_CONFIG_PATH** -- **MJOLNIR_HEALTH_PORT** -- **MJOLNIR_SERVICE_NAME** -- **PROMETHEUS_TARGET_PORT** -- **SYNAPSE_COMMAND_PATH** -- **SYNAPSE_CONFIG_PATH** -- **SYNAPSE_CONTAINER_NAME** -- **SYNAPSE_NGINX_CONTAINER_NAME** -- **SYNAPSE_NGINX_PORT** -- **SYNAPSE_SERVICE_NAME** - ---- - - - -## function `check_ready` - -```python -check_ready() → CheckDict -``` - -Return the Synapse container ready check. - - - -**Returns:** - - - `Dict`: check object converted to its dict representation. - - ---- - - - -## function `check_alive` - -```python -check_alive() → CheckDict -``` - -Return the Synapse container alive check. - - - -**Returns:** - - - `Dict`: check object converted to its dict representation. - - ---- - - - -## function `check_nginx_ready` - -```python -check_nginx_ready() → CheckDict -``` - -Return the Synapse NGINX container check. - - - -**Returns:** - - - `Dict`: check object converted to its dict representation. - - ---- - - - -## function `check_mjolnir_ready` - -```python -check_mjolnir_ready() → CheckDict -``` - -Return the Synapse Mjolnir service check. - - - -**Returns:** - - - `Dict`: check object converted to its dict representation. - - ---- - - - -## function `get_registration_shared_secret` - -```python -get_registration_shared_secret(container: Container) → Optional[str] -``` - -Get registration_shared_secret from configuration file. - - - -**Args:** - - - `container`: Container of the charm. - - - -**Returns:** - registration_shared_secret value. - - ---- - - - -## function `execute_migrate_config` - -```python -execute_migrate_config(container: Container, charm_state: CharmState) → None -``` - -Run the Synapse command migrate_config. - - - -**Args:** - - - `container`: Container of the charm. - - `charm_state`: Instance of CharmState. - - - -**Raises:** - - - `CommandMigrateConfigError`: something went wrong running migrate_config. - - ---- - - - -## function `enable_metrics` - -```python -enable_metrics(container: Container) → None -``` - -Change the Synapse configuration to enable metrics. - - - -**Args:** - - - `container`: Container of the charm. - - - -**Raises:** - - - `EnableMetricsError`: something went wrong enabling metrics. - - ---- - - - -## function `enable_serve_server_wellknown` - -```python -enable_serve_server_wellknown(container: Container) → None -``` - -Change the Synapse configuration to enable server wellknown file. - - - -**Args:** - - - `container`: Container of the charm. - - - -**Raises:** - - - `WorkloadError`: something went wrong enabling configuration. - - ---- - - - -## function `create_mjolnir_config` - -```python -create_mjolnir_config( - container: Container, - access_token: str, - room_id: str -) → None -``` - -Create mjolnir configuration. - - - -**Args:** - - - `container`: Container of the charm. - - `access_token`: access token to be used by the Mjolnir. - - `room_id`: management room id monitored by the Mjolnir. - - - -**Raises:** - - - `CreateMjolnirConfigError`: something went wrong creating mjolnir config. - - ---- - - - -## function `enable_saml` - -```python -enable_saml(container: Container, charm_state: CharmState) → None -``` - -Change the Synapse configuration to enable SAML. - - - -**Args:** - - - `container`: Container of the charm. - - `charm_state`: Instance of CharmState. - - - -**Raises:** - - - `EnableSAMLError`: something went wrong enabling SAML. - - ---- - - - -## function `enable_smtp` - -```python -enable_smtp(container: Container, charm_state: CharmState) → None -``` - -Change the Synapse configuration to enable SMTP. - - - -**Args:** - - - `container`: Container of the charm. - - `charm_state`: Instance of CharmState. - - - -**Raises:** - - - `WorkloadError`: something went wrong enabling SMTP. - - ---- - - - -## function `reset_instance` - -```python -reset_instance(container: Container) → None -``` - -Erase data and config server_name. - - - -**Args:** - - - `container`: Container of the charm. - - - -**Raises:** - - - `PathError`: if somethings goes wrong while erasing the Synapse directory. - - ---- - - - -## function `get_environment` - -```python -get_environment(charm_state: CharmState) → Dict[str, str] -``` - -Generate a environment dictionary from the charm configurations. - - - -**Args:** - - - `charm_state`: Instance of CharmState. - - - -**Returns:** - A dictionary representing the Synapse environment variables. - - ---- - - - -## class `WorkloadError` -Exception raised when something fails while interacting with workload. - -Attrs: msg (str): Explanation of the error. - - - -### method `__init__` - -```python -__init__(msg: str) -``` - -Initialize a new instance of the SynapseWorkloadError exception. - - - -**Args:** - - - `msg` (str): Explanation of the error. - - - - - ---- - - - -## class `CommandMigrateConfigError` -Exception raised when a charm configuration is invalid. - - - -### method `__init__` - -```python -__init__(msg: str) -``` - -Initialize a new instance of the SynapseWorkloadError exception. - - - -**Args:** - - - `msg` (str): Explanation of the error. - - - - - ---- - - - -## class `ServerNameModifiedError` -Exception raised while checking configuration file. - - - -### method `__init__` - -```python -__init__(msg: str) -``` - -Initialize a new instance of the SynapseWorkloadError exception. - - - -**Args:** - - - `msg` (str): Explanation of the error. - - - - - ---- - - - -## class `EnableMetricsError` -Exception raised when something goes wrong while enabling metrics. - - - -### method `__init__` - -```python -__init__(msg: str) -``` - -Initialize a new instance of the SynapseWorkloadError exception. - - - -**Args:** - - - `msg` (str): Explanation of the error. - - - - - ---- - - - -## class `CreateMjolnirConfigError` -Exception raised when something goes wrong while creating mjolnir config. - - - -### method `__init__` - -```python -__init__(msg: str) -``` - -Initialize a new instance of the SynapseWorkloadError exception. - - - -**Args:** - - - `msg` (str): Explanation of the error. - - - - - ---- - - - -## class `EnableSAMLError` -Exception raised when something goes wrong while enabling SAML. - - - -### method `__init__` - -```python -__init__(msg: str) -``` - -Initialize a new instance of the SynapseWorkloadError exception. - - - -**Args:** - - - `msg` (str): Explanation of the error. - - - - - ---- - - - -## class `ExecResult` -A named tuple representing the result of executing a command. - - - -**Attributes:** - - - `exit_code`: The exit status of the command (0 for success, non-zero for failure). - - `stdout`: The standard output of the command as a string. - - `stderr`: The standard error output of the command as a string. - - - - - diff --git a/src/charm.py b/src/charm.py index a6b57a30..ed1a95ff 100755 --- a/src/charm.py +++ b/src/charm.py @@ -7,15 +7,16 @@ import logging import typing +from secrets import token_hex import ops from charms.nginx_ingress_integrator.v0.nginx_route import require_nginx_route from charms.traefik_k8s.v1.ingress import IngressPerAppRequirer from ops.charm import ActionEvent +from ops.jujuversion import JujuVersion from ops.main import main import actions -import secret_storage import synapse from charm_state import CharmConfigInvalidError, CharmState from database_observer import DatabaseObserver @@ -25,6 +26,12 @@ from saml_observer import SAMLObserver from user import User +JUJU_HAS_SECRETS = JujuVersion.from_environ().has_secrets +PEER_RELATION_NAME = "synapse-peers" +# Disabling it since these are not hardcoded password +SECRET_ID = "secret-id" # nosec +SECRET_KEY = "secret-key" # nosec + logger = logging.getLogger(__name__) @@ -179,6 +186,71 @@ def _on_register_user_action(self, event: ActionEvent) -> None: results = {"register-user": True, "user-password": user.password} event.set_results(results) + def _create_admin_user(self) -> typing.Optional[User]: + """Create admin user. + + Returns: + Admin user with token to be used in Synapse API requests or None if fails. + """ + container = self.unit.get_container(synapse.SYNAPSE_CONTAINER_NAME) + if not container.can_connect(): + logger.error("Failed to create admin user: waiting for pebble") + return None + # The username is random because if the user exists, register_user will try to get the + # access_token. + # But to do that it needs an admin user and we don't have one yet. + # So, to be on the safe side, the user name is randomly generated and if for any reason + # there is no access token on peer data/secret, another user will be created. + # + # Using 16 to create a random value but to be secure against brute-force attacks, + # please check the docs: + # https://docs.python.org/3/library/secrets.html#how-many-bytes-should-tokens-use + username = token_hex(16) + return actions.register_user(container, username, True) + + def get_admin_access_token(self) -> typing.Optional[str]: + """Get admin access token. + + Returns: + admin access token or None if fails. + """ + peer_relation = self.model.get_relation(PEER_RELATION_NAME) + if not peer_relation: + logger.error( + "Failed to get admin access token: no peer relation %s found", PEER_RELATION_NAME + ) + return None + admin_access_token = None + if JUJU_HAS_SECRETS: + secret_id = peer_relation.data[self.app].get(SECRET_ID) + if secret_id: + secret = self.model.get_secret(id=secret_id) + admin_access_token = secret.get_content().get(SECRET_KEY) + else: + # There is Secrets support but none was created + # So lets create the user and store its token in the secret + admin_user = self._create_admin_user() + if not admin_user: + return None + logger.debug("Adding secret") + secret = self.app.add_secret({SECRET_KEY: admin_user.access_token}) + peer_relation.data[self.app].update({SECRET_ID: secret.id}) + admin_access_token = admin_user.access_token + else: + # There is no Secrets support and none relation data was created + # So lets create the user and store its token in the peer relation + secret_value = peer_relation.data[self.app].get(SECRET_KEY) + if secret_value: + admin_access_token = secret_value + else: + admin_user = self._create_admin_user() + if not admin_user: + return None + logger.debug("Adding peer data") + peer_relation.data[self.app].update({SECRET_KEY: admin_user.access_token}) + admin_access_token = admin_user.access_token + return admin_access_token + def _on_promote_user_admin_action(self, event: ActionEvent) -> None: """Promote user admin and report action result. @@ -193,7 +265,10 @@ def _on_promote_user_admin_action(self, event: ActionEvent) -> None: event.fail("Failed to connect to container") return try: - admin_access_token = secret_storage.get_admin_access_token(self) + admin_access_token = self.get_admin_access_token() + if not admin_access_token: + event.fail("Failed to get admin access token") + return username = event.params["username"] server = self._charm_state.synapse_config.server_name user = User(username=username, admin=True) @@ -201,10 +276,7 @@ def _on_promote_user_admin_action(self, event: ActionEvent) -> None: user=user, server=server, admin_access_token=admin_access_token ) results["promote-user-admin"] = True - except ( - synapse.APIError, - secret_storage.AdminAccessTokenNotFoundError, - ) as exc: + except synapse.APIError as exc: event.fail(str(exc)) return event.set_results(results) diff --git a/src/mjolnir.py b/src/mjolnir.py index f86d15a6..d117a17d 100644 --- a/src/mjolnir.py +++ b/src/mjolnir.py @@ -3,13 +3,15 @@ """Provide the Mjolnir class to represent the Mjolnir plugin for Synapse.""" +# disabling due the fact that collect status does many checks +# pylint: disable=too-many-return-statements + import logging import typing import ops import actions -import secret_storage import synapse from charm_state import CharmState @@ -48,6 +50,19 @@ def _pebble_service(self) -> typing.Any: """ return getattr(self._charm, "pebble_service", None) + @property + def _admin_access_token(self) -> typing.Optional[str]: + """Get admin access token. + + Returns: + admin access token or None if fails. + """ + get_admin_access_token = getattr(self._charm, "get_admin_access_token", None) + if not get_admin_access_token: + logging.error("Failed to get method get_admin_access_token.") + return None + return get_admin_access_token() + def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: """Collect status event handler. @@ -74,8 +89,13 @@ def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: # the service status is checked here. self._charm.unit.status = ops.MaintenanceStatus("Waiting for Synapse") return + if not self._admin_access_token: + self._charm.unit.status = ops.MaintenanceStatus( + "Failed to get admin access token. Please, check the logs." + ) + return try: - if self.get_membership_room_id() is None: + if self.get_membership_room_id(self._admin_access_token) is None: status = ops.BlockedStatus( f"{synapse.MJOLNIR_MEMBERSHIP_ROOM} not found and " "is required by Mjolnir. Please, check the logs." @@ -89,34 +109,29 @@ def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: ) event.add_status(status) return - except (synapse.APIError, secret_storage.AdminAccessTokenNotFoundError) as exc: + except synapse.APIError as exc: logger.exception( "Failed to check for membership_room. Mjolnir will not be configured: %r", exc, ) return - self.enable_mjolnir() + self.enable_mjolnir(self._admin_access_token) event.add_status(ops.ActiveStatus()) - def get_membership_room_id(self) -> typing.Optional[str]: + def get_membership_room_id(self, admin_access_token: str) -> typing.Optional[str]: """Check if membership room exists. + Args: + admin_access_token: not empty admin access token. + Returns: The room id or None if is not found. - - Raises: - AdminAccessTokenNotFoundError: if there is not admin access token. """ - try: - admin_access_token = secret_storage.get_admin_access_token(self._charm) - return synapse.get_room_id( - room_name=synapse.MJOLNIR_MEMBERSHIP_ROOM, admin_access_token=admin_access_token - ) - except secret_storage.AdminAccessTokenNotFoundError: - logger.warning("Failed to get membership room id while getting admin access token.") - raise + return synapse.get_room_id( + room_name=synapse.MJOLNIR_MEMBERSHIP_ROOM, admin_access_token=admin_access_token + ) - def enable_mjolnir(self) -> None: + def enable_mjolnir(self, admin_access_token: str) -> None: """Enable mjolnir service. The required steps to enable Mjolnir are: @@ -130,12 +145,14 @@ def enable_mjolnir(self) -> None: - Create the Mjolnir configuration file. - Override Mjolnir user rate limit. - Finally, add Mjolnir pebble layer. + + Args: + admin_access_token: not empty admin access token. """ container = self._charm.unit.get_container(synapse.SYNAPSE_CONTAINER_NAME) if not container.can_connect(): self._charm.unit.status = ops.MaintenanceStatus("Waiting for pebble") return - admin_access_token = secret_storage.get_admin_access_token(self._charm) self._charm.model.unit.status = ops.MaintenanceStatus("Configuring Mjolnir") mjolnir_user = actions.register_user( container, diff --git a/src/secret_storage.py b/src/secret_storage.py deleted file mode 100644 index dd77ca19..00000000 --- a/src/secret_storage.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Helper module used to manage interactions with Synapse secrets.""" - -import logging -from secrets import token_hex - -import ops -from ops.jujuversion import JujuVersion - -import actions -import synapse - -JUJU_HAS_SECRETS = JujuVersion.from_environ().has_secrets -PEER_RELATION_NAME = "synapse-peers" -# Disabling it since these are not hardcoded password -SECRET_ID = "secret-id" # nosec -SECRET_KEY = "secret-key" # nosec - -logger = logging.getLogger(__name__) - - -class AdminAccessTokenNotFoundError(Exception): - """Exception raised when there is not admin access token. - - Attrs: - msg (str): Explanation of the error. - """ - - def __init__(self, msg: str): - """Initialize a new instance of the AdminAccessTokenNotFoundError exception. - - Args: - msg (str): Explanation of the error. - """ - self.msg = msg - - -def _update_peer_data(charm: ops.CharmBase, container: ops.model.Container) -> None: - """Update peer data if needed. - - The admin access token is stored in a Secret (Juju 3) or in a peer relation - data. If already exists, no action is taken. Otherwise, will create an admin - user and store the token. - - Args: - charm: The charm object. - container: Synapse container. - """ - # If there is no secret, we use peer relation data - # If there is secret, then we update the secret and add the secret id to peer data - peer_relation = charm.model.get_relation(PEER_RELATION_NAME) - if not peer_relation: - # there is no peer relation so nothing to be done - return - # The username is random because if the user exists, register_user will try to get the - # access_token. - # But to do that it needs an admin user and we don't have one yet. - # So, to be on the safe side, the user name is randomly generated and if for any reason - # there is no access token on peer data/secret, another user will be created. - # - # Using 16 to create a random value but to be secure against brute-force attacks, - # please check the docs: - # https://docs.python.org/3/library/secrets.html#how-many-bytes-should-tokens-use - username = token_hex(16) - if JUJU_HAS_SECRETS and not peer_relation.data[charm.app].get(SECRET_ID): - # we can create secrets and the one that we need was not created yet - logger.debug("Adding secret") - admin_user = actions.register_user(container, username, True) - secret = charm.app.add_secret({SECRET_KEY: admin_user.access_token}) - peer_relation.data[charm.app].update({SECRET_ID: secret.id}) - return - - if not JUJU_HAS_SECRETS and not peer_relation.data[charm.app].get(SECRET_KEY): - # we can't create secrets and peer data is empty - logger.debug("Updating peer relation data") - admin_user = actions.register_user(container, username, True) - peer_relation.data[charm.app].update({SECRET_KEY: admin_user.access_token}) - - -def get_admin_access_token(charm: ops.CharmBase) -> str: - """Get admin access token. - - Args: - charm: The charm object. - - Returns: - admin access token. - - Raises: - AdminAccessTokenNotFoundError: if admin access token is not found. - """ - container = charm.unit.get_container(synapse.SYNAPSE_CONTAINER_NAME) - if not container.can_connect(): - raise AdminAccessTokenNotFoundError("Container not ready to connect") - peer_relation = charm.model.get_relation(PEER_RELATION_NAME) - if not peer_relation: - raise AdminAccessTokenNotFoundError("No peer relation found") - _update_peer_data(charm, container) - if JUJU_HAS_SECRETS: - secret_id = peer_relation.data[charm.app].get(SECRET_ID) - if not secret_id: - raise AdminAccessTokenNotFoundError(f"No secret id {SECRET_ID} found") - secret = charm.model.get_secret(id=secret_id) - secret_value = secret.get_content().get(SECRET_KEY) - else: - secret_value = peer_relation.data[charm.app].get(SECRET_KEY) - if not secret_value: - raise AdminAccessTokenNotFoundError(f"No secret key {SECRET_KEY} found") - return secret_value diff --git a/src/synapse/workload.py b/src/synapse/workload.py index 81f14b56..5ee4f0c2 100644 --- a/src/synapse/workload.py +++ b/src/synapse/workload.py @@ -12,9 +12,10 @@ import yaml from ops.pebble import Check, ExecError, PathError -import synapse from charm_state import CharmState +from .api import SYNAPSE_PORT, SYNAPSE_URL, VERSION_URL + CHECK_ALIVE_NAME = "synapse-alive" CHECK_MJOLNIR_READY_NAME = "synapse-mjolnir-ready" CHECK_NGINX_READY_NAME = "synapse-nginx-ready" @@ -94,7 +95,7 @@ def check_ready() -> ops.pebble.CheckDict: check = Check(CHECK_READY_NAME) check.override = "replace" check.level = "ready" - check.http = {"url": synapse.VERSION_URL} + check.http = {"url": VERSION_URL} return check.to_dict() @@ -107,7 +108,7 @@ def check_alive() -> ops.pebble.CheckDict: check = Check(CHECK_ALIVE_NAME) check.override = "replace" check.level = "alive" - check.tcp = {"port": synapse.SYNAPSE_PORT} + check.tcp = {"port": SYNAPSE_PORT} return check.to_dict() @@ -318,8 +319,8 @@ def _get_mjolnir_config(access_token: str, room_id: str) -> typing.Dict: """ with open("templates/mjolnir_production.yaml", encoding="utf-8") as mjolnir_config_file: config = yaml.safe_load(mjolnir_config_file) - config["homeserverUrl"] = synapse.SYNAPSE_URL - config["rawHomeserverUrl"] = synapse.SYNAPSE_URL + config["homeserverUrl"] = SYNAPSE_URL + config["rawHomeserverUrl"] = SYNAPSE_URL config["accessToken"] = access_token config["managementRoom"] = room_id return config diff --git a/tests/unit/test_mjolnir.py b/tests/unit/test_mjolnir.py index 73d37655..2da8fce6 100644 --- a/tests/unit/test_mjolnir.py +++ b/tests/unit/test_mjolnir.py @@ -6,84 +6,15 @@ # pylint: disable=protected-access from secrets import token_hex -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import ANY, MagicMock import ops import pytest from ops.testing import Harness import actions -import secret_storage import synapse -from charm import SynapseCharm from mjolnir import Mjolnir -from user import User - - -@patch.object(ops.JujuVersion, "from_environ") -def test_update_peer_data_no_secrets( - mock_juju_env, harness: Harness, monkeypatch: pytest.MonkeyPatch -) -> None: - """ - arrange: start the Synapse charm, set server_name, mock container and create_admin_user. - act: call _update_peer_data. - assert: relation data is updated with access token. - """ - mock_juju_env.return_value = MagicMock(has_secrets=False) - harness.set_leader(True) - harness.update_config({"enable_mjolnir": True}) - harness.begin_with_initial_hooks() - container_mock = MagicMock() - username = "any-user" - user = User(username=username, admin=True) - user.access_token = token_hex(16) - create_admin_user_mock = MagicMock(return_value=user) - monkeypatch.setattr(secret_storage.actions, "register_user", create_admin_user_mock) - - secret_storage._update_peer_data(harness.charm, container_mock) - - create_admin_user_mock.assert_called_once() - peer_relation = harness.model.get_relation("synapse-peers") - assert peer_relation - assert ( - harness.get_relation_data(peer_relation.id, harness.charm.app.name).get("secret-key") - == user.access_token - ) - - -def test_update_peer_data_with_secrets(harness: Harness, monkeypatch: pytest.MonkeyPatch) -> None: - """ - arrange: start the Synapse charm, set server_name, mock container and create_admin_user. - act: call _update_peer_data. - assert: secret with access token. - """ - harness = Harness(SynapseCharm) - harness.update_config({"enable_mjolnir": True}) - harness.begin_with_initial_hooks() - secret_storage.JUJU_HAS_SECRETS = True - harness.set_leader(True) - container_mock = MagicMock() - username = "any-user" - user = User(username=username, admin=True) - user.access_token = token_hex(16) - create_admin_user_mock = MagicMock(return_value=user) - monkeypatch.setattr(secret_storage.actions, "register_user", create_admin_user_mock) - secret_mock = MagicMock - secret_id = token_hex(16) - secret_mock.id = secret_id - add_secret_mock = MagicMock(return_value=secret_mock) - monkeypatch.setattr(harness.charm.app, "add_secret", add_secret_mock) - - secret_storage._update_peer_data(harness.charm, container_mock) - - create_admin_user_mock.assert_called_once() - add_secret_mock.assert_called_once_with({"secret-key": user.access_token}) - peer_relation = harness.model.get_relation("synapse-peers") - assert peer_relation - assert ( - harness.get_relation_data(peer_relation.id, harness.charm.app.name).get("secret-id") - == secret_id - ) def test_get_membership_room_id(harness: Harness, monkeypatch: pytest.MonkeyPatch) -> None: @@ -98,9 +29,7 @@ def test_get_membership_room_id(harness: Harness, monkeypatch: pytest.MonkeyPatc admin_access_token = token_hex(16) get_room_id = MagicMock() monkeypatch.setattr(synapse, "get_room_id", get_room_id) - monkeypatch.setattr( - secret_storage, "get_admin_access_token", MagicMock(return_value=admin_access_token) - ) + monkeypatch.setattr(Mjolnir, "_admin_access_token", admin_access_token) harness.charm._mjolnir.get_membership_room_id() @@ -211,9 +140,7 @@ def test_on_collect_status_active(harness: Harness, monkeypatch: pytest.MonkeyPa harness.begin_with_initial_hooks() harness.set_leader(True) admin_access_token = token_hex(16) - monkeypatch.setattr( - secret_storage, "get_admin_access_token", MagicMock(return_value=admin_access_token) - ) + monkeypatch.setattr(Mjolnir, "_admin_access_token", admin_access_token) membership_room_id_mock = MagicMock(return_value="123") monkeypatch.setattr(Mjolnir, "get_membership_room_id", membership_room_id_mock) enable_mjolnir_mock = MagicMock(return_value=None) @@ -241,9 +168,7 @@ def test_on_collect_status_api_error(harness: Harness, monkeypatch: pytest.Monke harness.begin_with_initial_hooks() harness.set_leader(True) admin_access_token = token_hex(16) - monkeypatch.setattr( - secret_storage, "get_admin_access_token", MagicMock(return_value=admin_access_token) - ) + monkeypatch.setattr(Mjolnir, "_admin_access_token", admin_access_token) membership_room_id_mock = MagicMock(side_effect=synapse.APIError("error")) monkeypatch.setattr(Mjolnir, "get_membership_room_id", membership_room_id_mock) enable_mjolnir_mock = MagicMock(return_value=None) @@ -259,52 +184,6 @@ def test_on_collect_status_api_error(harness: Harness, monkeypatch: pytest.Monke enable_mjolnir_mock.assert_not_called() -def test_get_admin_access_token_no_secrets(harness: Harness) -> None: - """ - arrange: start the Synapse charm, set server_name, no secrets. - act: update relation data with secret key. - assert: token is returned as expected. - """ - harness.update_config({"enable_mjolnir": True}) - harness.begin_with_initial_hooks() - secret_storage.JUJU_HAS_SECRETS = False - peer_relation = harness.model.get_relation("synapse-peers") - assert peer_relation - - expected_token = token_hex(16) - harness.update_relation_data(peer_relation.id, "synapse", {"secret-key": expected_token}) - - assert secret_storage.get_admin_access_token(harness.charm) == expected_token - - -def test_get_admin_access_token_with_secrets( - harness: Harness, monkeypatch: pytest.MonkeyPatch -) -> None: - """ - arrange: start the Synapse charm, set server_name, with secrets. - act: update relation data with secret key. - assert: token is returned as expected. - """ - harness.update_config({"enable_mjolnir": True}) - harness.begin_with_initial_hooks() - secret_storage.JUJU_HAS_SECRETS = True - peer_relation = harness.model.get_relation("synapse-peers") - assert peer_relation - expected_id = token_hex(16) - harness.update_relation_data(peer_relation.id, "synapse", {"secret-id": expected_id}) - secret_mock = MagicMock - expected_token = token_hex(16) - expected_content = {"secret-key": expected_token} - secret_mock.get_content = MagicMock(return_value=expected_content) - get_secret_mock = MagicMock(return_value=secret_mock) - monkeypatch.setattr(harness.charm.model, "get_secret", get_secret_mock) - - token = secret_storage.get_admin_access_token(harness.charm) - - get_secret_mock.assert_called_once() - assert token == expected_token - - def test_enable_mjolnir(harness: Harness, monkeypatch: pytest.MonkeyPatch) -> None: """ arrange: start the Synapse charm, set server_name, mock calls to validate args. @@ -315,9 +194,7 @@ def test_enable_mjolnir(harness: Harness, monkeypatch: pytest.MonkeyPatch) -> No harness.begin_with_initial_hooks() harness.set_leader(True) admin_access_token = token_hex(16) - monkeypatch.setattr( - secret_storage, "get_admin_access_token", MagicMock(return_value=admin_access_token) - ) + monkeypatch.setattr(Mjolnir, "_admin_access_token", admin_access_token) mjolnir_user_mock = MagicMock() mjolnir_access_token = token_hex(16) mjolnir_user_mock.access_token = mjolnir_access_token @@ -362,9 +239,7 @@ def test_enable_mjolnir_room_none(harness: Harness, monkeypatch: pytest.MonkeyPa harness.begin_with_initial_hooks() harness.set_leader(True) admin_access_token = token_hex(16) - monkeypatch.setattr( - secret_storage, "get_admin_access_token", MagicMock(return_value=admin_access_token) - ) + monkeypatch.setattr(Mjolnir, "_admin_access_token", admin_access_token) mjolnir_user_mock = MagicMock() mjolnir_access_token = token_hex(16) mjolnir_user_mock.access_token = mjolnir_access_token diff --git a/tests/unit/test_promote_user_admin_action.py b/tests/unit/test_promote_user_admin_action.py index b126f7aa..11253259 100644 --- a/tests/unit/test_promote_user_admin_action.py +++ b/tests/unit/test_promote_user_admin_action.py @@ -24,8 +24,6 @@ def test_promote_user_admin_action(harness: Harness, monkeypatch: pytest.MonkeyP """ harness.begin_with_initial_hooks() admin_access_token = token_hex(16) - get_admin_access_token_mock = unittest.mock.Mock(return_value=admin_access_token) - monkeypatch.setattr("secret_storage.get_admin_access_token", get_admin_access_token_mock) promote_user_admin_mock = unittest.mock.Mock() monkeypatch.setattr("synapse.promote_user_admin", promote_user_admin_mock) user = "username" @@ -38,7 +36,6 @@ def test_promote_user_admin_action(harness: Harness, monkeypatch: pytest.MonkeyP assert event.set_results.call_count == 1 event.set_results.assert_called_with({"promote-user-admin": True}) - get_admin_access_token_mock.assert_called_once() promote_user_admin_mock.assert_called_with( user=unittest.mock.ANY, server=unittest.mock.ANY, admin_access_token=admin_access_token ) @@ -51,14 +48,11 @@ def test_promote_user_admin_api_error(harness: Harness, monkeypatch: pytest.Monk assert: event fails as expected. """ harness.begin_with_initial_hooks() - admin_access_token = token_hex(16) - get_admin_access_token_mock = unittest.mock.Mock(return_value=admin_access_token) - monkeypatch.setattr("secret_storage.get_admin_access_token", get_admin_access_token_mock) fail_message = "Some fail message" synapse_api_error = synapse.APIError(fail_message) promote_user_admin_mock = unittest.mock.MagicMock(side_effect=synapse_api_error) monkeypatch.setattr("synapse.promote_user_admin", promote_user_admin_mock) - admin_access_token = token_hex(16) + # admin_access_token = token_hex(16) user = "username" event = unittest.mock.MagicMock(spec=ActionEvent) event.params = {