Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,40 @@ 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 or updated, the module will return the entry ID.

```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"
register: secrets
```

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
```
6 changes: 6 additions & 0 deletions plugins/module_utils/vaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Contributor

@otxi otxi Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this function is useless as it relies on non-functional code. The get_vault_entries(server_base_url, token, vault_id) function is buggy because it doesn't handle pagination so we are limited until only 25 secrets (25 secrets / page) ... The result of a search in the variable entries is incomplete.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the information, I also noticed that when I moved the function from fetch_secrets.py. I already have implementing a filter for folders/paths on my to-do list (after this pull request is merged), maybe I will implement a pagination logic then as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for highlighting this issue. The pagination bug is duly noted and will be fixed in a future update.

for entry in entries:
if entry.get('name') == name:
return entry
return None
190 changes: 190 additions & 0 deletions plugins/modules/create_secret.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/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:
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
value:
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)
'''

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"
value: "p@ssw0rd1"
register: secrets
'''

RETURN = r'''
id:
description: returns the ID of the created/updated entry.
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=dict(
type='dict',
options=dict(
secret_name=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(
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']

secret = module.params.get('secret')
secret_name = secret.get('secret_name')
password = secret.get('value')
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')

try:
token = login(server_base_url, app_key, app_secret)
entries = get_vault_entries(server_base_url, token, vault_id)

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)
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)
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()
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)
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()
8 changes: 1 addition & 7 deletions plugins/modules/fetch_secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down