Skip to content
Open
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
2 changes: 2 additions & 0 deletions changelogs/fragments/10917-bitwarden-attachement.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- bitwarden lookup plugin - add availbility to get attachment file from bitwarden (https://github.com/ansible-collections/community.general/pull/10917).
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you mean "ability" or "availability" (an "a" is missing for that)? I think the former makes more sense:

Suggested change
- bitwarden lookup plugin - add availbility to get attachment file from bitwarden (https://github.com/ansible-collections/community.general/pull/10917).
- bitwarden lookup plugin - add ability to get attachment file from bitwarden (https://github.com/ansible-collections/community.general/pull/10917).

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is a bit odd as "availability", indeed.

81 changes: 75 additions & 6 deletions plugins/lookup/bitwarden.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,17 @@
default: name
version_added: 5.7.0
field:
description: Field to fetch. Leave unset to fetch whole response.
description:
- Field to fetch. Leave unset to fetch whole response.
- Mutually exclusive with O(attachment).
type: str
attachment:
description:
- Name of the attachment to download from the item.
- When set, the plugin will download the attachment content in raw format.
Copy link
Collaborator

Choose a reason for hiding this comment

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

What does "raw format" mean? Do you mean "without further processing"? What if the file is binary data (Ansible doesn't like to handle binary data, one usually encodes it as Base64 because of that)?

- Mutually exclusive with O(field).
type: str
version_added: 12.0.0
collection_id:
description:
- Collection ID to filter results by collection. Leave unset to skip filtering.
Expand Down Expand Up @@ -72,6 +81,25 @@
msg: >-
{{ lookup('community.general.bitwarden', 'bafba515-af11-47e6-abe3-af1200cd18b2', search='id', field='password') | first }}

- name: "Get attachment 'vpn-server.key' from Bitwarden record named 'VPN Config'"
ansible.builtin.debug:
msg: >-
{{ lookup('community.general.bitwarden', 'VPN Config', attachment='vpn-server.key') }}

- name: "Save attachment to file"
ansible.builtin.copy:
content: "{{ lookup('community.general.bitwarden', 'VPN Config', attachment='vpn-server.key') | first }}"
Copy link
Collaborator

Choose a reason for hiding this comment

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

While this isn't the first time | first was used in the examples: what's the goal of using | first here? Does this always return a list of more than one elemnts? If yes, | first is fine. If not, | first is problematic because lookup() itself is problematic - if the lookup plugin returns a one-element list, it automatically takes the single element and returns that instead of the list. (And I think you get None or "" if the list is empty - forgot which one.) Usually it's better to use query(), since that will always return the list, even if it has zero or one elements.

dest: /etc/vpn/server.key
mode: '0600'
Comment on lines +91 to +93
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not necessarily a problem, but I can't help remembering that the lookup plugins run on the controller, whilst the copy task (unless otherwise set) will run on the target(s). This feature has potential to make a dent on the network bandwidth, depending on the file size and number of targets. It would be nice to have at least a warning in the documentation, for new and possibly unwise users.

That begin said, when slurping a file, the result comes encoded as base64 - I wonder why that is so and whether it would make sense to make that in this case as well - take this with a large grain of salt, I have no hard evidence to support any claim.

Choose a reason for hiding this comment

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

A comment was putted just now.

Don't sure that store file from bitwarden is the better idea; but works for now, with couple of stored files.

For network traffic, from Ansible computer to target, any copy will generate network traffic. In this case we'll add from bitwarden/vaultwarden to ansible computer.

# Be aware, as the lookup run into the Ansible computer, it can generate important network traffic.
# Once from bitwarden/vaultwarden to the Ansible computer;
# Twice (as for locally stored files) from Ansible computer to the Ansible target.

- name: "Get attachment from item by ID"
ansible.builtin.debug:
msg: >-
{{ lookup('community.general.bitwarden', 'bafba515-af11-47e6-abe3-af1200cd18b2', search='id', attachment='cert.pem') | first }}

- name: "Get 'password' from all Bitwarden records named 'a_test' from collection"
ansible.builtin.debug:
msg: >-
Expand Down Expand Up @@ -114,6 +142,7 @@
- A one-element list that contains a list of requested fields or JSON objects of matches.
- If you use C(query), you get a list of lists. If you use C(lookup) without C(wantlist=true), this always gets reduced
to a list of field values or JSON objects.
- When O(attachment) is specified, returns the raw content of the attachment(s).
type: list
elements: list
"""
Expand Down Expand Up @@ -232,6 +261,33 @@ def get_field(self, field, search_value, search_field="name", collection_id=None

return field_matches

def get_attachment(self, attachment_name, search_value, search_field="name", collection_id=None, organization_id=None):
"""Download attachment from records whose search_field match search_value.
Returns a list of attachment contents (as raw bytes converted to text) for each matching item.
"""
matches = self._get_matches(search_value, search_field, collection_id, organization_id)

if not matches:
raise AnsibleError(f"No item found matching {search_field}={search_value}")

attachment_contents = []
for match in matches:
item_id = match.get('id')
if not item_id:
raise AnsibleError(f"Item {match.get('name', 'unknown')} has no ID")

try:
params = ['get', 'attachment', attachment_name, '--itemid', item_id, '--raw']
out, err = self._run(params)
Copy link
Collaborator

Choose a reason for hiding this comment

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

out and err are unicode strings; the output of the CLI is parsed as UTF-8. Is this what is meant by "raw format"?

(Can Bitwarden actually store binary files, or must all files be UTF-8 encoded text?)

attachment_contents.append(out)
except BitwardenException as e:
# Provide more context about which item failed
item_name = match.get('name', item_id)
raise AnsibleError(
f"Failed to get attachment '{attachment_name}' from item '{item_name}' (ID: {item_id}): {str(e)}"
)
return attachment_contents

def get_collection_ids(self, collection_name: str, organization_id=None) -> list[str]:
"""Return matching IDs of collections whose name is equal to collection_name."""

Expand All @@ -256,6 +312,7 @@ class LookupModule(LookupBase):
def run(self, terms=None, variables=None, **kwargs):
self.set_options(var_options=variables, direct=kwargs)
field = self.get_option('field')
attachment = self.get_option('attachment')
search_field = self.get_option('search')
collection_id = self.get_option('collection_id')
collection_name = self.get_option('collection_name')
Expand All @@ -266,6 +323,10 @@ def run(self, terms=None, variables=None, **kwargs):
if not _bitwarden.unlocked:
raise AnsibleError("Bitwarden Vault locked. Run 'bw unlock'.")

# Validate mutually exclusive options
if field and attachment:
raise AnsibleOptionsError("'field' and 'attachment' are mutually exclusive!")

if not terms:
terms = [None]

Expand All @@ -278,11 +339,19 @@ def run(self, terms=None, variables=None, **kwargs):
else:
collection_ids = [collection_id]

results = [
_bitwarden.get_field(field, term, search_field, collection_id, organization_id)
for collection_id in collection_ids
for term in terms
]
# Choose the appropriate method based on what's requested
if attachment:
results = [
_bitwarden.get_attachment(attachment, term, search_field, collection_id, organization_id)
for collection_id in collection_ids
for term in terms
]
else:
results = [
_bitwarden.get_field(field, term, search_field, collection_id, organization_id)
for collection_id in collection_ids
for term in terms
]

for result in results:
if result_count is not None and len(result) != result_count:
Expand Down