From 2517fe72db72f243f255b0954d575f31f4f08357 Mon Sep 17 00:00:00 2001 From: transcaffeine Date: Fri, 20 Sep 2024 19:25:04 +0200 Subject: [PATCH] feat(synapse_signing_key): add ansible module for managing signing keys --- README.md | 4 + plugins/modules/synapse_signing_key.md | 33 ++++++ plugins/modules/synapse_signing_key.py | 139 +++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 plugins/modules/synapse_signing_key.md create mode 100644 plugins/modules/synapse_signing_key.py diff --git a/README.md b/README.md index a1430fd..358d025 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Roles for deploying matrix infrastructure using ansible. +## Modules + +- [`synapse_signing_key`](plugins/modules/synapse_signing_key.md): Module for managing / generating synapse signing keys + ## Roles - [`cinny`](roles/cinny/README.md): [Cinny](https://cinny.in/) Web Client diff --git a/plugins/modules/synapse_signing_key.md b/plugins/modules/synapse_signing_key.md new file mode 100644 index 0000000..6493bdf --- /dev/null +++ b/plugins/modules/synapse_signing_key.md @@ -0,0 +1,33 @@ +# `finallycoffee.matrix.synapse_signing_key` module + +Module to generate and manage synapse signing keys. + +> [!TIP] +> Supports `check mode` and `diff` rendering + +## Requirements + +- `python >= 3.9` +- `signed_json >= 1.1.4` + + +## Usage examples + +```yaml +# Generate a key +- finallycoffee.matrix.synapse_signing_key: + path: "/not/there/yet/signing.key" + state: present + +# Read a key from the filesystem or generate one +- finallycoffee.matrix.synapse_signing_key: + path: "/maybe/existing/signing/key" + register: key_result +- debug: + msg: "Signing key is '{{ key_result.signing_key }}'" + +# Delete an existing signing key file +- finallycoffee.matrix.synapse_signing_key: + path: "/path/to/key/to/delete.key" + state: absent +``` diff --git a/plugins/modules/synapse_signing_key.py b/plugins/modules/synapse_signing_key.py new file mode 100644 index 0000000..a9b4821 --- /dev/null +++ b/plugins/modules/synapse_signing_key.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python + +__metaclass__ = type + +import os +import traceback +import random, string +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +from dataclasses import asdict + +LIB_IMPORT_ERR = None +try: + from signedjson.key import * + HAS_LIB = True +except: + HAS_LIB = False + LIB_IMPORT_ERR = traceback.format_exc() + +DOCUMENTION = r""" +--- +module: synapse_signing_key +author: + - transcaffeine (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 + - signed_json >= 1.1.4 +short_description: Generate and persist a synapse signing key +description: + - "Allows to generate, save and manipulate synapse signing keys" +options: + path: + description: "Where the signing key is saved or read from" + type: str + required: false + state: + description: "State of the key to enforce, can be used to remove keys or generate them" + type: str + choices: [present, absent] + default: present + required: false +""" + +EXAMPLES = r""" +- name: Read existing signing key from filesystem + finallycoffee.matrix.synapse_signing_key: + path: "/path/to/existing-synapse.signing.key" +- name: Delete existing signing key + finallycoffee.matrix.synapse.signing_key: + path: "/this/signing/key/will/be/deleted.signing.key" + state: absent +""" + +RETURN = r""" +signing_key: + description: Complete synapse signing key + returned: always + type: str +""" + +def main() -> None: + _ = dict + module = AnsibleModule( + argument_spec=_( + path=_(required=False, type="str"), + state=_( + required=False, + type="str", + default="present", + choices=["present", "absent"], + ), + ), + supports_check_mode=True, + ) + + result = _(changed=False, diff={}, msg="") + failed = False + + if not HAS_LIB: + module.fail_json(msg=missing_required_lib("signed_json"), exception=LIB_IMPORT_ERR) + + path = module.params['path'] + state = module.params['state'] + existing_key_found = False + if path: + try: + keys = _read_signing_keys(module.params['path']) + existing_key_found = True + except FileNotFoundError: + existing_key_found = False + if not existing_key_found: + keys = _generate_signing_key() + result['changed'] = True + + if not module.check_mode: + if state == 'present' and not existing_key_found and path: + _write_signing_keys(path, keys) + if state == 'absent' and existing_key_found: + os.remove(path) + result['changed'] = True + + result['diff'] = { + "before_header": path, + "after_header": path, + "before": _render_signing_keys(keys) if existing_key_found else "", + "after": "" if state == 'absent' else _render_signing_keys(keys), + } + result['signing_key'] = _render_signing_keys(keys) + + if failed: + module.fail_json(**result) + else: + module.exit_json(**result) + +def _render_signing_keys(keys) -> str: + return "\n".join(_render_signing_key(k) for k in keys) + +def _render_signing_key(key) -> str: + key_b64 = encode_signing_key_base64(key) + return f"{key.alg} {key.version} {key_b64}" + +def _read_signing_keys(file): + with open(file, "r", opener=lambda path, f: os.open(path, f)) as stream: + return read_signing_keys(stream) + +def _write_signing_keys(file, keys) -> None: + with open(file, "w", opener=lambda path, f: os.open(path, f, mode=0o640)) as stream: + write_signing_keys(stream, keys) + +def _generate_signing_key(): + id = '' + for i in range(0, 4): + id += random.choice(string.ascii_letters) + key_id = "a_" + id + key = generate_signing_key(key_id) + return [key] + +if __name__ == "__main__": + main()