We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
lookup bitwarden field=notes resulting in KeyError
### Summary

When using the community.general.bitwarden lookup there is an error trying to lookup a secure note.

Playbook:
```yaml
- debug:
    msg: >-
      "{{ lookup('community.general.bitwarden', 'secure note 2023', field='notes') }}"
```

Error:
```
fatal: [my.hostname.net]: FAILED! => {
    "msg": "An unhandled exception occurred while running the lookup plugin 'community.general.bitwarden'. Error was a <class 'KeyError'>, original message: 'fields'. 'fields'"
}
```
With debug:
```console
exception during Jinja2 execution: Traceback (most recent call last):
  File "/opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible/template/__init__.py", line 831, in _lookup
    ran = instance.run(loop_terms, variables=self._available_variables, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible_collections/community/general/plugins/lookup/bitwarden.py", line 159, in run
    return [_bitwarden.get_field(field, term, search_field, collection_id) for term in terms]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible_collections/community/general/plugins/lookup/bitwarden.py", line 159, in <listcomp>
    return [_bitwarden.get_field(field, term, search_field, collection_id) for term in terms]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible_collections/community/general/plugins/lookup/bitwarden.py", line 141, in get_field
    for custom_field in match['fields']:
                        ~~~~~^^^^^^^^^^
KeyError: 'fields'
fatal: [my.hostname.net]: FAILED! => {
    "msg": "An unhandled exception occurred while running the lookup plugin 'community.general.bitwarden'. Error was a <class 'KeyError'>, original message: 'fields'. 'fields'"
}
```

Same thing when trying to do the lookup by id:
```yaml
- debug:
    msg: >-
      "{{ lookup('community.general.bitwarden', 'ba9f1125-e52a-4d4f-9b2b-af1c43a00bd6', search='id', field='notes') }}"
```

### Issue Type

Bug Report

### Component Name

bitwarden

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.15.0]
  config file = /Users/sjoerd/.ansible.cfg
  configured module search path = ['/Users/name/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible
  ansible collection location = /Users/name/.ansible/collections:/usr/share/ansible/collections
  executable location = /opt/homebrew/bin/ansible
  python version = 3.11.3 (main, Apr  7 2023, 20:13:31) [Clang 14.0.0 (clang-1400.0.29.202)] (/opt/homebrew/Cellar/ansible/8.0.0/libexec/bin/python3.11)
  jinja version = 3.1.2
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general

# /opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 7.0.1
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

Source: MacOS
Target: CentOS Stream 9


### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- debug:
    msg: >-
      "{{ lookup('community.general.bitwarden', 'secure note 2023', field='notes') }}"

```


### Expected Results

Expected the contents of the secure not to be printed to the terminal

### Actual Results

```console (paste below)
fatal: [my.hostname.net]: FAILED! => {
    "msg": "An unhandled exception occurred while running the lookup plugin 'community.general.bitwarden'. Error was a <class 'KeyError'>, original message: 'fields'. 'fields'"
}
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `plugins/lookup/bitwarden.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2022, Jonathan Lung <lungj@heresjono.com>
3 # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
4 # SPDX-License-Identifier: GPL-3.0-or-later
5 from __future__ import (absolute_import, division, print_function)
6 __metaclass__ = type
7 
8 DOCUMENTATION = """
9     name: bitwarden
10     author:
11       - Jonathan Lung (@lungj) <lungj@heresjono.com>
12     requirements:
13       - bw (command line utility)
14       - be logged into bitwarden
15       - bitwarden vault unlocked
16       - E(BW_SESSION) environment variable set
17     short_description: Retrieve secrets from Bitwarden
18     version_added: 5.4.0
19     description:
20       - Retrieve secrets from Bitwarden.
21     options:
22       _terms:
23         description: Key(s) to fetch values for from login info.
24         required: true
25         type: list
26         elements: str
27       search:
28         description: Field to retrieve, for example V(name) or V(id).
29         type: str
30         default: name
31         version_added: 5.7.0
32       field:
33         description: Field to fetch. Leave unset to fetch whole response.
34         type: str
35       collection_id:
36         description: Collection ID to filter results by collection. Leave unset to skip filtering.
37         type: str
38         version_added: 6.3.0
39 """
40 
41 EXAMPLES = """
42 - name: "Get 'password' from Bitwarden record named 'a_test'"
43   ansible.builtin.debug:
44     msg: >-
45       {{ lookup('community.general.bitwarden', 'a_test', field='password') }}
46 
47 - name: "Get 'password' from Bitwarden record with id 'bafba515-af11-47e6-abe3-af1200cd18b2'"
48   ansible.builtin.debug:
49     msg: >-
50       {{ lookup('community.general.bitwarden', 'bafba515-af11-47e6-abe3-af1200cd18b2', search='id', field='password') }}
51 
52 - name: "Get 'password' from Bitwarden record named 'a_test' from collection"
53   ansible.builtin.debug:
54     msg: >-
55       {{ lookup('community.general.bitwarden', 'a_test', field='password', collection_id='bafba515-af11-47e6-abe3-af1200cd18b2') }}
56 
57 - name: "Get full Bitwarden record named 'a_test'"
58   ansible.builtin.debug:
59     msg: >-
60       {{ lookup('community.general.bitwarden', 'a_test') }}
61 
62 - name: "Get custom field 'api_key' from Bitwarden record named 'a_test'"
63   ansible.builtin.debug:
64     msg: >-
65       {{ lookup('community.general.bitwarden', 'a_test', field='api_key') }}
66 """
67 
68 RETURN = """
69   _raw:
70     description: List of requested field or JSON object of list of matches.
71     type: list
72     elements: raw
73 """
74 
75 from subprocess import Popen, PIPE
76 
77 from ansible.errors import AnsibleError
78 from ansible.module_utils.common.text.converters import to_bytes, to_text
79 from ansible.parsing.ajson import AnsibleJSONDecoder
80 from ansible.plugins.lookup import LookupBase
81 
82 
83 class BitwardenException(AnsibleError):
84     pass
85 
86 
87 class Bitwarden(object):
88 
89     def __init__(self, path='bw'):
90         self._cli_path = path
91 
92     @property
93     def cli_path(self):
94         return self._cli_path
95 
96     @property
97     def unlocked(self):
98         out, err = self._run(['status'], stdin="")
99         decoded = AnsibleJSONDecoder().raw_decode(out)[0]
100         return decoded['status'] == 'unlocked'
101 
102     def _run(self, args, stdin=None, expected_rc=0):
103         p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE)
104         out, err = p.communicate(to_bytes(stdin))
105         rc = p.wait()
106         if rc != expected_rc:
107             raise BitwardenException(err)
108         return to_text(out, errors='surrogate_or_strict'), to_text(err, errors='surrogate_or_strict')
109 
110     def _get_matches(self, search_value, search_field, collection_id):
111         """Return matching records whose search_field is equal to key.
112         """
113 
114         # Prepare set of params for Bitwarden CLI
115         params = ['list', 'items', '--search', search_value]
116 
117         if collection_id:
118             params.extend(['--collectionid', collection_id])
119 
120         out, err = self._run(params)
121 
122         # This includes things that matched in different fields.
123         initial_matches = AnsibleJSONDecoder().raw_decode(out)[0]
124 
125         # Filter to only include results from the right field.
126         return [item for item in initial_matches if item[search_field] == search_value]
127 
128     def get_field(self, field, search_value, search_field="name", collection_id=None):
129         """Return a list of the specified field for records whose search_field match search_value
130         and filtered by collection if collection has been provided.
131 
132         If field is None, return the whole record for each match.
133         """
134         matches = self._get_matches(search_value, search_field, collection_id)
135 
136         if field in ['autofillOnPageLoad', 'password', 'passwordRevisionDate', 'totp', 'uris', 'username']:
137             return [match['login'][field] for match in matches]
138         elif not field:
139             return matches
140         else:
141             custom_field_matches = []
142             for match in matches:
143                 for custom_field in match['fields']:
144                     if custom_field['name'] == field:
145                         custom_field_matches.append(custom_field['value'])
146             if matches and not custom_field_matches:
147                 raise AnsibleError("Custom field {field} does not exist in {search_value}".format(field=field, search_value=search_value))
148             return custom_field_matches
149 
150 
151 class LookupModule(LookupBase):
152 
153     def run(self, terms, variables=None, **kwargs):
154         self.set_options(var_options=variables, direct=kwargs)
155         field = self.get_option('field')
156         search_field = self.get_option('search')
157         collection_id = self.get_option('collection_id')
158         if not _bitwarden.unlocked:
159             raise AnsibleError("Bitwarden Vault locked. Run 'bw unlock'.")
160 
161         return [_bitwarden.get_field(field, term, search_field, collection_id) for term in terms]
162 
163 
164 _bitwarden = Bitwarden()
165
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/plugins/lookup/bitwarden.py b/plugins/lookup/bitwarden.py
--- a/plugins/lookup/bitwarden.py
+++ b/plugins/lookup/bitwarden.py
@@ -132,20 +132,29 @@
         If field is None, return the whole record for each match.
         """
         matches = self._get_matches(search_value, search_field, collection_id)
-
-        if field in ['autofillOnPageLoad', 'password', 'passwordRevisionDate', 'totp', 'uris', 'username']:
-            return [match['login'][field] for match in matches]
-        elif not field:
+        if not field:
             return matches
-        else:
-            custom_field_matches = []
-            for match in matches:
+        field_matches = []
+        for match in matches:
+            # if there are no custom fields, then `match` has no key 'fields'
+            if 'fields' in match:
+                custom_field_found = False
                 for custom_field in match['fields']:
-                    if custom_field['name'] == field:
-                        custom_field_matches.append(custom_field['value'])
-            if matches and not custom_field_matches:
-                raise AnsibleError("Custom field {field} does not exist in {search_value}".format(field=field, search_value=search_value))
-            return custom_field_matches
+                    if field == custom_field['name']:
+                        field_matches.append(custom_field['value'])
+                        custom_field_found = True
+                        break
+                if custom_field_found:
+                    continue
+            if 'login' in match and field in match['login']:
+                field_matches.append(match['login'][field])
+                continue
+            if field in match:
+                field_matches.append(match[field])
+                continue
+        if matches and not field_matches:
+            raise AnsibleError("field {field} does not exist in {search_value}".format(field=field, search_value=search_value))
+        return field_matches
 
 
 class LookupModule(LookupBase):

{"golden_diff": "diff --git a/plugins/lookup/bitwarden.py b/plugins/lookup/bitwarden.py\n--- a/plugins/lookup/bitwarden.py\n+++ b/plugins/lookup/bitwarden.py\n@@ -132,20 +132,29 @@\n         If field is None, return the whole record for each match.\n         \"\"\"\n         matches = self._get_matches(search_value, search_field, collection_id)\n-\n-        if field in ['autofillOnPageLoad', 'password', 'passwordRevisionDate', 'totp', 'uris', 'username']:\n-            return [match['login'][field] for match in matches]\n-        elif not field:\n+        if not field:\n             return matches\n-        else:\n-            custom_field_matches = []\n-            for match in matches:\n+        field_matches = []\n+        for match in matches:\n+            # if there are no custom fields, then `match` has no key 'fields'\n+            if 'fields' in match:\n+                custom_field_found = False\n                 for custom_field in match['fields']:\n-                    if custom_field['name'] == field:\n-                        custom_field_matches.append(custom_field['value'])\n-            if matches and not custom_field_matches:\n-                raise AnsibleError(\"Custom field {field} does not exist in {search_value}\".format(field=field, search_value=search_value))\n-            return custom_field_matches\n+                    if field == custom_field['name']:\n+                        field_matches.append(custom_field['value'])\n+                        custom_field_found = True\n+                        break\n+                if custom_field_found:\n+                    continue\n+            if 'login' in match and field in match['login']:\n+                field_matches.append(match['login'][field])\n+                continue\n+            if field in match:\n+                field_matches.append(match[field])\n+                continue\n+        if matches and not field_matches:\n+            raise AnsibleError(\"field {field} does not exist in {search_value}\".format(field=field, search_value=search_value))\n+        return field_matches\n \n \n class LookupModule(LookupBase):\n", "issue": "lookup bitwarden field=notes resulting in KeyError\n### Summary\n\nWhen using the community.general.bitwarden lookup there is an error trying to lookup a secure note.\r\n\r\nPlaybook:\r\n```yaml\r\n- debug:\r\n    msg: >-\r\n      \"{{ lookup('community.general.bitwarden', 'secure note 2023', field='notes') }}\"\r\n```\r\n\r\nError:\r\n```\r\nfatal: [my.hostname.net]: FAILED! => {\r\n    \"msg\": \"An unhandled exception occurred while running the lookup plugin 'community.general.bitwarden'. Error was a <class 'KeyError'>, original message: 'fields'. 'fields'\"\r\n}\r\n```\r\nWith debug:\r\n```console\r\nexception during Jinja2 execution: Traceback (most recent call last):\r\n  File \"/opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible/template/__init__.py\", line 831, in _lookup\r\n    ran = instance.run(loop_terms, variables=self._available_variables, **kwargs)\r\n          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible_collections/community/general/plugins/lookup/bitwarden.py\", line 159, in run\r\n    return [_bitwarden.get_field(field, term, search_field, collection_id) for term in terms]\r\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible_collections/community/general/plugins/lookup/bitwarden.py\", line 159, in <listcomp>\r\n    return [_bitwarden.get_field(field, term, search_field, collection_id) for term in terms]\r\n            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible_collections/community/general/plugins/lookup/bitwarden.py\", line 141, in get_field\r\n    for custom_field in match['fields']:\r\n                        ~~~~~^^^^^^^^^^\r\nKeyError: 'fields'\r\nfatal: [my.hostname.net]: FAILED! => {\r\n    \"msg\": \"An unhandled exception occurred while running the lookup plugin 'community.general.bitwarden'. Error was a <class 'KeyError'>, original message: 'fields'. 'fields'\"\r\n}\r\n```\r\n\r\nSame thing when trying to do the lookup by id:\r\n```yaml\r\n- debug:\r\n    msg: >-\r\n      \"{{ lookup('community.general.bitwarden', 'ba9f1125-e52a-4d4f-9b2b-af1c43a00bd6', search='id', field='notes') }}\"\r\n```\n\n### Issue Type\n\nBug Report\n\n### Component Name\n\nbitwarden\n\n### Ansible Version\n\n```console (paste below)\r\n$ ansible --version\r\nansible [core 2.15.0]\r\n  config file = /Users/sjoerd/.ansible.cfg\r\n  configured module search path = ['/Users/name/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']\r\n  ansible python module location = /opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible\r\n  ansible collection location = /Users/name/.ansible/collections:/usr/share/ansible/collections\r\n  executable location = /opt/homebrew/bin/ansible\r\n  python version = 3.11.3 (main, Apr  7 2023, 20:13:31) [Clang 14.0.0 (clang-1400.0.29.202)] (/opt/homebrew/Cellar/ansible/8.0.0/libexec/bin/python3.11)\r\n  jinja version = 3.1.2\r\n  libyaml = True\r\n```\r\n\n\n### Community.general Version\n\n```console (paste below)\r\n$ ansible-galaxy collection list community.general\r\n\r\n# /opt/homebrew/Cellar/ansible/8.0.0/libexec/lib/python3.11/site-packages/ansible_collections\r\nCollection        Version\r\n----------------- -------\r\ncommunity.general 7.0.1\r\n```\r\n\n\n### Configuration\n\n```console (paste below)\r\n$ ansible-config dump --only-changed\r\n\r\n```\r\n\n\n### OS / Environment\n\nSource: MacOS\r\nTarget: CentOS Stream 9\r\n\n\n### Steps to Reproduce\n\n<!--- Paste example playbooks or commands between quotes below -->\r\n```yaml (paste below)\r\n- debug:\r\n    msg: >-\r\n      \"{{ lookup('community.general.bitwarden', 'secure note 2023', field='notes') }}\"\r\n\r\n```\r\n\n\n### Expected Results\n\nExpected the contents of the secure not to be printed to the terminal\n\n### Actual Results\n\n```console (paste below)\r\nfatal: [my.hostname.net]: FAILED! => {\r\n    \"msg\": \"An unhandled exception occurred while running the lookup plugin 'community.general.bitwarden'. Error was a <class 'KeyError'>, original message: 'fields'. 'fields'\"\r\n}\r\n```\r\n\n\n### Code of Conduct\n\n- [X] I agree to follow the Ansible Code of Conduct\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2022, Jonathan Lung <lungj@heresjono.com>\n# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)\n# SPDX-License-Identifier: GPL-3.0-or-later\nfrom __future__ import (absolute_import, division, print_function)\n__metaclass__ = type\n\nDOCUMENTATION = \"\"\"\n    name: bitwarden\n    author:\n      - Jonathan Lung (@lungj) <lungj@heresjono.com>\n    requirements:\n      - bw (command line utility)\n      - be logged into bitwarden\n      - bitwarden vault unlocked\n      - E(BW_SESSION) environment variable set\n    short_description: Retrieve secrets from Bitwarden\n    version_added: 5.4.0\n    description:\n      - Retrieve secrets from Bitwarden.\n    options:\n      _terms:\n        description: Key(s) to fetch values for from login info.\n        required: true\n        type: list\n        elements: str\n      search:\n        description: Field to retrieve, for example V(name) or V(id).\n        type: str\n        default: name\n        version_added: 5.7.0\n      field:\n        description: Field to fetch. Leave unset to fetch whole response.\n        type: str\n      collection_id:\n        description: Collection ID to filter results by collection. Leave unset to skip filtering.\n        type: str\n        version_added: 6.3.0\n\"\"\"\n\nEXAMPLES = \"\"\"\n- name: \"Get 'password' from Bitwarden record named 'a_test'\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'a_test', field='password') }}\n\n- name: \"Get 'password' from Bitwarden record with id 'bafba515-af11-47e6-abe3-af1200cd18b2'\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'bafba515-af11-47e6-abe3-af1200cd18b2', search='id', field='password') }}\n\n- name: \"Get 'password' from Bitwarden record named 'a_test' from collection\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'a_test', field='password', collection_id='bafba515-af11-47e6-abe3-af1200cd18b2') }}\n\n- name: \"Get full Bitwarden record named 'a_test'\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'a_test') }}\n\n- name: \"Get custom field 'api_key' from Bitwarden record named 'a_test'\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'a_test', field='api_key') }}\n\"\"\"\n\nRETURN = \"\"\"\n  _raw:\n    description: List of requested field or JSON object of list of matches.\n    type: list\n    elements: raw\n\"\"\"\n\nfrom subprocess import Popen, PIPE\n\nfrom ansible.errors import AnsibleError\nfrom ansible.module_utils.common.text.converters import to_bytes, to_text\nfrom ansible.parsing.ajson import AnsibleJSONDecoder\nfrom ansible.plugins.lookup import LookupBase\n\n\nclass BitwardenException(AnsibleError):\n    pass\n\n\nclass Bitwarden(object):\n\n    def __init__(self, path='bw'):\n        self._cli_path = path\n\n    @property\n    def cli_path(self):\n        return self._cli_path\n\n    @property\n    def unlocked(self):\n        out, err = self._run(['status'], stdin=\"\")\n        decoded = AnsibleJSONDecoder().raw_decode(out)[0]\n        return decoded['status'] == 'unlocked'\n\n    def _run(self, args, stdin=None, expected_rc=0):\n        p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE)\n        out, err = p.communicate(to_bytes(stdin))\n        rc = p.wait()\n        if rc != expected_rc:\n            raise BitwardenException(err)\n        return to_text(out, errors='surrogate_or_strict'), to_text(err, errors='surrogate_or_strict')\n\n    def _get_matches(self, search_value, search_field, collection_id):\n        \"\"\"Return matching records whose search_field is equal to key.\n        \"\"\"\n\n        # Prepare set of params for Bitwarden CLI\n        params = ['list', 'items', '--search', search_value]\n\n        if collection_id:\n            params.extend(['--collectionid', collection_id])\n\n        out, err = self._run(params)\n\n        # This includes things that matched in different fields.\n        initial_matches = AnsibleJSONDecoder().raw_decode(out)[0]\n\n        # Filter to only include results from the right field.\n        return [item for item in initial_matches if item[search_field] == search_value]\n\n    def get_field(self, field, search_value, search_field=\"name\", collection_id=None):\n        \"\"\"Return a list of the specified field for records whose search_field match search_value\n        and filtered by collection if collection has been provided.\n\n        If field is None, return the whole record for each match.\n        \"\"\"\n        matches = self._get_matches(search_value, search_field, collection_id)\n\n        if field in ['autofillOnPageLoad', 'password', 'passwordRevisionDate', 'totp', 'uris', 'username']:\n            return [match['login'][field] for match in matches]\n        elif not field:\n            return matches\n        else:\n            custom_field_matches = []\n            for match in matches:\n                for custom_field in match['fields']:\n                    if custom_field['name'] == field:\n                        custom_field_matches.append(custom_field['value'])\n            if matches and not custom_field_matches:\n                raise AnsibleError(\"Custom field {field} does not exist in {search_value}\".format(field=field, search_value=search_value))\n            return custom_field_matches\n\n\nclass LookupModule(LookupBase):\n\n    def run(self, terms, variables=None, **kwargs):\n        self.set_options(var_options=variables, direct=kwargs)\n        field = self.get_option('field')\n        search_field = self.get_option('search')\n        collection_id = self.get_option('collection_id')\n        if not _bitwarden.unlocked:\n            raise AnsibleError(\"Bitwarden Vault locked. Run 'bw unlock'.\")\n\n        return [_bitwarden.get_field(field, term, search_field, collection_id) for term in terms]\n\n\n_bitwarden = Bitwarden()\n", "path": "plugins/lookup/bitwarden.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2022, Jonathan Lung <lungj@heresjono.com>\n# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)\n# SPDX-License-Identifier: GPL-3.0-or-later\nfrom __future__ import (absolute_import, division, print_function)\n__metaclass__ = type\n\nDOCUMENTATION = \"\"\"\n    name: bitwarden\n    author:\n      - Jonathan Lung (@lungj) <lungj@heresjono.com>\n    requirements:\n      - bw (command line utility)\n      - be logged into bitwarden\n      - bitwarden vault unlocked\n      - E(BW_SESSION) environment variable set\n    short_description: Retrieve secrets from Bitwarden\n    version_added: 5.4.0\n    description:\n      - Retrieve secrets from Bitwarden.\n    options:\n      _terms:\n        description: Key(s) to fetch values for from login info.\n        required: true\n        type: list\n        elements: str\n      search:\n        description: Field to retrieve, for example V(name) or V(id).\n        type: str\n        default: name\n        version_added: 5.7.0\n      field:\n        description: Field to fetch. Leave unset to fetch whole response.\n        type: str\n      collection_id:\n        description: Collection ID to filter results by collection. Leave unset to skip filtering.\n        type: str\n        version_added: 6.3.0\n\"\"\"\n\nEXAMPLES = \"\"\"\n- name: \"Get 'password' from Bitwarden record named 'a_test'\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'a_test', field='password') }}\n\n- name: \"Get 'password' from Bitwarden record with id 'bafba515-af11-47e6-abe3-af1200cd18b2'\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'bafba515-af11-47e6-abe3-af1200cd18b2', search='id', field='password') }}\n\n- name: \"Get 'password' from Bitwarden record named 'a_test' from collection\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'a_test', field='password', collection_id='bafba515-af11-47e6-abe3-af1200cd18b2') }}\n\n- name: \"Get full Bitwarden record named 'a_test'\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'a_test') }}\n\n- name: \"Get custom field 'api_key' from Bitwarden record named 'a_test'\"\n  ansible.builtin.debug:\n    msg: >-\n      {{ lookup('community.general.bitwarden', 'a_test', field='api_key') }}\n\"\"\"\n\nRETURN = \"\"\"\n  _raw:\n    description: List of requested field or JSON object of list of matches.\n    type: list\n    elements: raw\n\"\"\"\n\nfrom subprocess import Popen, PIPE\n\nfrom ansible.errors import AnsibleError\nfrom ansible.module_utils.common.text.converters import to_bytes, to_text\nfrom ansible.parsing.ajson import AnsibleJSONDecoder\nfrom ansible.plugins.lookup import LookupBase\n\n\nclass BitwardenException(AnsibleError):\n    pass\n\n\nclass Bitwarden(object):\n\n    def __init__(self, path='bw'):\n        self._cli_path = path\n\n    @property\n    def cli_path(self):\n        return self._cli_path\n\n    @property\n    def unlocked(self):\n        out, err = self._run(['status'], stdin=\"\")\n        decoded = AnsibleJSONDecoder().raw_decode(out)[0]\n        return decoded['status'] == 'unlocked'\n\n    def _run(self, args, stdin=None, expected_rc=0):\n        p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE)\n        out, err = p.communicate(to_bytes(stdin))\n        rc = p.wait()\n        if rc != expected_rc:\n            raise BitwardenException(err)\n        return to_text(out, errors='surrogate_or_strict'), to_text(err, errors='surrogate_or_strict')\n\n    def _get_matches(self, search_value, search_field, collection_id):\n        \"\"\"Return matching records whose search_field is equal to key.\n        \"\"\"\n\n        # Prepare set of params for Bitwarden CLI\n        params = ['list', 'items', '--search', search_value]\n\n        if collection_id:\n            params.extend(['--collectionid', collection_id])\n\n        out, err = self._run(params)\n\n        # This includes things that matched in different fields.\n        initial_matches = AnsibleJSONDecoder().raw_decode(out)[0]\n\n        # Filter to only include results from the right field.\n        return [item for item in initial_matches if item[search_field] == search_value]\n\n    def get_field(self, field, search_value, search_field=\"name\", collection_id=None):\n        \"\"\"Return a list of the specified field for records whose search_field match search_value\n        and filtered by collection if collection has been provided.\n\n        If field is None, return the whole record for each match.\n        \"\"\"\n        matches = self._get_matches(search_value, search_field, collection_id)\n        if not field:\n            return matches\n        field_matches = []\n        for match in matches:\n            # if there are no custom fields, then `match` has no key 'fields'\n            if 'fields' in match:\n                custom_field_found = False\n                for custom_field in match['fields']:\n                    if field == custom_field['name']:\n                        field_matches.append(custom_field['value'])\n                        custom_field_found = True\n                        break\n                if custom_field_found:\n                    continue\n            if 'login' in match and field in match['login']:\n                field_matches.append(match['login'][field])\n                continue\n            if field in match:\n                field_matches.append(match[field])\n                continue\n        if matches and not field_matches:\n            raise AnsibleError(\"field {field} does not exist in {search_value}\".format(field=field, search_value=search_value))\n        return field_matches\n\n\nclass LookupModule(LookupBase):\n\n    def run(self, terms, variables=None, **kwargs):\n        self.set_options(var_options=variables, direct=kwargs)\n        field = self.get_option('field')\n        search_field = self.get_option('search')\n        collection_id = self.get_option('collection_id')\n        if not _bitwarden.unlocked:\n            raise AnsibleError(\"Bitwarden Vault locked. Run 'bw unlock'.\")\n\n        return [_bitwarden.get_field(field, term, search_field, collection_id) for term in terms]\n\n\n_bitwarden = Bitwarden()\n", "path": "plugins/lookup/bitwarden.py"}]}