1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Support 1Password Connect (#5588) (#7116)

* Support 1Password Connect (#5588)

- Support 1Password Connect with the opv2 client

* Follow pep8, be less explicit

* Update changelog to include PR

* 1Password Connect host and token are now also parameters

* Get argument values from the environment or lookup arguments

* Move imports

* Force using Connect token and host at the same time

* Update unit tests

* Update documentation

* Additional tests
This commit is contained in:
Xeryus Stokkel 2023-11-16 20:57:11 +01:00 committed by GitHub
parent 32fa588f47
commit f8652571f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 6 deletions

View file

@ -0,0 +1,3 @@
minor_changes:
- onepassword lookup plugin - support 1Password Connect with the opv2 client by setting the connect_host and connect_token parameters (https://github.com/ansible-collections/community.general/pull/7116).
- onepassword_raw lookup plugin - support 1Password Connect with the opv2 client by setting the connect_host and connect_token parameters (https://github.com/ansible-collections/community.general/pull/7116)

View file

@ -52,6 +52,18 @@ DOCUMENTATION = '''
- Only works with 1Password CLI version 2 or later. - Only works with 1Password CLI version 2 or later.
type: str type: str
version_added: 7.1.0 version_added: 7.1.0
connect_host:
description: The host for 1Password Connect. Must be used in combination with O(connect_token).
type: str
env:
- name: OP_CONNECT_HOST
version_added: 8.1.0
connect_token:
description: The token for 1Password Connect. Must be used in combination with O(connect_host).
type: str
env:
- name: OP_CONNECT_TOKEN
version_added: 8.1.0
vault: vault:
description: Vault containing the item to retrieve (case-insensitive). If absent will search all vaults. description: Vault containing the item to retrieve (case-insensitive). If absent will search all vaults.
notes: notes:
@ -119,7 +131,7 @@ import json
import subprocess import subprocess
from ansible.plugins.lookup import LookupBase from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleLookupError from ansible.errors import AnsibleLookupError, AnsibleOptionsError
from ansible.module_utils.common.process import get_bin_path from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils.common.text.converters import to_bytes, to_text from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible.module_utils.six import with_metaclass from ansible.module_utils.six import with_metaclass
@ -139,6 +151,8 @@ class OnePassCLIBase(with_metaclass(abc.ABCMeta, object)):
master_password=None, master_password=None,
service_account_token=None, service_account_token=None,
account_id=None, account_id=None,
connect_host=None,
connect_token=None,
): ):
self.subdomain = subdomain self.subdomain = subdomain
self.domain = domain self.domain = domain
@ -147,6 +161,8 @@ class OnePassCLIBase(with_metaclass(abc.ABCMeta, object)):
self.secret_key = secret_key self.secret_key = secret_key
self.service_account_token = service_account_token self.service_account_token = service_account_token
self.account_id = account_id self.account_id = account_id
self.connect_host = connect_host
self.connect_token = connect_token
self._path = None self._path = None
self._version = None self._version = None
@ -325,6 +341,10 @@ class OnePassCLIv1(OnePassCLIBase):
return not bool(rc) return not bool(rc)
def full_signin(self): 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: if self.service_account_token:
raise AnsibleLookupError( raise AnsibleLookupError(
"1Password CLI version 1 does not support Service Accounts. Please use version 2 or later.") "1Password CLI version 1 does not support Service Accounts. Please use version 2 or later.")
@ -510,6 +530,9 @@ class OnePassCLIv2(OnePassCLIBase):
return "" return ""
def assert_logged_in(self): def assert_logged_in(self):
if self.connect_host and self.connect_token:
return True
if self.service_account_token: if self.service_account_token:
args = ["whoami"] args = ["whoami"]
environment_update = {"OP_SERVICE_ACCOUNT_TOKEN": self.service_account_token} environment_update = {"OP_SERVICE_ACCOUNT_TOKEN": self.service_account_token}
@ -569,6 +592,15 @@ class OnePassCLIv2(OnePassCLIBase):
if vault is not None: if vault is not None:
args += ["--vault={0}".format(vault)] args += ["--vault={0}".format(vault)]
if self.connect_host and self.connect_token:
if vault is None:
raise AnsibleLookupError("'vault' is required with 1Password Connect")
environment_update = {
"OP_CONNECT_HOST": self.connect_host,
"OP_CONNECT_TOKEN": self.connect_token,
}
return self._run(args, environment_update=environment_update)
if self.service_account_token: if self.service_account_token:
if vault is None: if vault is None:
raise AnsibleLookupError("'vault' is required with 'service_account_token'") raise AnsibleLookupError("'vault' is required with 'service_account_token'")
@ -592,7 +624,7 @@ class OnePassCLIv2(OnePassCLIBase):
class OnePass(object): class OnePass(object):
def __init__(self, subdomain=None, domain="1password.com", username=None, secret_key=None, master_password=None, def __init__(self, subdomain=None, domain="1password.com", username=None, secret_key=None, master_password=None,
service_account_token=None, account_id=None): service_account_token=None, account_id=None, connect_host=None, connect_token=None):
self.subdomain = subdomain self.subdomain = subdomain
self.domain = domain self.domain = domain
self.username = username self.username = username
@ -600,6 +632,8 @@ class OnePass(object):
self.master_password = master_password self.master_password = master_password
self.service_account_token = service_account_token self.service_account_token = service_account_token
self.account_id = account_id self.account_id = account_id
self.connect_host = connect_host
self.connect_token = connect_token
self.logged_in = False self.logged_in = False
self.token = None self.token = None
@ -612,7 +646,8 @@ class OnePass(object):
for cls in OnePassCLIBase.__subclasses__(): for cls in OnePassCLIBase.__subclasses__():
if cls.supports_version == version.split(".")[0]: if cls.supports_version == version.split(".")[0]:
try: try:
return cls(self.subdomain, self.domain, self.username, self.secret_key, self.master_password, self.service_account_token, self.account_id) return cls(self.subdomain, self.domain, self.username, self.secret_key, self.master_password, self.service_account_token,
self.account_id, self.connect_host, self.connect_token)
except TypeError as e: except TypeError as e:
raise AnsibleLookupError(e) raise AnsibleLookupError(e)
@ -677,8 +712,13 @@ class LookupModule(LookupBase):
master_password = self.get_option("master_password") master_password = self.get_option("master_password")
service_account_token = self.get_option("service_account_token") service_account_token = self.get_option("service_account_token")
account_id = self.get_option("account_id") account_id = self.get_option("account_id")
connect_host = self.get_option("connect_host")
connect_token = self.get_option("connect_token")
op = OnePass(subdomain, domain, username, secret_key, master_password, service_account_token, account_id) if (connect_host or connect_token) and None in (connect_host, connect_token):
raise AnsibleOptionsError("connect_host and connect_token are required together")
op = OnePass(subdomain, domain, username, secret_key, master_password, service_account_token, account_id, connect_host, connect_token)
op.assert_logged_in() op.assert_logged_in()
values = [] values = []

View file

@ -49,6 +49,18 @@ DOCUMENTATION = '''
- Only works with 1Password CLI version 2 or later. - Only works with 1Password CLI version 2 or later.
type: string type: string
version_added: 7.1.0 version_added: 7.1.0
connect_host:
description: The host for 1Password Connect. Must be used in combination with O(connect_token).
type: str
env:
- name: OP_CONNECT_HOST
version_added: 8.1.0
connect_token:
description: The token for 1Password Connect. Must be used in combination with O(connect_host).
type: str
env:
- name: OP_CONNECT_TOKEN
version_added: 8.1.0
vault: vault:
description: Vault containing the item to retrieve (case-insensitive). If absent will search all vaults. description: Vault containing the item to retrieve (case-insensitive). If absent will search all vaults.
notes: notes:
@ -86,6 +98,7 @@ RETURN = """
import json import json
from ansible_collections.community.general.plugins.lookup.onepassword import OnePass from ansible_collections.community.general.plugins.lookup.onepassword import OnePass
from ansible.errors import AnsibleOptionsError
from ansible.plugins.lookup import LookupBase from ansible.plugins.lookup import LookupBase
@ -102,8 +115,13 @@ class LookupModule(LookupBase):
master_password = self.get_option("master_password") master_password = self.get_option("master_password")
service_account_token = self.get_option("service_account_token") service_account_token = self.get_option("service_account_token")
account_id = self.get_option("account_id") account_id = self.get_option("account_id")
connect_host = self.get_option("connect_host")
connect_token = self.get_option("connect_token")
op = OnePass(subdomain, domain, username, secret_key, master_password, service_account_token, account_id) if (connect_host or connect_token) and None in (connect_host, connect_token):
raise AnsibleOptionsError("connect_host and connect_token are required together")
op = OnePass(subdomain, domain, username, secret_key, master_password, service_account_token, account_id, connect_host, connect_token)
op.assert_logged_in() op.assert_logged_in()
values = [] values = []

View file

@ -18,7 +18,7 @@ from .onepassword_conftest import ( # noqa: F401, pylint: disable=unused-import
) )
from .onepassword_common import MOCK_ENTRIES from .onepassword_common import MOCK_ENTRIES
from ansible.errors import AnsibleLookupError from ansible.errors import AnsibleLookupError, AnsibleOptionsError
from ansible.plugins.loader import lookup_loader from ansible.plugins.loader import lookup_loader
from ansible_collections.community.general.plugins.lookup.onepassword import ( from ansible_collections.community.general.plugins.lookup.onepassword import (
OnePassCLIv1, OnePassCLIv1,
@ -82,6 +82,12 @@ def test_assert_logged_in_v2(mocker, args, out, expected_call_args, expected_cal
assert result == expected assert result == expected
def test_assert_logged_in_v2_connect():
op_cli = OnePassCLIv2(connect_host="http://localhost:8080", connect_token="foobar")
result = op_cli.assert_logged_in()
assert result
def test_full_signin_v2(mocker): def test_full_signin_v2(mocker):
mocker.patch.object(OnePassCLIv2, "_run", return_value=[0, "", ""]) mocker.patch.object(OnePassCLIv2, "_run", return_value=[0, "", ""])
@ -266,3 +272,34 @@ def test_signin(op_fixture, request):
op._cli.signin() op._cli.signin()
print(op._cli.version) print(op._cli.version)
op._cli._run.assert_called_once_with(['signin', '--raw'], command_input=b"master_pass") op._cli._run.assert_called_once_with(['signin', '--raw'], command_input=b"master_pass")
@pytest.mark.parametrize(
("plugin", "connect_host", "connect_token"),
[
(plugin, connect_host, connect_token)
for plugin in ("community.general.onepassword", "community.general.onepassword_raw")
for (connect_host, connect_token) in
(
("http://localhost", None),
(None, "foobar"),
)
]
)
def test_op_connect_partial_args(plugin, connect_host, connect_token):
op_lookup = lookup_loader.get(plugin)
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()