diff --git a/changelogs/fragments/onepassword-v2.yml b/changelogs/fragments/onepassword-v2.yml new file mode 100644 index 0000000000..251cf473ca --- /dev/null +++ b/changelogs/fragments/onepassword-v2.yml @@ -0,0 +1,2 @@ +breaking_changes: + - "onepassword* lookup plugins - drop support for ``op`` version 1 (https://github.com/ansible-collections/community.general/pull/12061)." diff --git a/plugins/doc_fragments/_onepassword.py b/plugins/doc_fragments/_onepassword.py index 4df7ff93ab..4f20fa8797 100644 --- a/plugins/doc_fragments/_onepassword.py +++ b/plugins/doc_fragments/_onepassword.py @@ -11,6 +11,7 @@ from __future__ import annotations class ModuleDocFragment: DOCUMENTATION = r""" requirements: + - C(op) 1Password command line utility version 2 or later. - See U(https://support.1password.com/command-line/) options: master_password: @@ -39,7 +40,6 @@ options: service_account_token: description: - The access key for a service account. - - Only works with 1Password CLI version 2 or later. type: str vault: description: Vault containing the item to retrieve (case-insensitive). If absent, searches all vaults. diff --git a/plugins/lookup/onepassword.py b/plugins/lookup/onepassword.py index 863643b3db..d63015a6ac 100644 --- a/plugins/lookup/onepassword.py +++ b/plugins/lookup/onepassword.py @@ -15,8 +15,6 @@ author: short_description: Fetch field values from 1Password description: - P(community.general.onepassword#lookup) wraps the C(op) command line utility to fetch specific field values from 1Password. -requirements: - - C(op) 1Password command line utility options: _terms: description: @@ -214,151 +212,6 @@ class OnePassCLIBase(metaclass=abc.ABCMeta): return to_text(b_out).strip() -class OnePassCLIv1(OnePassCLIBase): - supports_version = "1" - - def _parse_field(self, data_json, field_name, section_title): - """ - Retrieves the desired field from the `op` response payload - - When the item is a `password` type, the password is a key within the `details` key: - - $ op get item 'test item' | jq - { - [...] - "templateUuid": "005", - "details": { - "notesPlain": "", - "password": "foobar", - "passwordHistory": [], - "sections": [ - { - "name": "linked items", - "title": "Related Items" - } - ] - }, - [...] - } - - However, when the item is a `login` type, the password is within a fields array: - - $ op get item 'test item' | jq - { - [...] - "details": { - "fields": [ - { - "designation": "username", - "name": "username", - "type": "T", - "value": "foo" - }, - { - "designation": "password", - "name": "password", - "type": "P", - "value": "bar" - } - ], - [...] - }, - [...] - """ - data = json.loads(data_json) - if section_title is None: - # https://github.com/ansible-collections/community.general/pull/1610: - # check the details dictionary for `field_name` and return it immediately if it exists - # when the entry is a "password" instead of a "login" item, the password field is a key - # in the `details` dictionary: - if field_name in data["details"]: - return data["details"][field_name] - - # when the field is not found above, iterate through the fields list in the object details - for field_data in data["details"].get("fields", []): - if field_data.get("name", "").lower() == field_name.lower(): - return field_data.get("value", "") - - for section_data in data["details"].get("sections", []): - if section_title is not None and section_title.lower() != section_data["title"].lower(): - continue - - for field_data in section_data.get("fields", []): - if field_data.get("t", "").lower() == field_name.lower(): - return field_data.get("v", "") - - return "" - - def assert_logged_in(self): - args = ["get", "account"] - if self.account_id: - args.extend(["--account", self.account_id]) - elif self.subdomain: - account = f"{self.subdomain}.{self.domain}" - args.extend(["--account", account]) - - rc, out, err = self._run(args, ignore_errors=True) - - return not bool(rc) - - def full_signin(self): - if self.connect_host or self.connect_token: - raise AnsibleLookupError( - "1Password Connect is not available with 1Password CLI version 1. Please use version 2 or later." - ) - - if self.service_account_token: - raise AnsibleLookupError( - "1Password CLI version 1 does not support Service Accounts. Please use version 2 or later." - ) - - required_params = [ - "subdomain", - "username", - "secret_key", - "master_password", - ] - self._check_required_params(required_params) - - args = [ - "signin", - f"{self.subdomain}.{self.domain}", - to_bytes(self.username), - to_bytes(self.secret_key), - "--raw", - ] - - return self._run(args, command_input=to_bytes(self.master_password)) - - def get_raw(self, item_id, vault=None, token=None): - args = ["get", "item", item_id] - - if self.account_id: - args.extend(["--account", self.account_id]) - - if vault is not None: - args += [f"--vault={vault}"] - - if token is not None: - args += [to_bytes("--session=") + token] - - return self._run(args) - - def signin(self): - self._check_required_params(["master_password"]) - - args = ["signin", "--raw"] - if self.subdomain: - args.append(self.subdomain) - - return self._run(args, command_input=to_bytes(self.master_password)) - - def get_secret_reference(self, reference): - raise AnsibleLookupError( - "Secret references are not supported in op v1. Upgrade to op v2 or use item names/UUIDs" - ) - - class OnePassCLIv2(OnePassCLIBase): """ CLIv2 Syntax Reference: https://developer.1password.com/docs/cli/upgrade#step-2-update-your-scripts diff --git a/plugins/lookup/onepassword_doc.py b/plugins/lookup/onepassword_doc.py index d6d4f84e49..8d38a10a94 100644 --- a/plugins/lookup/onepassword_doc.py +++ b/plugins/lookup/onepassword_doc.py @@ -8,15 +8,12 @@ DOCUMENTATION = r""" name: onepassword_doc author: - Sam Doran (@samdoran) -requirements: - - C(op) 1Password command line utility version 2 or later. short_description: Fetch documents stored in 1Password version_added: "8.1.0" description: - P(community.general.onepassword_doc#lookup) wraps C(op) command line utility to fetch one or more documents from 1Password. notes: - The document contents are a string exactly as stored in 1Password. - - This plugin requires C(op) version 2 or later. options: _terms: description: Identifier(s) (case-insensitive UUID or name) of item(s) to retrieve. diff --git a/plugins/lookup/onepassword_raw.py b/plugins/lookup/onepassword_raw.py index 712136c038..48e6918ee1 100644 --- a/plugins/lookup/onepassword_raw.py +++ b/plugins/lookup/onepassword_raw.py @@ -12,8 +12,6 @@ author: - Scott Buchanan (@scottsb) - Andrew Zenk (@azenk) - Sam Doran (@samdoran) -requirements: - - C(op) 1Password command line utility short_description: Fetch an entire item from 1Password description: - P(community.general.onepassword_raw#lookup) wraps C(op) command line utility to fetch an entire item from 1Password. diff --git a/plugins/lookup/onepassword_ssh_key.py b/plugins/lookup/onepassword_ssh_key.py index 1bc30f083a..5bbe049a1e 100644 --- a/plugins/lookup/onepassword_ssh_key.py +++ b/plugins/lookup/onepassword_ssh_key.py @@ -8,8 +8,6 @@ DOCUMENTATION = r""" name: onepassword_ssh_key author: - Mohammed Babelly (@mohammedbabelly20) -requirements: - - C(op) 1Password command line utility version 2 or later. short_description: Fetch SSH keys stored in 1Password version_added: "10.3.0" description: @@ -17,7 +15,6 @@ description: notes: - By default, it returns the private key value in PKCS#8 format, unless O(ssh_format=true) is passed. - The pluging works only for C(SSHKEY) type items. - - This plugin requires C(op) version 2 or later. options: _terms: description: Identifier(s) (case-insensitive UUID or name) of item(s) to retrieve. diff --git a/tests/unit/plugins/lookup/onepassword_common.py b/tests/unit/plugins/lookup/onepassword_common.py index be35a33788..a001309c01 100644 --- a/tests/unit/plugins/lookup/onepassword_common.py +++ b/tests/unit/plugins/lookup/onepassword_common.py @@ -8,7 +8,6 @@ import json import os from ansible_collections.community.general.plugins.lookup.onepassword import ( - OnePassCLIv1, OnePassCLIv2, ) @@ -20,26 +19,6 @@ def load_file(file): # Intentionally excludes metadata leaf nodes that would exist in real output if not relevant. MOCK_ENTRIES = { - OnePassCLIv1: [ - { - "vault_name": 'Acme "Quot\'d" Servers', - "queries": ["0123456789", 'Mock "Quot\'d" Server'], - "expected": ["t0pS3cret", "t0pS3cret"], - "output": load_file("v1_out_01.json"), - }, - { - "vault_name": "Acme Logins", - "queries": ["9876543210", "Mock Website", "acme.com"], - "expected": ["t0pS3cret", "t0pS3cret", "t0pS3cret"], - "output": load_file("v1_out_02.json"), - }, - { - "vault_name": "Acme Logins", - "queries": ["864201357"], - "expected": ["vauxhall"], - "output": load_file("v1_out_03.json"), - }, - ], OnePassCLIv2: [ { "vault_name": "Test Vault", diff --git a/tests/unit/plugins/lookup/test_onepassword.py b/tests/unit/plugins/lookup/test_onepassword.py index 4e48f2ee17..88addde485 100644 --- a/tests/unit/plugins/lookup/test_onepassword.py +++ b/tests/unit/plugins/lookup/test_onepassword.py @@ -13,73 +13,12 @@ from ansible.errors import AnsibleLookupError, AnsibleOptionsError from ansible.plugins.loader import lookup_loader from ansible_collections.community.general.plugins.lookup.onepassword import ( - OnePassCLIv1, OnePassCLIv2, ) from .onepassword_common import MOCK_ENTRIES -OP_VERSION_FIXTURES = ["opv1", "opv2"] - - -@pytest.mark.parametrize( - ("args", "rc", "expected_call_args", "expected_call_kwargs", "expected"), - ( - ( - [], - 0, - ["get", "account"], - {"ignore_errors": True}, - True, - ), - ( - [], - 1, - ["get", "account"], - {"ignore_errors": True}, - False, - ), - ( - ["acme"], - 1, - ["get", "account", "--account", "acme.1password.com"], - {"ignore_errors": True}, - False, - ), - ), -) -def test_assert_logged_in_v1(mocker, args, rc, expected_call_args, expected_call_kwargs, expected): - mocker.patch.object(OnePassCLIv1, "_run", return_value=[rc, "", ""]) - - op_cli = OnePassCLIv1(*args) - result = op_cli.assert_logged_in() - - op_cli._run.assert_called_with(expected_call_args, **expected_call_kwargs) - assert result == expected - - -def test_full_signin_v1(mocker): - mocker.patch.object(OnePassCLIv1, "_run", return_value=[0, "", ""]) - - op_cli = OnePassCLIv1( - subdomain="acme", - username="bob@acme.com", - secret_key="SECRET", - master_password="ONEKEYTORULETHEMALL", - ) - result = op_cli.full_signin() - - op_cli._run.assert_called_with( - [ - "signin", - "acme.1password.com", - b"bob@acme.com", - b"SECRET", - "--raw", - ], - command_input=b"ONEKEYTORULETHEMALL", - ) - assert result == [0, "", ""] +OP_VERSION_FIXTURES = ["opv2"] @pytest.mark.parametrize( @@ -153,10 +92,7 @@ def test_full_signin_v2(mocker): @pytest.mark.parametrize( ("version", "version_class"), - ( - ("1.17.2", OnePassCLIv1), - ("2.27.4", OnePassCLIv2), - ), + (("2.27.4", OnePassCLIv2),), ) def test_op_correct_cli_class(fake_op, version, version_class): op = fake_op(version) @@ -165,6 +101,8 @@ def test_op_correct_cli_class(fake_op, version, version_class): def test_op_unsupported_cli_version(fake_op): + with pytest.raises(AnsibleLookupError, match="is unsupported"): + fake_op("1.17.2") with pytest.raises(AnsibleLookupError, match="is unsupported"): fake_op("99.77.77") @@ -249,17 +187,6 @@ def test_op_assert_logged_in(mocker, login_status, op_fixture, request): op.set_token.assert_called_once() -@pytest.mark.parametrize("op_fixture", OP_VERSION_FIXTURES) -def test_op_get_raw_v1(mocker, op_fixture, request): - op = request.getfixturevalue(op_fixture) - mocker.patch.object(op._cli, "get_raw", return_value=[99, "RAW OUTPUT", ""]) - - result = op.get_raw("some item") - - assert result == "RAW OUTPUT" - op._cli.get_raw.assert_called_once() - - @pytest.mark.parametrize( ("op_fixture", "output", "expected"), ( @@ -354,16 +281,3 @@ def test_op_connect_partial_args(plugin, connect_host, connect_token, mocker): with pytest.raises(AnsibleOptionsError): op_lookup.run("login", vault_name="test vault", connect_host=connect_host, connect_token=connect_token) - - -@pytest.mark.parametrize( - ("kwargs"), - ( - {"connect_host": "http://localhost", "connect_token": "foobar"}, - {"service_account_token": "foobar"}, - ), -) -def test_opv1_unsupported_features(kwargs): - op_cli = OnePassCLIv1(**kwargs) - with pytest.raises(AnsibleLookupError): - op_cli.full_signin()