Skip to content

Commit

Permalink
[PR #9728/410999df backport][stable-10] bitwarden lookup: add options…
Browse files Browse the repository at this point in the history
… to filter by collection_name and validate number of results (#9757)

bitwarden lookup: add options to filter by collection_name and validate number of results (#9728)

* feat(lookups/bitwarden): add collection_name filter

* feat(lookups/bitwarden): add result_count check

* docs(lookups/bitwarden): add changelog fragment

* Update changelogs/fragments/9728-bitwarden-collection-name-filter.yml

Co-authored-by: Felix Fontein <[email protected]>

* Update plugins/lookup/bitwarden.py

Co-authored-by: Felix Fontein <[email protected]>

* Update plugins/lookup/bitwarden.py

Co-authored-by: Felix Fontein <[email protected]>

* Update plugins/lookup/bitwarden.py

Co-authored-by: Felix Fontein <[email protected]>

* Update plugins/lookup/bitwarden.py

Co-authored-by: Felix Fontein <[email protected]>

* fix(lookups/bitwarden): fix result_count check for multiple terms

* fix(lookups/bitwarden): Enforce mutual exclusion of 'collection_name' and 'collection_id'

* formatting(lookups/bitwarden): remove trailing whitespace

* Update plugins/lookup/bitwarden.py

Co-authored-by: Alexei Znamensky <[email protected]>

* Update plugins/lookup/bitwarden.py

Co-authored-by: Alexei Znamensky <[email protected]>

* Update plugins/lookup/bitwarden.py

Co-authored-by: Alexei Znamensky <[email protected]>

* formatting(lookups/bitwarden): remove trailing whitespace

---------

Co-authored-by: Felix Fontein <[email protected]>
Co-authored-by: Alexei Znamensky <[email protected]>
(cherry picked from commit 410999d)

Co-authored-by: Jonas <[email protected]>
  • Loading branch information
patchback[bot] and frq-asgard-josi authored Feb 16, 2025
1 parent df28c80 commit ebb150c
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- bitwarden lookup plugin - add new option ``collection_name`` to filter results by collection name, and new option ``result_count`` to validate number of results (https://github.com/ansible-collections/community.general/pull/9728).
70 changes: 67 additions & 3 deletions plugins/lookup/bitwarden.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,17 @@
description: Field to fetch. Leave unset to fetch whole response.
type: str
collection_id:
description: Collection ID to filter results by collection. Leave unset to skip filtering.
description:
- Collection ID to filter results by collection. Leave unset to skip filtering.
- O(collection_id) and O(collection_name) are mutually exclusive.
type: str
version_added: 6.3.0
collection_name:
description:
- Collection name to filter results by collection. Leave unset to skip filtering.
- O(collection_id) and O(collection_name) are mutually exclusive.
type: str
version_added: 10.4.0
organization_id:
description: Organization ID to filter results by organization. Leave unset to skip filtering.
type: str
Expand All @@ -48,6 +56,12 @@
description: Pass session key instead of reading from env.
type: str
version_added: 8.4.0
result_count:
description:
- Number of results expected for the lookup query. Task will fail if O(result_count)
is set but does not match the number of query results. Leave empty to skip this check.
type: int
version_added: 10.4.0
"""

EXAMPLES = """
Expand Down Expand Up @@ -85,6 +99,16 @@
ansible.builtin.debug:
msg: >-
{{ lookup('community.general.bitwarden', None, collection_id='bafba515-af11-47e6-abe3-af1200cd18b2') }}
- name: "Get all Bitwarden records from collection"
ansible.builtin.debug:
msg: >-
{{ lookup('community.general.bitwarden', None, collection_name='my_collections/test_collection') }}
- name: "Get Bitwarden record named 'a_test', ensure there is exactly one match"
ansible.builtin.debug:
msg: >-
{{ lookup('community.general.bitwarden', 'a_test', result_count=1) }}
"""

RETURN = """
Expand All @@ -99,7 +123,7 @@

from subprocess import Popen, PIPE

from ansible.errors import AnsibleError
from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible.parsing.ajson import AnsibleJSONDecoder
from ansible.plugins.lookup import LookupBase
Expand Down Expand Up @@ -211,6 +235,24 @@ def get_field(self, field, search_value, search_field="name", collection_id=None

return field_matches

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."""

# Prepare set of params for Bitwarden CLI
params = ['list', 'collections', '--search', collection_name]

if organization_id:
params.extend(['--organizationid', organization_id])

out, err = self._run(params)

# This includes things that matched in different fields.
initial_matches = AnsibleJSONDecoder().raw_decode(out)[0]

# Filter to only return the ID of a collections with exactly matching name
return [item['id'] for item in initial_matches
if str(item.get('name')).lower() == collection_name.lower()]


class LookupModule(LookupBase):

Expand All @@ -219,7 +261,9 @@ def run(self, terms=None, variables=None, **kwargs):
field = self.get_option('field')
search_field = self.get_option('search')
collection_id = self.get_option('collection_id')
collection_name = self.get_option('collection_name')
organization_id = self.get_option('organization_id')
result_count = self.get_option('result_count')
_bitwarden.session = self.get_option('bw_session')

if not _bitwarden.unlocked:
Expand All @@ -228,7 +272,27 @@ def run(self, terms=None, variables=None, **kwargs):
if not terms:
terms = [None]

return [_bitwarden.get_field(field, term, search_field, collection_id, organization_id) for term in terms]
if collection_name and collection_id:
raise AnsibleOptionsError("'collection_name' and 'collection_id' are mutually exclusive!")
elif collection_name:
collection_ids = _bitwarden.get_collection_ids(collection_name, organization_id)
if not collection_ids:
raise BitwardenException("No matching collections found!")
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
]

for result in results:
if result_count is not None and len(result) != result_count:
raise BitwardenException(
f"Number of results doesn't match result_count! ({len(result)} != {result_count})")

return results


_bitwarden = Bitwarden()
73 changes: 71 additions & 2 deletions tests/unit/plugins/lookup/test_bitwarden.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ansible.errors import AnsibleError
from ansible.module_utils import six
from ansible.plugins.loader import lookup_loader
from ansible_collections.community.general.plugins.lookup.bitwarden import Bitwarden
from ansible_collections.community.general.plugins.lookup.bitwarden import Bitwarden, BitwardenException
from ansible.parsing.ajson import AnsibleJSONEncoder

MOCK_COLLECTION_ID = "3b12a9da-7c49-40b8-ad33-aede017a7ead"
Expand Down Expand Up @@ -131,7 +131,21 @@
"reprompt": 0,
"revisionDate": "2024-14-15T11:30:00.000Z",
"type": 1
}
},
{
"object": "collection",
"id": MOCK_COLLECTION_ID,
"organizationId": MOCK_ORGANIZATION_ID,
"name": "MOCK_COLLECTION",
"externalId": None
},
{
"object": "collection",
"id": "3b12a9da-7c49-40b8-ad33-aede017a8ead",
"organizationId": "3b12a9da-7c49-40b8-ad33-aede017a9ead",
"name": "some/other/collection",
"externalId": None
},
]


Expand Down Expand Up @@ -164,6 +178,9 @@ def _run(self, args, stdin=None, expected_rc=0):

items = []
for item in MOCK_RECORDS:
if item.get('object') != 'item':
continue

if search_value and not re.search(search_value, item.get('name')):
continue
if collection_to_filter and collection_to_filter not in item.get('collectionIds', []):
Expand All @@ -172,6 +189,35 @@ def _run(self, args, stdin=None, expected_rc=0):
continue
items.append(item)
return AnsibleJSONEncoder().encode(items), ''
elif args[1] == 'collections':
try:
search_value = args[args.index('--search') + 1]
except ValueError:
search_value = None

try:
collection_to_filter = args[args.index('--collectionid') + 1]
except ValueError:
collection_to_filter = None

try:
organization_to_filter = args[args.index('--organizationid') + 1]
except ValueError:
organization_to_filter = None

collections = []
for item in MOCK_RECORDS:
if item.get('object') != 'collection':
continue

if search_value and not re.search(search_value, item.get('name')):
continue
if collection_to_filter and collection_to_filter not in item.get('collectionIds', []):
continue
if organization_to_filter and item.get('organizationId') != organization_to_filter:
continue
collections.append(item)
return AnsibleJSONEncoder().encode(collections), ''

return '[]', ''

Expand Down Expand Up @@ -261,3 +307,26 @@ def test_bitwarden_plugin_filter_organization(self):
def test_bitwarden_plugin_full_collection_organization(self):
self.assertEqual([MOCK_RECORDS[0], MOCK_RECORDS[2]], self.lookup.run(None,
collection_id=MOCK_COLLECTION_ID, organization_id=MOCK_ORGANIZATION_ID)[0])

@patch('ansible_collections.community.general.plugins.lookup.bitwarden._bitwarden', new=MockBitwarden())
def test_bitwarden_plugin_collection_name_filter(self):
# all passwords from MOCK_COLLECTION
self.assertEqual([MOCK_RECORDS[0], MOCK_RECORDS[2]], self.lookup.run(None,
collection_name="MOCK_COLLECTION")[0])
# Existing collection, no results
self.assertEqual([], self.lookup.run(None, collection_name="some/other/collection")[0])

# Non-Existent collection
with self.assertRaises(BitwardenException):
self.lookup.run(None, collection_name="nonexistent")

@patch('ansible_collections.community.general.plugins.lookup.bitwarden._bitwarden', new=MockBitwarden())
def test_bitwarden_plugin_result_count_check(self):
self.lookup.run(None, collection_id=MOCK_COLLECTION_ID, organization_id=MOCK_ORGANIZATION_ID, result_count=2)
with self.assertRaises(BitwardenException):
self.lookup.run(None, collection_id=MOCK_COLLECTION_ID, organization_id=MOCK_ORGANIZATION_ID,
result_count=1)

self.lookup.run(None, organization_id=MOCK_ORGANIZATION_ID, result_count=3)
with self.assertRaises(BitwardenException):
self.lookup.run(None, organization_id=MOCK_ORGANIZATION_ID, result_count=0)

0 comments on commit ebb150c

Please sign in to comment.