From 5441e64d8d218e12d58f8f88a145791adc83404d Mon Sep 17 00:00:00 2001 From: Rchard DEMONGEOT Date: Mon, 13 Oct 2025 09:06:03 +0200 Subject: [PATCH 1/7] Add a feature to lookup a file, and get it. --- plugins/lookup/bitwarden.py | 80 ++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/plugins/lookup/bitwarden.py b/plugins/lookup/bitwarden.py index e4d958a96f3..a4e9c599d55 100644 --- a/plugins/lookup/bitwarden.py +++ b/plugins/lookup/bitwarden.py @@ -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. + - Mutually exclusive with O(field). type: str + version_added: 11.0.0 collection_id: description: - Collection ID to filter results by collection. Leave unset to skip filtering. @@ -72,6 +81,22 @@ 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 }}" + dest: /etc/vpn/server.key + mode: '0600' + +- 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: >- @@ -114,6 +139,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 """ @@ -232,6 +258,35 @@ 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) + 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.""" @@ -256,6 +311,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') @@ -266,6 +322,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] @@ -278,11 +338,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: From f6307b45b242a4e30437449bceef96181c4fe33b Mon Sep 17 00:00:00 2001 From: Rchard DEMONGEOT Date: Mon, 13 Oct 2025 09:24:13 +0200 Subject: [PATCH 2/7] Correct typo --- plugins/lookup/bitwarden.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/lookup/bitwarden.py b/plugins/lookup/bitwarden.py index a4e9c599d55..173d8d1320f 100644 --- a/plugins/lookup/bitwarden.py +++ b/plugins/lookup/bitwarden.py @@ -260,20 +260,19 @@ def get_field(self, field, search_value, search_field="name", collection_id=None 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) @@ -284,7 +283,6 @@ def get_attachment(self, attachment_name, search_value, search_field="name", col 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]: From a0f71843a985ff963b929b18379ec99a07325fc9 Mon Sep 17 00:00:00 2001 From: Rchard DEMONGEOT Date: Mon, 13 Oct 2025 09:27:29 +0200 Subject: [PATCH 3/7] Correct typo (v2) --- plugins/lookup/bitwarden.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lookup/bitwarden.py b/plugins/lookup/bitwarden.py index 173d8d1320f..135b3f6fea3 100644 --- a/plugins/lookup/bitwarden.py +++ b/plugins/lookup/bitwarden.py @@ -263,7 +263,7 @@ def get_attachment(self, attachment_name, search_value, search_field="name", col 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}") From 5db79a1c660c3b1513f967af97f027828eefafb5 Mon Sep 17 00:00:00 2001 From: Rchard DEMONGEOT Date: Tue, 14 Oct 2025 08:52:44 +0200 Subject: [PATCH 4/7] Answer to russoz comment --- plugins/lookup/bitwarden.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/lookup/bitwarden.py b/plugins/lookup/bitwarden.py index 135b3f6fea3..df52680dabb 100644 --- a/plugins/lookup/bitwarden.py +++ b/plugins/lookup/bitwarden.py @@ -41,7 +41,7 @@ - When set, the plugin will download the attachment content in raw format. - Mutually exclusive with O(field). type: str - version_added: 11.0.0 + version_added: 12.0.0 collection_id: description: - Collection ID to filter results by collection. Leave unset to skip filtering. @@ -91,6 +91,9 @@ content: "{{ lookup('community.general.bitwarden', 'VPN Config', attachment='vpn-server.key') | first }}" dest: /etc/vpn/server.key mode: '0600' + # 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: From 79393491e29cf20ef43556cefb1632700d640465 Mon Sep 17 00:00:00 2001 From: Rchard DEMONGEOT Date: Tue, 14 Oct 2025 08:57:43 +0200 Subject: [PATCH 5/7] Add Changelog for pull request 10917 --- changelogs/fragments/251014-bitwarden-attachement.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/251014-bitwarden-attachement.yml diff --git a/changelogs/fragments/251014-bitwarden-attachement.yml b/changelogs/fragments/251014-bitwarden-attachement.yml new file mode 100644 index 00000000000..463e9ab0525 --- /dev/null +++ b/changelogs/fragments/251014-bitwarden-attachement.yml @@ -0,0 +1,2 @@ +minor_changes: + - bitwarden - add availbility to get Attachment file from bitwarden (https://github.com/ansible-collections/community.general/pull/10917). From 194ac69c3b2fbb60b4f0eb0e7a433fef87a45e49 Mon Sep 17 00:00:00 2001 From: Rchard DEMONGEOT Date: Tue, 14 Oct 2025 08:58:20 +0200 Subject: [PATCH 6/7] Add Changelog for pull request 10917 - with good name --- ...-bitwarden-attachement.yml => 10917-bitwarden-attachement.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelogs/fragments/{251014-bitwarden-attachement.yml => 10917-bitwarden-attachement.yml} (100%) diff --git a/changelogs/fragments/251014-bitwarden-attachement.yml b/changelogs/fragments/10917-bitwarden-attachement.yml similarity index 100% rename from changelogs/fragments/251014-bitwarden-attachement.yml rename to changelogs/fragments/10917-bitwarden-attachement.yml From e9e528432719ae3d4c8e2459733523108e5c4ca8 Mon Sep 17 00:00:00 2001 From: Rchard DEMONGEOT Date: Wed, 15 Oct 2025 09:06:22 +0200 Subject: [PATCH 7/7] PR-10917: update changelog to be compliant with the Ansible policy --- changelogs/fragments/10917-bitwarden-attachement.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/10917-bitwarden-attachement.yml b/changelogs/fragments/10917-bitwarden-attachement.yml index 463e9ab0525..d92c59bbf66 100644 --- a/changelogs/fragments/10917-bitwarden-attachement.yml +++ b/changelogs/fragments/10917-bitwarden-attachement.yml @@ -1,2 +1,2 @@ minor_changes: - - bitwarden - add availbility to get Attachment file from bitwarden (https://github.com/ansible-collections/community.general/pull/10917). + - bitwarden lookup plugin - add availbility to get attachment file from bitwarden (https://github.com/ansible-collections/community.general/pull/10917).