forked from ansible/ansible
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ability to unlock 1Password vault to lookup plugins (ansible#44923)
* Add ability to use login to 1Password vault to 1Password lookups * Adjust unit tests * Add changelog
- Loading branch information
Showing
4 changed files
with
81 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
minor_changes: | ||
- onepassword/onepassword_raw - accept subdomain and vault_password to allow Ansible to unlock 1Password vaults |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
author: | ||
- Scott Buchanan <[email protected]> | ||
- Andrew Zenk <[email protected]> | ||
- Sam Doran<[email protected]> | ||
version_added: "2.6" | ||
requirements: | ||
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/) | ||
|
@@ -25,31 +26,43 @@ | |
- onepassword wraps the C(op) command line utility to fetch specific field values from 1Password | ||
options: | ||
_terms: | ||
description: identifier(s) (UUID, name or domain; case-insensitive) of item(s) to retrieve | ||
description: identifier(s) (UUID, name, or subdomain; case-insensitive) of item(s) to retrieve | ||
required: True | ||
field: | ||
description: field to return from each matching item (case-insensitive) | ||
default: 'password' | ||
section: | ||
description: item section containing the field to retrieve (case-insensitive); if absent will return first match from any section | ||
default: None | ||
subdomain: | ||
description: The 1Password subdomain to authenticate against. | ||
default: None | ||
version_added: '2.7' | ||
vault: | ||
description: vault containing the item to retrieve (case-insensitive); if absent will search all vaults | ||
default: None | ||
vault_password: | ||
description: The password used to unlock the specified vault. | ||
default: None | ||
version_added: '2.7' | ||
""" | ||
|
||
EXAMPLES = """ | ||
- name: "retrieve password for KITT" | ||
- name: Retrieve password for KITT | ||
debug: | ||
msg: "{{ lookup('onepassword', 'KITT') }}" | ||
var: lookup('onepassword', 'KITT') | ||
- name: "retrieve password for Wintermute" | ||
- name: Retrieve password for Wintermute | ||
debug: | ||
msg: "{{ lookup('onepassword', 'Tessier-Ashpool', section='Wintermute') }}" | ||
var: lookup('onepassword', 'Tessier-Ashpool', section='Wintermute') | ||
- name: "retrieve username for HAL" | ||
- name: Retrieve username for HAL | ||
debug: | ||
msg: "{{ lookup('onepassword', 'HAL 9000', field='username', vault='Discovery') }}" | ||
var: lookup('onepassword', 'HAL 9000', field='username', vault='Discovery') | ||
- name: Retrieve password for HAL when not signed in to 1Password | ||
debug: | ||
var: lookup('onepassword', 'HAL 9000', subdomain='Discovery', vault_password='DmbslfLvasjdl') | ||
""" | ||
|
||
RETURN = """ | ||
|
@@ -64,45 +77,64 @@ | |
|
||
from ansible.plugins.lookup import LookupBase | ||
from ansible.errors import AnsibleLookupError | ||
from ansible.module_utils._text import to_bytes | ||
|
||
|
||
class OnePass(object): | ||
|
||
def __init__(self, path='op'): | ||
self._cli_path = path | ||
self._logged_in = False | ||
self._token = None | ||
self._subdomain = None | ||
self._vault_password = None | ||
|
||
@property | ||
def cli_path(self): | ||
return self._cli_path | ||
|
||
def get_token(self): | ||
if not self._subdomain and not self._vault_password: | ||
raise AnsibleLookupError('Both subdomain and password are required when logging in.') | ||
args = ['signin', self._subdomain, '--output=raw'] | ||
rc, out, err = self._run(args, command_input=to_bytes(self._vault_password)) | ||
self._token = out.strip() | ||
|
||
def assert_logged_in(self): | ||
try: | ||
self._run(["get", "account"]) | ||
rc, out, err = self._run(['get', 'account'], ignore_errors=True) | ||
if rc != 1: | ||
self._logged_in = True | ||
if not self._logged_in: | ||
self.get_token() | ||
except OSError as e: | ||
if e.errno == errno.ENOENT: | ||
raise AnsibleLookupError("1Password CLI tool not installed in path on control machine") | ||
raise e | ||
except AnsibleLookupError: | ||
raise AnsibleLookupError("Not logged into 1Password: please run 'op signin' first") | ||
raise AnsibleLookupError("Not logged into 1Password: please run 'op signin' first, or provide both subdomain and vault_password.") | ||
|
||
def get_raw(self, item_id, vault=None): | ||
args = ["get", "item", item_id] | ||
if vault is not None: | ||
args += ['--vault={0}'.format(vault)] | ||
output, dummy = self._run(args) | ||
if not self._logged_in: | ||
args += [to_bytes('--session=') + self._token] | ||
rc, output, dummy = self._run(args) | ||
return output | ||
|
||
def get_field(self, item_id, field, section=None, vault=None): | ||
output = self.get_raw(item_id, vault) | ||
return self._parse_field(output, field, section) if output != '' else '' | ||
|
||
def _run(self, args, expected_rc=0): | ||
p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE) | ||
out, err = p.communicate() | ||
def _run(self, args, expected_rc=0, command_input=None, ignore_errors=False): | ||
command = [self.cli_path] + args | ||
p = Popen(command, stdout=PIPE, stderr=PIPE, stdin=PIPE) | ||
out, err = p.communicate(input=command_input) | ||
rc = p.wait() | ||
if rc != expected_rc: | ||
if not ignore_errors and rc != expected_rc: | ||
raise AnsibleLookupError(err) | ||
return out, err | ||
return rc, out, err | ||
|
||
def _parse_field(self, data_json, field_name, section_title=None): | ||
data = json.loads(data_json) | ||
|
@@ -124,11 +156,13 @@ class LookupModule(LookupBase): | |
def run(self, terms, variables=None, **kwargs): | ||
op = OnePass() | ||
|
||
op.assert_logged_in() | ||
|
||
field = kwargs.get('field', 'password') | ||
section = kwargs.get('section') | ||
vault = kwargs.get('vault') | ||
op._subdomain = kwargs.get('subdomain') | ||
op._vault_password = kwargs.get('vault_password') | ||
|
||
op.assert_logged_in() | ||
|
||
values = [] | ||
for term in terms: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
author: | ||
- Scott Buchanan <[email protected]> | ||
- Andrew Zenk <[email protected]> | ||
- Sam Doran <[email protected]> | ||
version_added: "2.6" | ||
requirements: | ||
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/) | ||
|
@@ -27,15 +28,27 @@ | |
_terms: | ||
description: identifier(s) (UUID, name, or domain; case-insensitive) of item(s) to retrieve | ||
required: True | ||
subdomain: | ||
description: The 1Password subdomain to authenticate against. | ||
default: None | ||
version_added: '2.7' | ||
vault: | ||
description: vault containing the item to retrieve (case-insensitive); if absent will search all vaults | ||
default: None | ||
vault_password: | ||
description: The password used to unlock the specified vault. | ||
default: None | ||
version_added: '2.7' | ||
""" | ||
|
||
EXAMPLES = """ | ||
- name: "retrieve all data about Wintermute" | ||
- name: Retrieve all data about Wintermute | ||
debug: | ||
var: lookup('onepassword_raw', 'Wintermute') | ||
- name: Retrieve all data about Wintermute when not signed in to 1Password | ||
debug: | ||
msg: "{{ lookup('onepassword_raw', 'Wintermute') }}" | ||
var: lookup('onepassword_raw', 'Wintermute', subdomain='Turing', vault_password='DmbslfLvasjdl') | ||
""" | ||
|
||
RETURN = """ | ||
|
@@ -54,9 +67,11 @@ class LookupModule(LookupBase): | |
def run(self, terms, variables=None, **kwargs): | ||
op = OnePass() | ||
|
||
op.assert_logged_in() | ||
|
||
vault = kwargs.get('vault') | ||
op._subdomain = kwargs.get('subdomain') | ||
op._vault_password = kwargs.get('vault_password') | ||
|
||
op.assert_logged_in() | ||
|
||
values = [] | ||
for term in terms: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters