mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
* 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:
parent
32fa588f47
commit
f8652571f7
4 changed files with 104 additions and 6 deletions
3
changelogs/fragments/5588-support-1password-connect.yml
Normal file
3
changelogs/fragments/5588-support-1password-connect.yml
Normal 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)
|
|
@ -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 = []
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue