Repository: ansible-collections/community.general

Issue:
Onepassword v2 Lookup Plugin is Case Sensitive
### Summary

I try to do a lookup to a 1Password field, but because the Onepassword v2 plugin is case sensitive it cannot find the password from the field.

See: https://github.com/ansible-collections/community.general/blob/c604cc5ba901574697b99bd31236d99e0e6cd1a3/plugins/lookup/onepassword.py#L453

### Issue Type

Bug Report

### Component Name

onepassword

### Ansible Version

```console
ansible [core 2.13.4]
  config file = /Users/jip/Ansible/servers/[REMOVED]/ansible.cfg
  configured module search path = ['/Users/jip/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.9/site-packages/ansible
  ansible collection location = /Users/jip/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.9.12 (main, Mar 26 2022, 15:52:10) [Clang 13.0.0 (clang-1300.0.29.30)]
  jinja version = 3.1.2
  libyaml = True
```


### Community.general Version

```console (paste below)
Collection        Version
----------------- -------
community.general 6.3.0
```


### Configuration

_No response_

### OS / Environment

Mac OS 12.4 x86

### Steps to Reproduce

```yaml (paste below)
password: "{{ lookup('community.general.onepassword', 'password_item', vault='password_vault', field='password', section='section name') }}"
```


### Expected Results

I expect that the field `password` in the section `section name` get the password from 1Password which has the field name `Password` and the section name `Section Name`.

### Actual Results

The lookup returns an empty string.

### Code of Conduct

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


Discussion:
Files identified in the description:
* [`plugins/modules/onepassword_info.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/onepassword_info.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @Rylon
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Files identified in the description:
* [`plugins/lookup/onepassword.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/lookup/onepassword.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @azenk @samdoran @scottsb
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
+1
I'm not sure why this should be a **bug**. The lookup does not claim to be case-insensitive w.r.t. section and field name. So this looks more like a feature request to me?

(This might simply be a expectation mismatch as well because other 1Password tools are case insensitive. I don't know whether you can add two different sections which only differ by casing, or two different fields that differ only by casing.)
It seems like an expectation mismatch which could be interpreted as a bug. I think it would be good to make the field name matching case insensitive since the items created in 1Password may be either "Password" or "password".
Is the field name selected by the user? I'm wondering why it's sometimes `password` and sometimes `Password`. If it is determined by the user, is it possible to add both a `password` and a `Password` field to the same entry?
1Password creates the field, but the user can change it. I'm thinking that may older versions of 1Password created items with a 'Password' label, but I'm not sure.

> If it is determined by the user, is it possible to add both a password and a Password field to the same entry?

Yes. There is an `id` and `label` for each item. `id` is a unique value. `label` is what is visible in the UI. The plugins favor `label` over `id` when looking for the requested value. The first matched value is returned.
Sounds like a good matching algorithm needs to be somewhat complex then :)
@felixfontein for clarification: the problem is that the behaviour of the lookup plugin changed from case-insensitive to case-sensitive causing quite a lot of fallout, since 1password used to display all section names, always, in upper case letters, for several years and still does it on the mac client in the default views (on edit you get, since a year or so, the correct upper/lower strings) 
so we got a lot of those upper-case lookups in our ansible code that are now failing

I see the issue now. When I wrote the [v2 code](https://github.com/ansible-collections/community.general/blob/9b16392648711ad42ced286aae7b6c5c2f4a29b4/plugins/lookup/onepassword.py#L511), I did not make it case insensitive [like the v1 code is](https://github.com/ansible-collections/community.general/blob/9b16392648711ad42ced286aae7b6c5c2f4a29b4/plugins/lookup/onepassword.py#L318). This is a regression.
Ah, that's good to know! Thanks for the update!
@rembart @bitjip Could you test with #7564 to see if that resolves this issue?

Resolving patch:
diff --git a/plugins/lookup/onepassword.py b/plugins/lookup/onepassword.py
--- a/plugins/lookup/onepassword.py
+++ b/plugins/lookup/onepassword.py
@@ -96,6 +96,14 @@
 from ansible_collections.community.general.plugins.module_utils.onepassword import OnePasswordConfig
 
 
+def _lower_if_possible(value):
+    """Return the lower case version value, otherwise return the value"""
+    try:
+        return value.lower()
+    except AttributeError:
+        return value
+
+
 class OnePassCLIBase(with_metaclass(abc.ABCMeta, object)):
     bin = "op"
 
@@ -457,6 +465,7 @@ def _parse_field(self, data_json, field_name, section_title=None):
             }
         """
         data = json.loads(data_json)
+        field_name = _lower_if_possible(field_name)
         for field in data.get("fields", []):
             if section_title is None:
                 # If the field name exists in the section, return that value
@@ -465,17 +474,19 @@ def _parse_field(self, data_json, field_name, section_title=None):
 
                 # If the field name doesn't exist in the section, match on the value of "label"
                 # then "id" and return "value"
-                if field.get("label") == field_name:
+                if field.get("label", "").lower() == field_name:
                     return field.get("value", "")
 
-                if field.get("id") == field_name:
+                if field.get("id", "").lower() == field_name:
                     return field.get("value", "")
 
             # Look at the section data and get an identifier. The value of 'id' is either a unique ID
             # or a human-readable string. If a 'label' field exists, prefer that since
             # it is the value visible in the 1Password UI when both 'id' and 'label' exist.
             section = field.get("section", {})
-            current_section_title = section.get("label", section.get("id"))
+            section_title = _lower_if_possible(section_title)
+
+            current_section_title = section.get("label", section.get("id", "")).lower()
             if section_title == current_section_title:
                 # In the correct section. Check "label" then "id" for the desired field_name
                 if field.get("label") == field_name:


Test patch:
diff --git a/tests/unit/plugins/lookup/onepassword_common.py b/tests/unit/plugins/lookup/onepassword_common.py
--- a/tests/unit/plugins/lookup/onepassword_common.py
+++ b/tests/unit/plugins/lookup/onepassword_common.py
@@ -107,7 +107,7 @@ def load_file(file):
             "queries": ["Omitted values"],
             "kwargs": {
                 "field": "section-label-without-value",
-                "section": "section-without-values"
+                "section": "Section-Without-Values"
             },
             "expected": [""],
             "output": load_file("v2_out_04.json")
diff --git a/tests/unit/plugins/lookup/onepassword_fixtures/v2_out_01.json b/tests/unit/plugins/lookup/onepassword_fixtures/v2_out_01.json
--- a/tests/unit/plugins/lookup/onepassword_fixtures/v2_out_01.json
+++ b/tests/unit/plugins/lookup/onepassword_fixtures/v2_out_01.json
@@ -13,10 +13,10 @@
     "additional_information": "Jan 18, 2015, 08:13:38",
     "fields": [
         {
-            "id": "password",
+            "id": "Password",
             "type": "CONCEALED",
             "purpose": "PASSWORD",
-            "label": "password",
+            "label": "Password",
             "value": "OctoberPoppyNuttyDraperySabbath",
             "reference": "op://Test Vault/Authy Backup/password",
             "password_details": {