From 01336e7a9fa6235904ecaf98f8ae815b4797068f Mon Sep 17 00:00:00 2001 From: Ansgar Jazdzewski Date: Mon, 13 Dec 2021 09:08:34 +0100 Subject: [PATCH 1/2] ADD: Organization and Collection to lookup. This will enable lager bitwarden deployments to make use of collections. In order to have the same key stored with different values, for multiple environments --- README.md | 24 ++++++++++ lookup_plugins/bitwarden.py | 96 ++++++++++++++++++++++++++++++++----- 2 files changed, 109 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d538c42..0d0105a 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,30 @@ ok: [localhost] => { } ``` +### Get a single password use organization and collection + +``` +--- +- hosts: localhost + roles: + - ansible-modules-bitwarden + tasks: + - debug: + msg: "{{ lookup('bitwarden', 'google', field='password', organization='my org', collection='shared accounts', sync=True) }}" +``` + +The above might result in: + +``` +TASK [Gathering Facts] ********************************************************* +ok: [localhost] + +TASK [debug] ********************************************************* +ok: [localhost] => { + "msg": "mysecret" +} +``` + ### See all available fields ```yaml diff --git a/lookup_plugins/bitwarden.py b/lookup_plugins/bitwarden.py index 03a7828..969e469 100755 --- a/lookup_plugins/bitwarden.py +++ b/lookup_plugins/bitwarden.py @@ -117,6 +117,9 @@ def _run(self, args): elif out.startswith("Not found."): raise AnsibleError("Error accessing Bitwarden vault. " "Specified item not found: {}".format(args[-1])) + elif out.startswith("More than one result was found."): + raise AnsibleError("Error accessing Bitwarden vault. " + "Specified item found more than once: {}".format(args[-1])) else: raise AnsibleError("Unknown failure in 'bw' command: " "{0}".format(out)) @@ -132,15 +135,78 @@ def status(self): raise AnsibleError("Error decoding Bitwarden status: %s" % e) return data['status'] - def get_entry(self, key, field): - return self._run(["get", field, key]) + def organization(self, name): + try: + data = json.loads(self._run(['list', 'organizations'])) + except json.decoder.JSONDecodeError as e: + raise AnsibleError("Error decoding Bitwarden list organizations: %s" % e) + + if not isinstance(data, list): + raise AnsibleError("Error decoding Bitwarden list organizations no organization in list") + + if len(data) == 0: + raise AnsibleError("Error decoding Bitwarden list organizations no organization in list") - def get_notes(self, key): - data = json.loads(self.get_entry(key, 'item')) - return data['notes'] + for organization in data: + if 'id' in organization.keys() and 'name' in organization.keys() and organization['name'] == name: + return(organization['id']) - def get_custom_field(self, key, field): - data = json.loads(self.get_entry(key, 'item')) + raise AnsibleError("Error decoding Bitwarden list organizations no organization not found: %s" % name) + + def collection(self, name): + try: + data = json.loads(self._run(['list', 'collections'])) + except json.decoder.JSONDecodeError as e: + raise AnsibleError("Error decoding Bitwarden list collections: %s" % e) + + if not isinstance(data, list): + raise AnsibleError("Error decoding Bitwarden list collections no collection in list") + + if len(data) == 0: + raise AnsibleError("Error decoding Bitwarden list collections no collection in list") + + for collection in data: + if 'id' in collection.keys() and 'name' in collection.keys() and collection['name'] == name: + return(collection['id']) + + raise AnsibleError("Error decoding Bitwarden list collections no collection not found: %s" % name) + + def get_entry(self, key, field, organizationId=None, collectionId=None): + #return self._run(["get", field, key]) + data = json.loads(self._run(['list', 'items', '--search', key])) + if not isinstance(data, list): + raise AnsibleError("Error decoding Bitwarden list items no item in list") + + if len(data) == 0: + raise AnsibleError("Error decoding Bitwarden list items no item in list") + + _return = [] + for result in data: + if 'id' in result.keys() and 'name' in result.keys() and 'collectionIds' in result.keys() and 'organizationId' in result.keys(): + if organizationId == None: + pass + elif result['organizationId'] != organizationId: + continue + if collectionId == None: + pass + elif collectionId not in result['collectionIds']: + continue + if field == 'item': + _return.append(result) + elif field == 'password': + _return.append(result['login']['password']) + elif field == 'username': + _return.append(result['login']['username']) + elif field in result.keys(): + _return.append(result[field]) + if len(_return) > 1: + raise AnsibleError("Error decoding Bitwarden list items more then one item found for: %s" % field) + elif len(_return) == 1: + return _return[0] + raise AnsibleError("Error decoding Bitwarden list items no field not found: %s" % field) + + def get_custom_field(self, key, field, organizationId=None, collectionId=None): + data = json.loads(self.get_entry(key, 'item', organizationId, collectionId)) return next(x for x in data['fields'] if x['name'] == field)['value'] def get_attachments(self, key, itemid, output): @@ -160,8 +226,18 @@ def run(self, terms, variables=None, **kwargs): "BW_SESSION environment variable first") field = kwargs.get('field', 'password') + organization = kwargs.get('organization', None) + organizationId = None + collection = kwargs.get('collection', None) + collectionId = None values = [] + if organization != None: + organizationId = bw.organization(organization) + + if collection != None: + collectionId = bw.collection(collection) + if kwargs.get('sync'): bw.sync() if kwargs.get('session'): @@ -169,9 +245,7 @@ def run(self, terms, variables=None, **kwargs): for term in terms: if kwargs.get('custom_field'): - values.append(bw.get_custom_field(term, field)) - elif field == 'notes': - values.append(bw.get_notes(term)) + values.append(bw.get_custom_field(term, field, organizationId, collectionId)) elif kwargs.get('attachments'): if kwargs.get('itemid'): itemid = kwargs.get('itemid') @@ -182,7 +256,7 @@ def run(self, terms, variables=None, **kwargs): "Please set parameters as example: - " "itemid='f12345-d343-4bd0-abbf-4532222' ") else: - values.append(bw.get_entry(term, field)) + values.append(bw.get_entry(term, field, organizationId, collectionId)) return values From 5a10681f2aaaa5575387ce707d20eaa2768b4cb5 Mon Sep 17 00:00:00 2001 From: Ansgar Jazdzewski Date: Fri, 1 Apr 2022 15:45:52 +0200 Subject: [PATCH 2/2] [STOR-368] add attachments and customfields in ansible-bitwarden --- README.md | 6 ++--- lookup_plugins/bitwarden.py | 44 ++++++++++++++----------------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 0d0105a..19c3417 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ ok: [localhost] => { ```yaml # Get the value of a custom field - debug: - msg: {{ lookup('bitwarden', 'Google', field='mycustomfield', custom_field=true) }} + msg: {{ lookup('bitwarden', 'Google', field='mycustomfield', type='custom') }} ``` The above might result in: @@ -150,7 +150,7 @@ ok: [localhost] => { ```yaml # Get the value of a custom field - debug: - msg: {{ lookup('bitwarden', 'privateKey.pem', itemid='123456-1234-1234-abbf-60c345aaa3e', attachments=true ) }} + msg: {{ lookup('bitwarden', 'Google', field='privateKey.pem', type='attachment' ) }} ``` Optional parameters - output='/ansible/publicKey.pem' @@ -159,6 +159,6 @@ The above might result in: ``` TASK [debug] ********************************************************* ok: [localhost] => { - "msg": "Saved /publicKey.pem" + "msg": "ssh-rsa xxxx foo@bar" } ``` diff --git a/lookup_plugins/bitwarden.py b/lookup_plugins/bitwarden.py index 969e469..a639f94 100755 --- a/lookup_plugins/bitwarden.py +++ b/lookup_plugins/bitwarden.py @@ -171,7 +171,7 @@ def collection(self, name): raise AnsibleError("Error decoding Bitwarden list collections no collection not found: %s" % name) - def get_entry(self, key, field, organizationId=None, collectionId=None): + def get_entry(self, key, field, organizationId=None, collectionId=None, type='default'): #return self._run(["get", field, key]) data = json.loads(self._run(['list', 'items', '--search', key])) if not isinstance(data, list): @@ -191,28 +191,28 @@ def get_entry(self, key, field, organizationId=None, collectionId=None): pass elif collectionId not in result['collectionIds']: continue - if field == 'item': + + if type == 'default' and field == 'item': _return.append(result) - elif field == 'password': + elif type == 'default' and field == 'password': _return.append(result['login']['password']) - elif field == 'username': + elif type == 'default' and field == 'username': _return.append(result['login']['username']) - elif field in result.keys(): + elif type == 'custom' and 'fields' in result.keys() and any(field in x['name'] for x in result['fields']): + for x in result['fields']: + if x['name'] == field: + _return.append( x['value']) + elif type == 'attachment' and 'attachments' in result.keys() and any(field in x['fileName'] for x in result['attachments']): + for x in result['attachments']: + if x['fileName'] == field: + _return.append(self._run(['get', 'attachment', x['id'], '--quiet', '--raw', '--output', '/dev/stdout', '--itemid', result['id']])) + elif type == 'default' and field in result.keys(): _return.append(result[field]) if len(_return) > 1: raise AnsibleError("Error decoding Bitwarden list items more then one item found for: %s" % field) elif len(_return) == 1: return _return[0] raise AnsibleError("Error decoding Bitwarden list items no field not found: %s" % field) - - def get_custom_field(self, key, field, organizationId=None, collectionId=None): - data = json.loads(self.get_entry(key, 'item', organizationId, collectionId)) - return next(x for x in data['fields'] if x['name'] == field)['value'] - - def get_attachments(self, key, itemid, output): - attachment = ['get', 'attachment', '{}'.format( - key), '--output={}'.format(output), '--itemid={}'.format(itemid)] - return self._run(attachment) class LookupModule(LookupBase): @@ -226,6 +226,7 @@ def run(self, terms, variables=None, **kwargs): "BW_SESSION environment variable first") field = kwargs.get('field', 'password') + type = kwargs.get('type', 'default') organization = kwargs.get('organization', None) organizationId = None collection = kwargs.get('collection', None) @@ -240,23 +241,12 @@ def run(self, terms, variables=None, **kwargs): if kwargs.get('sync'): bw.sync() + if kwargs.get('session'): bw.session = kwargs.get('session') for term in terms: - if kwargs.get('custom_field'): - values.append(bw.get_custom_field(term, field, organizationId, collectionId)) - elif kwargs.get('attachments'): - if kwargs.get('itemid'): - itemid = kwargs.get('itemid') - output = kwargs.get('output', term) - values.append(bw.get_attachments(term, itemid, output)) - else: - raise AnsibleError("Missing value for - itemid - " - "Please set parameters as example: - " - "itemid='f12345-d343-4bd0-abbf-4532222' ") - else: - values.append(bw.get_entry(term, field, organizationId, collectionId)) + values.append(bw.get_entry(term, field, organizationId, collectionId, type)) return values