Skip to content

Commit b12cf75

Browse files
scottsbsamdoran
authored andcommitted
1Password lookup plugin (ansible#37207)
* add pytest_cache to gitignore * onepassword lookup plugin * fix linter/style test complaints * second pass at making pycodestyle happy * use json module instead of jq * update copyrights, license & version added * fix python2 compatibility * doh. fix spacing issue. * use standard ansible exception * remove potentially problematic stdin argument * actually call assertion method * add support for top-level fields * make vault uuids pedantically consistent in fixture * fix new style issues * ability specify section & correct case handling * improve error handling * add onepassword_raw plugin * Add maintainer info * Move common code to module_utils/onepassword.py * Load raw data JSON data for easier use in Ansible * Put OnePass class back inside lookup plugin There is no good place for sharing code across lookups currently. * Remove debugging code in unit tests * Patche proper module in raw unit tests * Add changelog entry Co-authored-by: Scott Buchanan <[email protected]>
1 parent 7e20877 commit b12cf75

File tree

6 files changed

+525
-0
lines changed

6 files changed

+525
-0
lines changed

.github/BOTMETA.yml

+8
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,8 @@ files:
903903
$module_utils/network:
904904
maintainers: $team_networking
905905
labels: networking
906+
$module_utils/onepassword.py:
907+
maintainers: samdoran
906908
lib/ansible/playbook/handler.py:
907909
keywords:
908910
- handlers
@@ -1042,6 +1044,12 @@ files:
10421044
lib/ansible/plugins/netconf/:
10431045
maintainers: $team_networking
10441046
labels: networking
1047+
lib/ansible/plugins/lookup/onepassword.py:
1048+
maintainers: samdoran
1049+
ignored: azenk
1050+
lib/ansible/plugins/lookup/onepassword_raw.py:
1051+
maintainers: samdoran
1052+
ignored: azenk
10451053
lib/ansible/plugins/shell/powershell.py:
10461054
maintainers: $team_windows_core
10471055
labels:

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ credentials.yml
5757
# test output
5858
*.retry
5959
*.out
60+
.pytest_cache/
6061
.tox
6162
.cache
6263
.pytest_cache
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
features:
2+
- onepassword lookup - add lookup plugins onepassword and onepassword_raw to retrieve secrets from 1Password vault (https://github.com/ansible/ansible/pull/37207)
+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# -*- coding: utf-8 -*-
2+
# (c) 2018, Scott Buchanan <[email protected]>
3+
# (c) 2016, Andrew Zenk <[email protected]> (lastpass.py used as starting point)
4+
# (c) 2018, Ansible Project
5+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
7+
from __future__ import (absolute_import, division, print_function)
8+
__metaclass__ = type
9+
10+
ANSIBLE_METADATA = {'metadata_version': '1.1',
11+
'status': ['preview'],
12+
'supported_by': 'community'}
13+
14+
DOCUMENTATION = """
15+
lookup: onepassword
16+
author:
17+
- Scott Buchanan <[email protected]>
18+
- Andrew Zenk <[email protected]>
19+
version_added: "2.6"
20+
requirements:
21+
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/)
22+
- must have already logged into 1Password using C(op) CLI
23+
short_description: fetch field values from 1Password
24+
description:
25+
- onepassword wraps the C(op) command line utility to fetch specific field values from 1Password
26+
options:
27+
_terms:
28+
description: identifier(s) (UUID, name or domain; case-insensitive) of item(s) to retrieve
29+
required: True
30+
field:
31+
description: field to return from each matching item (case-insensitive)
32+
default: 'password'
33+
section:
34+
description: item section containing the field to retrieve (case-insensitive); if absent will return first match from any section
35+
default: None
36+
vault:
37+
description: vault containing the item to retrieve (case-insensitive); if absent will search all vaults
38+
default: None
39+
"""
40+
41+
EXAMPLES = """
42+
- name: "retrieve password for KITT"
43+
debug:
44+
msg: "{{ lookup('onepassword', 'KITT') }}"
45+
46+
- name: "retrieve password for Wintermute"
47+
debug:
48+
msg: "{{ lookup('onepassword', 'Tessier-Ashpool', section='Wintermute') }}"
49+
50+
- name: "retrieve username for HAL"
51+
debug:
52+
msg: "{{ lookup('onepassword', 'HAL 9000', field='username', vault='Discovery') }}"
53+
"""
54+
55+
RETURN = """
56+
_raw:
57+
description: field data requested
58+
"""
59+
60+
import json
61+
import errno
62+
63+
from subprocess import Popen, PIPE
64+
65+
from ansible.plugins.lookup import LookupBase
66+
from ansible.errors import AnsibleLookupError
67+
68+
69+
class OnePass(object):
70+
71+
def __init__(self, path='op'):
72+
self._cli_path = path
73+
74+
@property
75+
def cli_path(self):
76+
return self._cli_path
77+
78+
def assert_logged_in(self):
79+
try:
80+
self._run(["get", "account"])
81+
except OSError as e:
82+
if e.errno == errno.ENOENT:
83+
raise AnsibleLookupError("1Password CLI tool not installed in path on control machine")
84+
raise e
85+
except AnsibleLookupError:
86+
raise AnsibleLookupError("Not logged into 1Password: please run 'op signin' first")
87+
88+
def get_raw(self, item_id, vault=None):
89+
args = ["get", "item", item_id]
90+
if vault is not None:
91+
args += ['--vault={0}'.format(vault)]
92+
output, dummy = self._run(args)
93+
return output
94+
95+
def get_field(self, item_id, field, section=None, vault=None):
96+
output = self.get_raw(item_id, vault)
97+
return self._parse_field(output, field, section) if output != '' else ''
98+
99+
def _run(self, args, expected_rc=0):
100+
p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE)
101+
out, err = p.communicate()
102+
rc = p.wait()
103+
if rc != expected_rc:
104+
raise AnsibleLookupError(err)
105+
return out, err
106+
107+
def _parse_field(self, data_json, field_name, section_title=None):
108+
data = json.loads(data_json)
109+
if section_title is None:
110+
for field_data in data['details'].get('fields', []):
111+
if field_data.get('name').lower() == field_name.lower():
112+
return field_data.get('value', '')
113+
for section_data in data['details'].get('sections', []):
114+
if section_title is not None and section_title.lower() != section_data['title'].lower():
115+
continue
116+
for field_data in section_data.get('fields', []):
117+
if field_data.get('t').lower() == field_name.lower():
118+
return field_data.get('v', '')
119+
return ''
120+
121+
122+
class LookupModule(LookupBase):
123+
124+
def run(self, terms, variables=None, **kwargs):
125+
op = OnePass()
126+
127+
op.assert_logged_in()
128+
129+
field = kwargs.get('field', 'password')
130+
section = kwargs.get('section')
131+
vault = kwargs.get('vault')
132+
133+
values = []
134+
for term in terms:
135+
values.append(op.get_field(term, field, section, vault))
136+
return values
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -*- coding: utf-8 -*-
2+
# (c) 2018, Scott Buchanan <[email protected]>
3+
# (c) 2016, Andrew Zenk <[email protected]> (lastpass.py used as starting point)
4+
# (c) 2018, Ansible Project
5+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
7+
from __future__ import (absolute_import, division, print_function)
8+
__metaclass__ = type
9+
10+
ANSIBLE_METADATA = {'metadata_version': '1.1',
11+
'status': ['preview'],
12+
'supported_by': 'community'}
13+
14+
DOCUMENTATION = """
15+
lookup: onepassword_raw
16+
author:
17+
- Scott Buchanan <[email protected]>
18+
- Andrew Zenk <[email protected]>
19+
version_added: "2.6"
20+
requirements:
21+
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/)
22+
- must have already logged into 1Password using op CLI
23+
short_description: fetch raw json data from 1Password
24+
description:
25+
- onepassword_raw wraps C(op) command line utility to fetch an entire item from 1Password
26+
options:
27+
_terms:
28+
description: identifier(s) (UUID, name, or domain; case-insensitive) of item(s) to retrieve
29+
required: True
30+
vault:
31+
description: vault containing the item to retrieve (case-insensitive); if absent will search all vaults
32+
default: None
33+
"""
34+
35+
EXAMPLES = """
36+
- name: "retrieve all data about Wintermute"
37+
debug:
38+
msg: "{{ lookup('onepassword_raw', 'Wintermute') }}"
39+
"""
40+
41+
RETURN = """
42+
_raw:
43+
description: field data requested
44+
"""
45+
46+
import json
47+
48+
from ansible.plugins.lookup.onepassword import OnePass
49+
from ansible.plugins.lookup import LookupBase
50+
51+
52+
class LookupModule(LookupBase):
53+
54+
def run(self, terms, variables=None, **kwargs):
55+
op = OnePass()
56+
57+
op.assert_logged_in()
58+
59+
vault = kwargs.get('vault')
60+
61+
values = []
62+
for term in terms:
63+
data = json.loads(op.get_raw(term, vault))
64+
values.append(data)
65+
return values

0 commit comments

Comments
 (0)