From 2e9bbf3b6089455835f3664ae8db52a39b4428c4 Mon Sep 17 00:00:00 2001 From: Patrick Preisler Date: Thu, 30 Jan 2025 08:31:18 +0100 Subject: [PATCH 1/8] moved find_entry_by_name function so that it can be used by other modules --- plugins/module_utils/vaults.py | 6 ++++++ plugins/modules/fetch_secrets.py | 8 +------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/vaults.py b/plugins/module_utils/vaults.py index f1b0d29..5722841 100644 --- a/plugins/module_utils/vaults.py +++ b/plugins/module_utils/vaults.py @@ -53,3 +53,9 @@ def get_vault_entries(server_base_url, token, vault_id): return json_data.get('data', []) except Exception as e: raise Exception(f"An error occurred while getting vault entries: {e}") + +def find_entry_by_name(entries, name): + for entry in entries: + if entry.get('name') == name: + return entry + return None \ No newline at end of file diff --git a/plugins/modules/fetch_secrets.py b/plugins/modules/fetch_secrets.py index 2ba538a..3a06a8c 100644 --- a/plugins/modules/fetch_secrets.py +++ b/plugins/modules/fetch_secrets.py @@ -72,17 +72,11 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.devolutions.dvls.plugins.module_utils.auth import login, logout -from ansible_collections.devolutions.dvls.plugins.module_utils.vaults import get_vaults, get_vault_entry, get_vault_entries +from ansible_collections.devolutions.dvls.plugins.module_utils.vaults import get_vaults, get_vault_entry, get_vault_entries, find_entry_by_name import os import json import requests -def find_entry_by_name(entries, name): - for entry in entries: - if entry.get('name') == name: - return entry - return None - def run_module(): module_args = dict( server_base_url=dict(type='str', required=True), From f56af3696ca186cd082e338599d769526a762689 Mon Sep 17 00:00:00 2001 From: Patrick Preisler Date: Thu, 30 Jan 2025 08:31:43 +0100 Subject: [PATCH 2/8] added module for writing credentials into the vault --- plugins/modules/create_secret.py | 195 +++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 plugins/modules/create_secret.py diff --git a/plugins/modules/create_secret.py b/plugins/modules/create_secret.py new file mode 100644 index 0000000..920da78 --- /dev/null +++ b/plugins/modules/create_secret.py @@ -0,0 +1,195 @@ +#!/usr/bin/python + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: create_secret + +short_description: create or update a credential to DVLS + +description: + - This module logs into the DVLS (Devolutions Server) service, checks if a entry inside a given path already exists and updates or creates a Credential by name. + - The module requires DVLS application credentials, a server base URL and the data needed to create a secret. + +options: + server_base_url: + description: The base URL of your DVLS. + required: true + type: str + app_key: + description: Application key for DVLS authentication. + required: true + type: str + app_secret: + description: Application secret for DVLS authentication. + required: true + type: str + vault_id: + description: The ID of the vault to access. + required: true + type: str + secret_path: + description: the (Folder-)Path where the secret should end up. + required: false + type: str + secret_type: + description: the type of secret that will get created. + required: false + type: str + default: Credentials + secret_subtype: + description: the secret subtype. + required: false + type: str + default: Default + secret_description: + description: the description for the secret. + required: false + type: str + secret: + description: the credential object, containing username and password. + required: true + type: list + elements: dict + suboptions: + secret_name: + description: the entry name/username. + required: true + type: str + password: + description: the password. + required: true + type: str + +author: + - Danny Bédard (@DannyBedard) +''' + +EXAMPLES = r''' +- name: Upload Credentials to DVLS + devolutions.dvls.create_secret: + server_base_url: "https://example.yourcompany.com" + app_key: "{{ lookup('env', 'DVLS_APP_KEY') }}" + app_secret: "{{ lookup('env', 'DVLS_APP_SECRET') }}" + vault_id: "00000000-0000-0000-0000-000000000000" + secret: + - secret_name: "my_secret_1" + - password: + register: secrets +''' + +RETURN = r''' +id: + description: when a new entry is created, the ID of that Entry will be returned. When an Entry is updated, nothing will be returned. + type: dict + returned: changed +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.devolutions.dvls.plugins.module_utils.auth import login, logout +from ansible_collections.devolutions.dvls.plugins.module_utils.vaults import get_vault_entries, find_entry_by_name +import requests + +def run_module(): + module_args = dict( + server_base_url=dict(type='str', required=True), + app_key=dict(type='str', required=True), + app_secret=dict(type='str', required=True), + vault_id=dict(type='str', required=True), + secret_path=dict(type='str', required=False), + secret_type=dict(type='str', required=False, default='Credential'), + secret_subtype=dict(type='str', required=False, default='Default'), + secret_description=dict(type='str', required=False), + secret=dict( + type='dict', + options=dict( + secret_name=dict(type='str', required=True), + password=dict(type='str', required=True) + ), + required=True + ) + ) + + result = dict() + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + if module.check_mode: + module.exit_json(**result) + + server_base_url = module.params['server_base_url'] + app_key = module.params['app_key'] + app_secret = module.params['app_secret'] + + try: + vault_id = module.params.get('vault_id') + secret = module.params.get('secret') + except Exception as e: + module.fail_json(msg=str(e), **result) + + try: + token = login(server_base_url, app_key, app_secret) + entries = get_vault_entries(server_base_url, token, vault_id) + + secret_name = secret.get('secret_name') + password = secret.get('password') + secret_type = module.params['secret_type'] + secret_subtype = module.params['secret_subtype'] + secret_path = module.params['secret_path'] + description = module.params['secret_description'] + + vault_headers = { + "Content-Type": "application/json", + "tokenId": token + } + + vault_body = { + "name": secret_name, + "type": secret_type, + "subtype": secret_subtype, + "path": secret_path, + "description": description, + "data": { + "username": secret_name, + "password": password + } + } + + #this filters the response by path (folder) + if secret_path: + path_entries = [] + for entry in entries: + if entry['path'] == secret_path: + path_entries.append(entry) + + #when an existing entry is found, it gets updated. Otherwise a new entry gets created + entry = find_entry_by_name(path_entries, secret_name) + if entry: + vault_url = f"{server_base_url}/api/v1/vault/{vault_id}/entry/{entry['id']}" + response = requests.put(vault_url, headers=vault_headers, json=vault_body) + response.raise_for_status() + else: + vault_url = f"{server_base_url}/api/v1/vault/{vault_id}/entry" + response = requests.post(vault_url, headers=vault_headers, json=vault_body) + response.raise_for_status() + result['id'] = response.json()['id'] + + result['changed'] = True + + except Exception as e: + module.fail_json(msg=str(e), **result) + finally: + logout(server_base_url, token) + + module.exit_json(**result) + +def main(): + run_module() + +if __name__ == '__main__': + main() From 67bd4a57fc98ef4d2e4f5058076b3ae130bcf18a Mon Sep 17 00:00:00 2001 From: Patrick Preisler Date: Mon, 3 Feb 2025 16:16:31 +0100 Subject: [PATCH 3/8] updated documentation --- README.md | 19 +++++++++++++++++++ plugins/modules/create_secret.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 98003ad..350a53f 100644 --- a/README.md +++ b/README.md @@ -147,3 +147,22 @@ For example, if you want to access the ```apiId``` from an ```API key secret```, ```yaml {{ secrets['some api key'].apiId }} ``` + +## Usage writing secrets + +If there is an existing secret in that path, it will update the secret. Otherwise a new secret entry will be created. +When a new secret was created, the module will return the entry ID. If an existing entry was updated, nothing will be returned. + +```yaml +- name: Upload Credentials to DVLS + devolutions.dvls.create_secret: + server_base_url: "https://example.yourcompany.com" + app_key: "{{ lookup('env', 'DVLS_APP_KEY') }}" + app_secret: "{{ lookup('env', 'DVLS_APP_SECRET') }}" + vault_id: "00000000-0000-0000-0000-000000000000" + secret_path: "path\\to\\folder" + secret: + - secret_name: "my_secret_1" + - password: "p@ssw0rd1" + register: secrets +``` \ No newline at end of file diff --git a/plugins/modules/create_secret.py b/plugins/modules/create_secret.py index 920da78..89d29ef 100644 --- a/plugins/modules/create_secret.py +++ b/plugins/modules/create_secret.py @@ -76,7 +76,7 @@ vault_id: "00000000-0000-0000-0000-000000000000" secret: - secret_name: "my_secret_1" - - password: + - password: "p@ssw0rd1" register: secrets ''' From 540c5b0ac922201a1b841e30b4aa66dbead20d80 Mon Sep 17 00:00:00 2001 From: Patrick Preisler Date: Mon, 3 Feb 2025 16:18:19 +0100 Subject: [PATCH 4/8] updated documentation --- README.md | 4 ++-- plugins/modules/create_secret.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 350a53f..417dd29 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ When a new secret was created, the module will return the entry ID. If an existi vault_id: "00000000-0000-0000-0000-000000000000" secret_path: "path\\to\\folder" secret: - - secret_name: "my_secret_1" - - password: "p@ssw0rd1" + secret_name: "my_secret_1" + password: "p@ssw0rd1" register: secrets ``` \ No newline at end of file diff --git a/plugins/modules/create_secret.py b/plugins/modules/create_secret.py index 89d29ef..8f552d3 100644 --- a/plugins/modules/create_secret.py +++ b/plugins/modules/create_secret.py @@ -75,8 +75,8 @@ app_secret: "{{ lookup('env', 'DVLS_APP_SECRET') }}" vault_id: "00000000-0000-0000-0000-000000000000" secret: - - secret_name: "my_secret_1" - - password: "p@ssw0rd1" + secret_name: "my_secret_1" + password: "p@ssw0rd1" register: secrets ''' From 24b2ca825696ebb505e91d4c01d9be2133bb4630 Mon Sep 17 00:00:00 2001 From: Patrick Preisler Date: Tue, 4 Feb 2025 09:56:35 +0100 Subject: [PATCH 5/8] code and logic cleanup --- plugins/modules/create_secret.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/plugins/modules/create_secret.py b/plugins/modules/create_secret.py index 8f552d3..2239827 100644 --- a/plugins/modules/create_secret.py +++ b/plugins/modules/create_secret.py @@ -126,23 +126,20 @@ def run_module(): app_key = module.params['app_key'] app_secret = module.params['app_secret'] - try: - vault_id = module.params.get('vault_id') - secret = module.params.get('secret') - except Exception as e: - module.fail_json(msg=str(e), **result) + secret = module.params.get('secret') + secret_name = secret.get('secret_name') + password = secret.get('password') + + vault_id = module.params.get('vault_id') + secret_type = module.params['secret_type'] + secret_subtype = module.params['secret_subtype'] + secret_path = module.params['secret_path'] + description = module.params['secret_description'] try: token = login(server_base_url, app_key, app_secret) entries = get_vault_entries(server_base_url, token, vault_id) - secret_name = secret.get('secret_name') - password = secret.get('password') - secret_type = module.params['secret_type'] - secret_subtype = module.params['secret_subtype'] - secret_path = module.params['secret_path'] - description = module.params['secret_description'] - vault_headers = { "Content-Type": "application/json", "tokenId": token @@ -161,11 +158,7 @@ def run_module(): } #this filters the response by path (folder) - if secret_path: - path_entries = [] - for entry in entries: - if entry['path'] == secret_path: - path_entries.append(entry) + path_entries = [entry for entry in entries if entry.get('path') == secret_path] if secret_path else entries #when an existing entry is found, it gets updated. Otherwise a new entry gets created entry = find_entry_by_name(path_entries, secret_name) From 4a392ffeaa385ee3742ff76553963d5ca4907fef Mon Sep 17 00:00:00 2001 From: Patrick Preisler Date: Fri, 7 Feb 2025 08:27:04 +0100 Subject: [PATCH 6/8] Update plugins/modules/create_secret.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danny Bédard --- plugins/modules/create_secret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/create_secret.py b/plugins/modules/create_secret.py index 2239827..79f1795 100644 --- a/plugins/modules/create_secret.py +++ b/plugins/modules/create_secret.py @@ -58,7 +58,7 @@ description: the entry name/username. required: true type: str - password: + value: description: the password. required: true type: str From 2a76a758f5532b28f02ab3790e1d101c8ad64cb9 Mon Sep 17 00:00:00 2001 From: Patrick Preisler Date: Wed, 19 Feb 2025 22:48:20 +0100 Subject: [PATCH 7/8] moved secret properties to secret option, added consistent return value (id) and updated documentation --- README.md | 26 +++++++++++--- plugins/modules/create_secret.py | 62 ++++++++++++++++---------------- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 417dd29..ad1d095 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ For example, if you want to access the ```apiId``` from an ```API key secret```, ## Usage writing secrets If there is an existing secret in that path, it will update the secret. Otherwise a new secret entry will be created. -When a new secret was created, the module will return the entry ID. If an existing entry was updated, nothing will be returned. +When a new secret was created or updated, the module will return the entry ID. ```yaml - name: Upload Credentials to DVLS @@ -160,9 +160,27 @@ When a new secret was created, the module will return the entry ID. If an existi app_key: "{{ lookup('env', 'DVLS_APP_KEY') }}" app_secret: "{{ lookup('env', 'DVLS_APP_SECRET') }}" vault_id: "00000000-0000-0000-0000-000000000000" - secret_path: "path\\to\\folder" secret: secret_name: "my_secret_1" - password: "p@ssw0rd1" + value: "p@ssw0rd1" register: secrets -``` \ No newline at end of file +``` + +Example with additional available options (Currently only the "Credential" type and "Default" subtype are supported): + +```yaml +- name: Upload Credentials to DVLS + devolutions.dvls.create_secret: + server_base_url: "https://example.yourcompany.com" + app_key: "{{ lookup('env', 'DVLS_APP_KEY') }}" + app_secret: "{{ lookup('env', 'DVLS_APP_SECRET') }}" + vault_id: "00000000-0000-0000-0000-000000000000" + secret: + secret_name: "my_secret_1" + value: "p@ssw0rd1" + secret_path: "path\\to\\folder" + secret_type: "Credentials" + secret_subtype: "Default" + secret_description: "a description for the secret" + register: secrets +``` diff --git a/plugins/modules/create_secret.py b/plugins/modules/create_secret.py index 79f1795..f09e1d1 100644 --- a/plugins/modules/create_secret.py +++ b/plugins/modules/create_secret.py @@ -30,24 +30,6 @@ description: The ID of the vault to access. required: true type: str - secret_path: - description: the (Folder-)Path where the secret should end up. - required: false - type: str - secret_type: - description: the type of secret that will get created. - required: false - type: str - default: Credentials - secret_subtype: - description: the secret subtype. - required: false - type: str - default: Default - secret_description: - description: the description for the secret. - required: false - type: str secret: description: the credential object, containing username and password. required: true @@ -62,6 +44,24 @@ description: the password. required: true type: str + secret_path: + description: the (Folder-)Path where the secret should end up. + required: false + type: str + secret_type: + description: the type of secret that will get created. + required: false + type: str + default: Credentials + secret_subtype: + description: the secret subtype. + required: false + type: str + default: Default + secret_description: + description: the description for the secret. + required: false + type: str author: - Danny Bédard (@DannyBedard) @@ -76,13 +76,13 @@ vault_id: "00000000-0000-0000-0000-000000000000" secret: secret_name: "my_secret_1" - password: "p@ssw0rd1" + value: "p@ssw0rd1" register: secrets ''' RETURN = r''' id: - description: when a new entry is created, the ID of that Entry will be returned. When an Entry is updated, nothing will be returned. + description: returns the ID of the created/updated entry. type: dict returned: changed ''' @@ -98,20 +98,21 @@ def run_module(): app_key=dict(type='str', required=True), app_secret=dict(type='str', required=True), vault_id=dict(type='str', required=True), - secret_path=dict(type='str', required=False), - secret_type=dict(type='str', required=False, default='Credential'), - secret_subtype=dict(type='str', required=False, default='Default'), - secret_description=dict(type='str', required=False), secret=dict( type='dict', options=dict( secret_name=dict(type='str', required=True), - password=dict(type='str', required=True) + value=dict(type='str', required=True), + secret_path=dict(type='str', required=False), + secret_type=dict(type='str', required=False, default='Credential'), + secret_subtype=dict(type='str', required=False, default='Default'), + secret_description=dict(type='str', required=False), ), required=True ) ) + result = dict() module = AnsibleModule( @@ -126,15 +127,15 @@ def run_module(): app_key = module.params['app_key'] app_secret = module.params['app_secret'] - secret = module.params.get('secret') + secret = module.params.get('value') secret_name = secret.get('secret_name') password = secret.get('password') + secret_type = secret.get('secret_type') + secret_subtype = secret.get('secret_subtype') + secret_path = secret.get('secret_path') + description = secret.get('secret_description') vault_id = module.params.get('vault_id') - secret_type = module.params['secret_type'] - secret_subtype = module.params['secret_subtype'] - secret_path = module.params['secret_path'] - description = module.params['secret_description'] try: token = login(server_base_url, app_key, app_secret) @@ -166,6 +167,7 @@ def run_module(): vault_url = f"{server_base_url}/api/v1/vault/{vault_id}/entry/{entry['id']}" response = requests.put(vault_url, headers=vault_headers, json=vault_body) response.raise_for_status() + result['id'] = entry['id'] else: vault_url = f"{server_base_url}/api/v1/vault/{vault_id}/entry" response = requests.post(vault_url, headers=vault_headers, json=vault_body) From 2d8282bf8e5f9f4139fb53752eb460665fffd2f7 Mon Sep 17 00:00:00 2001 From: Patrick Preisler Date: Thu, 20 Feb 2025 08:55:24 +0100 Subject: [PATCH 8/8] fixed variable initialization --- plugins/modules/create_secret.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/create_secret.py b/plugins/modules/create_secret.py index f09e1d1..0f42984 100644 --- a/plugins/modules/create_secret.py +++ b/plugins/modules/create_secret.py @@ -127,9 +127,9 @@ def run_module(): app_key = module.params['app_key'] app_secret = module.params['app_secret'] - secret = module.params.get('value') + secret = module.params.get('secret') secret_name = secret.get('secret_name') - password = secret.get('password') + password = secret.get('value') secret_type = secret.get('secret_type') secret_subtype = secret.get('secret_subtype') secret_path = secret.get('secret_path')