diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index dbe3c9cfe19..6c7e48ec095 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1341,6 +1341,10 @@ files: maintainers: precurse $modules/sysrc.py: maintainers: dlundgren + $modules/systemd_creds_decrypt.py: + maintainers: konstruktoid + $modules/systemd_creds_encrypt.py: + maintainers: konstruktoid $modules/sysupgrade.py: maintainers: precurse $modules/taiga_issue.py: diff --git a/plugins/modules/systemd_creds_decrypt.py b/plugins/modules/systemd_creds_decrypt.py new file mode 100644 index 00000000000..a6c7126b2ef --- /dev/null +++ b/plugins/modules/systemd_creds_decrypt.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r""" +module: systemd_creds_decrypt +short_description: C(systemd)'s C(systemd-creds decrypt) plugin +description: + - This module decrypts input using C(systemd)'s C(systemd-creds decrypt). +author: + - Thomas Sjögren (@konstruktoid) +version_added: '10.2.0' +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. +options: + name: + description: + - The credential name to validate the embedded credential name. + type: str + required: false + newline: + description: + - Whether to add a trailing newline character to the end of the output, + if not present. + type: bool + required: false + default: false + secret: + description: + - The secret to decrypt. + type: str + required: true + timestamp: + description: + - The timestamp to use to validate the V(not-after) timestamp that + was used during encryption. + - Takes a timestamp specification in the format described in + V(systemd.time(7\)). + type: str + required: false + transcode: + description: + - Whether to transcode the output before returning it. + type: str + choices: [ base64, unbase64, hex, unhex ] + required: false + user: + description: + - A user name or numeric UID when decrypting from a specific user context. + - If set to the special string V(self) it sets the user to the user + of the calling process. + - Requires C(systemd) 256 or later. + type: str + required: false +notes: + - C(systemd-creds) requires C(systemd) 250 or later. +""" + +EXAMPLES = """ +- name: Decrypt secret + community.general.systemd_creds_decrypt: + name: db + secret: "WhQZht+JQJax1aZemmGLxmAAAA..." + register: decrypted_secret + +- name: Print the decrypted secret + ansible.builtin.debug: + msg: "{{ decrypted_secret }}" +""" + +RETURN = r""" +value: + description: + - The decrypted secret. + - Note that Ansible only supports returning UTF-8 encoded strings. + If the decrypted secret is binary data, or a string encoded in another + way, use O(transcode=base64) or O(transcode=hex) to circument this + restriction. You then need to decode the data when using it, for + example using the P(ansible.builtin.b64decode#filter) filter. + type: str + returned: always + sample: "access_token" +""" + + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """Decrypt secret using systemd-creds.""" + module = AnsibleModule( + argument_spec=dict( + name=dict(type="str", required=False), + newline=dict(type="bool", required=False, default=False), + secret=dict(type="str", required=True, no_log=True), + timestamp=dict(type="str", required=False), + transcode=dict( + type="str", + choices=["base64", "unbase64", "hex", "unhex"], + required=False, + ), + user=dict(type="str", required=False), + ), + supports_check_mode=True, + ) + + cmd = module.get_bin_path("systemd-creds", required=True) + + name = module.params["name"] + newline = module.params["newline"] + secret = module.params["secret"] + timestamp = module.params["timestamp"] + transcode = module.params["transcode"] + user = module.params["user"] + + decrypt_cmd = [cmd, "decrypt"] + if name: + decrypt_cmd.append("--name=" + name) + else: + decrypt_cmd.append("--name=") + decrypt_cmd.append("--newline=" + ("yes" if newline else "no")) + if timestamp: + decrypt_cmd.append("--timestamp=" + timestamp) + if transcode: + decrypt_cmd.append("--transcode=" + transcode) + if user: + decrypt_cmd.append("--uid=" + user) + decrypt_cmd.extend(["-", "-"]) + + rc, stdout, stderr = module.run_command(decrypt_cmd, data=secret, binary_data=True) + + module.exit_json( + changed=False, + value=stdout, + rc=rc, + stderr=stderr, + ) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/systemd_creds_encrypt.py b/plugins/modules/systemd_creds_encrypt.py new file mode 100644 index 00000000000..07b68f96f27 --- /dev/null +++ b/plugins/modules/systemd_creds_encrypt.py @@ -0,0 +1,149 @@ +#!/usr/bin/python + +# Copyright (c) 2024, Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = r""" +module: systemd_creds_encrypt +short_description: C(systemd)'s C(systemd-creds encrypt) plugin +description: + - This module encrypts input using C(systemd)'s C(systemd-creds encrypt). +author: + - Thomas Sjögren (@konstruktoid) +version_added: '10.2.0' +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. +options: + name: + description: + - The credential name to embed in the encrypted credential data. + type: str + required: false + not_after: + description: + - The time when the credential shall not be used anymore. + - Takes a timestamp specification in the format described in + V(systemd.time(7\)). + type: str + required: false + pretty: + description: + - Pretty print the output so that it may be pasted directly into a + unit file. + type: bool + required: false + default: false + secret: + description: + - The secret to encrypt. + type: str + required: true + timestamp: + description: + - The timestamp to embed into the encrypted credential. + - Takes a timestamp specification in the format described in + V(systemd.time(7\)). + type: str + required: false + user: + description: + - A user name or numeric UID to encrypt the credential for. + - If set to the special string V(self) it sets the user to the user + of the calling process. + - Requires C(systemd) 256 or later. + type: str + required: false +notes: + - C(systemd-creds) requires C(systemd) 250 or later. +""" + +EXAMPLES = """ +- name: Encrypt secret + become: true + community.general.systemd_creds_encrypt: + name: db + not_after: +48hr + secret: access_token + register: encrypted_secret + +- name: Print the encrypted secret + ansible.builtin.debug: + msg: "{{ encrypted_secret }}" +""" + +RETURN = r""" +value: + description: The Base64 encoded encrypted secret. + type: str + returned: always + sample: "WhQZht+JQJax1aZemmGLxmAAAA..." +""" + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """Encrypt secret using systemd-creds.""" + module = AnsibleModule( + argument_spec=dict( + name=dict(type="str", required=False), + not_after=dict(type="str", required=False), + pretty=dict(type="bool", default=False), + secret=dict(type="str", required=True, no_log=True), + timestamp=dict(type="str", required=False), + user=dict(type="str", required=False), + ), + supports_check_mode=True, + ) + + cmd = module.get_bin_path("systemd-creds", required=True) + + name = module.params["name"] + not_after = module.params["not_after"] + pretty = module.params["pretty"] + secret = module.params["secret"] + timestamp = module.params["timestamp"] + user = module.params["user"] + + encrypt_cmd = [cmd, "encrypt"] + if name: + encrypt_cmd.append("--name=" + name) + else: + encrypt_cmd.append("--name=") + if not_after: + encrypt_cmd.append("--not-after=" + not_after) + if pretty: + encrypt_cmd.append("--pretty") + if timestamp: + encrypt_cmd.append("--timestamp=" + timestamp) + if user: + encrypt_cmd.append("--uid=" + user) + encrypt_cmd.extend(["-", "-"]) + + rc, stdout, stderr = module.run_command(encrypt_cmd, data=secret, binary_data=True) + + module.exit_json( + changed=False, + value=stdout, + rc=rc, + stderr=stderr, + ) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/systemd_creds_decrypt/aliases b/tests/integration/targets/systemd_creds_decrypt/aliases new file mode 100644 index 00000000000..bac6fad493b --- /dev/null +++ b/tests/integration/targets/systemd_creds_decrypt/aliases @@ -0,0 +1,11 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +needs/root + +azp/posix/1 +skip/aix +skip/freebsd +skip/osx +skip/macos diff --git a/tests/integration/targets/systemd_creds_decrypt/tasks/main.yaml b/tests/integration/targets/systemd_creds_decrypt/tasks/main.yaml new file mode 100644 index 00000000000..2249bd7fa9d --- /dev/null +++ b/tests/integration/targets/systemd_creds_decrypt/tasks/main.yaml @@ -0,0 +1,58 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Test systemd_creds_decrypt + when: + - ansible_systemd.version is defined + - ansible_systemd.version | int >= 250 + block: + - name: Encrypt secret + become: true + systemd_creds_encrypt: + name: api + not_after: +48hr + secret: access_token + register: encrypted_api_secret + + - name: Print the encrypted secret + ansible.builtin.debug: + msg: "{{ encrypted_api_secret }}" + + - name: Decrypt secret + community.general.systemd_creds_decrypt: + name: api + newline: false + secret: "{{ encrypted_api_secret.value }}" + register: decrypted_secret + + - name: Print the decrypted secret + ansible.builtin.debug: + msg: "{{ decrypted_secret }}" + + - name: Assert that the decrypted secret is the same as the original secret + ansible.builtin.assert: + that: + - decrypted_secret.value == 'access_token' + fail_msg: "Decrypted secret is not the same as the original secret" + success_msg: "Decrypted secret is the same as the original secret" + + - name: Decrypt secret into hex + community.general.systemd_creds_decrypt: + name: api + newline: false + secret: "{{ encrypted_api_secret.value }}" + transcode: hex + register: decrypted_secret_hex + + - name: Print the trancoded decrypted secret + ansible.builtin.debug: + msg: "{{ decrypted_secret_hex }}" + + - name: Assert that the decrypted secret is the same as the original secret + ansible.builtin.assert: + that: + - decrypted_secret_hex.value == '6163636573735f746f6b656e' + fail_msg: "Decrypted secret is not the same as the original secret" + success_msg: "Decrypted secret is the same as the original secret" diff --git a/tests/integration/targets/systemd_creds_encrypt/aliases b/tests/integration/targets/systemd_creds_encrypt/aliases new file mode 100644 index 00000000000..bac6fad493b --- /dev/null +++ b/tests/integration/targets/systemd_creds_encrypt/aliases @@ -0,0 +1,11 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +needs/root + +azp/posix/1 +skip/aix +skip/freebsd +skip/osx +skip/macos diff --git a/tests/integration/targets/systemd_creds_encrypt/tasks/main.yaml b/tests/integration/targets/systemd_creds_encrypt/tasks/main.yaml new file mode 100644 index 00000000000..362fe90bcbf --- /dev/null +++ b/tests/integration/targets/systemd_creds_encrypt/tasks/main.yaml @@ -0,0 +1,55 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Test systemd_creds_encrypt + when: + - ansible_systemd.version is defined + - ansible_systemd.version | int >= 250 + block: + - name: Encrypt secret + become: true + systemd_creds_encrypt: + name: db + not_after: +48hr + secret: access_token + register: encrypted_secret + + - name: Assert encrypted secret output is base64 encoded + ansible.builtin.assert: + that: + - encrypted_secret.value | b64decode + fail_msg: "Encrypted secret is not base64 encoded" + success_msg: "Encrypted secret is base64 encoded" + + - name: Print the encrypted secret + ansible.builtin.debug: + msg: "{{ encrypted_secret }}" + + - name: Assert that SetCredentialEncrypted message is not in the output + ansible.builtin.assert: + that: + - '"SetCredentialEncrypted" not in encrypted_secret.value' + fail_msg: "SetCredentialEncrypted is in the output" + success_msg: "SetCredentialEncrypted is not in the output" + + - name: Encrypt secret + become: true + community.general.systemd_creds_encrypt: + name: web + not_after: +5y + pretty: true + secret: token + register: pretty_encrypted_secret + + - name: Pretty print the encrypted secret + ansible.builtin.debug: + msg: "{{ pretty_encrypted_secret }}" + + - name: Assert that SetCredentialEncrypted message is in the output + ansible.builtin.assert: + that: + - '"SetCredentialEncrypted=web: " in pretty_encrypted_secret.value' + fail_msg: "SetCredentialEncrypted is not in the output" + success_msg: "SetCredentialEncrypted is in the output"