Skip to content

Commit

Permalink
feat: add register-client-account action (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
amandahla authored Nov 7, 2024
1 parent 321465d commit 46f0ef0
Show file tree
Hide file tree
Showing 9 changed files with 508 additions and 20 deletions.
19 changes: 19 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,22 @@ actions:
name:
type: string
description: The name of the administrator user.
register-client-account:
description: Register Matrix client account for a bot. The result is user ID,
password, access token and device ID that should be used for registering a
client.

See Maubot documentation for more details.
https://docs.mau.fi/maubot/usage/basic.html#creating-clients
params:
admin-name:
type: string
description: The name of the administrator user that will be used for
creating the account.
admin-password:
type: string
description: The password of the administrator user that will be used
for creating the account.
account-name:
type: string
description: The Matrix account you want to use as a bot.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
ops==2.17.0
requests==2.32.3
cosl==0.0.42
5 changes: 3 additions & 2 deletions src-docs/charm.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Maubot charm service.
**Global Variables**
---------------
- **BLACKBOX_NAME**
- **MATRIX_AUTH_HOMESERVER**
- **MAUBOT_CONFIGURATION_PATH**
- **MAUBOT_NAME**
- **NGINX_NAME**
Expand All @@ -27,7 +28,7 @@ Exception raised when an event fails.
## <kbd>class</kbd> `MaubotCharm`
Maubot charm.

<a href="../lib/charms/loki_k8s/v0/charm_logging.py#L70"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../lib/charms/loki_k8s/v0/charm_logging.py#L73"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand Down Expand Up @@ -88,7 +89,7 @@ Unit that this execution is responsible for.
## <kbd>class</kbd> `MissingRelationDataError`
Custom exception to be raised in case of malformed/missing relation data.

<a href="../src/charm.py#L51"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm.py#L54"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand Down
107 changes: 107 additions & 0 deletions src-docs/maubot.py.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<!-- markdownlint-disable -->

<a href="../src/maubot.py#L0"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

# <kbd>module</kbd> `maubot.py`
Maubot service.

**Global Variables**
---------------
- **MAUBOT_ROOT_URL**

---

<a href="../src/maubot.py#L33"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

## <kbd>function</kbd> `login`

```python
login(admin_name: str, admin_password: str) → str
```

Login in Maubot and returns a token.



**Args:**

- <b>`admin_name`</b>: admin name that will do the login.
- <b>`admin_password`</b>: admin password.



**Raises:**

- <b>`APIError`</b>: error while interacting with Maubot API.



**Returns:**
token to be used in further requests.


---

<a href="../src/maubot.py#L60"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

## <kbd>function</kbd> `register_account`

```python
register_account(
token: str,
account_name: str,
account_password: str,
matrix_server: str
) → str
```

Register account.



**Args:**

- <b>`token`</b>: valid token for authentication.
- <b>`account_name`</b>: account name to be registered.
- <b>`account_password`</b>: account password to be registered.
- <b>`matrix_server`</b>: Matrix server where the account will be registered.



**Raises:**

- <b>`APIError`</b>: error while interacting with Maubot API.



**Returns:**
Account access information.


---

## <kbd>class</kbd> `APIError`
Exception raised when something fails while interacting with Maubot API.

Attrs: msg (str): Explanation of the error.

<a href="../src/maubot.py#L24"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

```python
__init__(msg: str)
```

Initialize a new instance of the MaubotError exception.



**Args:**

- <b>`msg`</b> (str): Explanation of the error.





90 changes: 76 additions & 14 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@
)
from ops import pebble

import maubot

logger = logging.getLogger(__name__)

BLACKBOX_NAME = "blackbox"
MATRIX_AUTH_HOMESERVER = "synapse"
MAUBOT_CONFIGURATION_PATH = "/data/config.yaml"
MAUBOT_NAME = "maubot"
NGINX_NAME = "nginx"
Expand Down Expand Up @@ -95,6 +98,9 @@ def __init__(self, *args: Any):
self.framework.observe(self.on.config_changed, self._on_config_changed)
# Actions events handlers
self.framework.observe(self.on.create_admin_action, self._on_create_admin_action)
self.framework.observe(
self.on.register_client_account_action, self._on_register_client_account_action
)
# Integrations events handlers
self.framework.observe(self.postgresql.on.database_created, self._on_database_created)
self.framework.observe(self.postgresql.on.endpoints_changed, self._on_endpoints_changed)
Expand Down Expand Up @@ -174,6 +180,14 @@ def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None:
self._reconcile()

# Integrations events handlers
def _on_database_created(self, _: DatabaseCreatedEvent) -> None:
"""Handle database created event."""
self._reconcile()

def _on_endpoints_changed(self, _: DatabaseEndpointsChangedEvent) -> None:
"""Handle endpoints changed event."""
self._reconcile()

def _on_ingress_ready(self, _: IngressPerAppReadyEvent) -> None:
"""Handle ingress ready event."""
self._reconcile()
Expand All @@ -194,14 +208,10 @@ def _on_create_admin_action(self, event: ops.ActionEvent) -> None:
"""
try:
name = event.params["name"]
results = {"password": "", "error": ""}
results: dict[str, str] = {}
if name == "root":
raise EventFailError("root is reserved, please choose a different name")
if (
not self.container.can_connect()
or MAUBOT_NAME not in self.container.get_plan().services
or not self.container.get_service(MAUBOT_NAME).is_running()
):
if self._is_maubot_ready():
raise EventFailError("maubot is not ready")
password = secrets.token_urlsafe(10)
config = self._get_configuration()
Expand All @@ -217,14 +227,66 @@ def _on_create_admin_action(self, event: ops.ActionEvent) -> None:
event.set_results(results)
event.fail(str(e))

# Integrations events handlers
def _on_database_created(self, _: DatabaseCreatedEvent) -> None:
"""Handle database created event."""
self._reconcile()
def _on_register_client_account_action(self, event: ops.ActionEvent) -> None:
"""Handle register-client-account action.
def _on_endpoints_changed(self, _: DatabaseEndpointsChangedEvent) -> None:
"""Handle endpoints changed event."""
self._reconcile()
Matrix-auth integration required.
Args:
event: Action event.
Raises:
EventFailError: in case the event fails.
"""
try:
results: dict[str, str] = {}
if self._is_maubot_ready():
raise EventFailError("maubot is not ready")

config = self._get_configuration()
if MATRIX_AUTH_HOMESERVER not in config["homeservers"]:
raise EventFailError("matrix-auth integration is required")

admin_name = event.params["admin-name"]
admin_password = event.params["admin-password"]
if admin_name not in config["admins"]:
raise EventFailError(f"{admin_name} not found in admin users")

# Login in Maubot
token = maubot.login(admin_name=admin_name, admin_password=admin_password)

# Register Matrix Account
account_name = event.params["account-name"]
account_password = secrets.token_urlsafe(10)
account_data = maubot.register_account(
token=token,
account_name=account_name,
account_password=account_password,
matrix_server=MATRIX_AUTH_HOMESERVER,
)

# Set results
results["user-id"] = account_data.get("user_id")
results["password"] = account_password
results["access-token"] = account_data.get("access_token")
results["device-id"] = account_data.get("device_id")
event.set_results(results)
except (maubot.APIError, EventFailError) as e:
results["error"] = str(e)
event.set_results(results)
event.fail(str(e))

def _is_maubot_ready(self) -> bool:
"""Check if Maubot container is ready.
Returns:
True if Maubot is ready.
"""
return (
not self.container.can_connect()
or MAUBOT_NAME not in self.container.get_plan().services
or not self.container.get_service(MAUBOT_NAME).is_running()
)

def _on_matrix_auth_request_processed(self, _: MatrixAuthRequestProcessed) -> None:
"""Handle matrix auth request processed event."""
Expand Down Expand Up @@ -284,7 +346,7 @@ def _get_matrix_credentials(self) -> dict[str, dict[str, str]]:
raise MissingRelationDataError(
"Missing mandatory relation data", relation_name="matrix-auth"
)
return {"synapse": {"url": homeserver, "secret": shared_secret_id}}
return {MATRIX_AUTH_HOMESERVER: {"url": homeserver, "secret": shared_secret_id}}

# Properties
@property
Expand Down
86 changes: 86 additions & 0 deletions src/maubot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3

# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""Maubot service."""

import logging

import requests

MAUBOT_ROOT_URL = "http://localhost:29316/_matrix/maubot"

logger = logging.getLogger(__name__)


class APIError(Exception):
"""Exception raised when something fails while interacting with Maubot API.
Attrs:
msg (str): Explanation of the error.
"""

def __init__(self, msg: str):
"""Initialize a new instance of the MaubotError exception.
Args:
msg (str): Explanation of the error.
"""
self.msg = msg


def login(admin_name: str, admin_password: str) -> str:
"""Login in Maubot and returns a token.
Args:
admin_name: admin name that will do the login.
admin_password: admin password.
Raises:
APIError: error while interacting with Maubot API.
Returns:
token to be used in further requests.
"""
url = f"{MAUBOT_ROOT_URL}/v1/auth/login"
payload = {"username": admin_name, "password": admin_password}
try:
response = requests.post(url, json=payload, timeout=5)
response.raise_for_status()
token = response.json().get("token")
if not token:
raise APIError("token not found in Maubot API response")
return token
except (requests.exceptions.RequestException, TimeoutError) as e:
logger.exception("failed to request Maubot API: %s", str(e))
raise APIError("error while interacting with Maubot API") from e


def register_account(
token: str, account_name: str, account_password: str, matrix_server: str
) -> str:
"""Register account.
Args:
token: valid token for authentication.
account_name: account name to be registered.
account_password: account password to be registered.
matrix_server: Matrix server where the account will be registered.
Raises:
APIError: error while interacting with Maubot API.
Returns:
Account access information.
"""
url = f"{MAUBOT_ROOT_URL}/v1/client/auth/{matrix_server}/register"
try:
payload = {"username": account_name, "password": account_password}
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(url, json=payload, headers=headers, timeout=5)
response.raise_for_status()
return response.json()
except (requests.exceptions.RequestException, TimeoutError) as e:
logger.exception("failed to request Maubot API: %s", str(e))
raise APIError("error while interacting with Maubot API") from e
Loading

0 comments on commit 46f0ef0

Please sign in to comment.