Skip to content

Commit

Permalink
1Password lookup plugin (ansible#37207)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
2 people authored and samdoran committed May 17, 2018
1 parent 7e20877 commit b12cf75
Show file tree
Hide file tree
Showing 6 changed files with 525 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,8 @@ files:
$module_utils/network:
maintainers: $team_networking
labels: networking
$module_utils/onepassword.py:
maintainers: samdoran
lib/ansible/playbook/handler.py:
keywords:
- handlers
Expand Down Expand Up @@ -1042,6 +1044,12 @@ files:
lib/ansible/plugins/netconf/:
maintainers: $team_networking
labels: networking
lib/ansible/plugins/lookup/onepassword.py:
maintainers: samdoran
ignored: azenk
lib/ansible/plugins/lookup/onepassword_raw.py:
maintainers: samdoran
ignored: azenk
lib/ansible/plugins/shell/powershell.py:
maintainers: $team_windows_core
labels:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ credentials.yml
# test output
*.retry
*.out
.pytest_cache/
.tox
.cache
.pytest_cache
Expand Down
2 changes: 2 additions & 0 deletions changelogs/fragments/onepassword-lookup.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
features:
- onepassword lookup - add lookup plugins onepassword and onepassword_raw to retrieve secrets from 1Password vault (https://github.com/ansible/ansible/pull/37207)
136 changes: 136 additions & 0 deletions lib/ansible/plugins/lookup/onepassword.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# (c) 2018, Scott Buchanan <[email protected]>
# (c) 2016, Andrew Zenk <[email protected]> (lastpass.py used as starting point)
# (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}

DOCUMENTATION = """
lookup: onepassword
author:
- Scott Buchanan <[email protected]>
- Andrew Zenk <[email protected]>
version_added: "2.6"
requirements:
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/)
- must have already logged into 1Password using C(op) CLI
short_description: fetch field values from 1Password
description:
- 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
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
vault:
description: vault containing the item to retrieve (case-insensitive); if absent will search all vaults
default: None
"""

EXAMPLES = """
- name: "retrieve password for KITT"
debug:
msg: "{{ lookup('onepassword', 'KITT') }}"
- name: "retrieve password for Wintermute"
debug:
msg: "{{ lookup('onepassword', 'Tessier-Ashpool', section='Wintermute') }}"
- name: "retrieve username for HAL"
debug:
msg: "{{ lookup('onepassword', 'HAL 9000', field='username', vault='Discovery') }}"
"""

RETURN = """
_raw:
description: field data requested
"""

import json
import errno

from subprocess import Popen, PIPE

from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleLookupError


class OnePass(object):

def __init__(self, path='op'):
self._cli_path = path

@property
def cli_path(self):
return self._cli_path

def assert_logged_in(self):
try:
self._run(["get", "account"])
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")

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)
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()
rc = p.wait()
if rc != expected_rc:
raise AnsibleLookupError(err)
return out, err

def _parse_field(self, data_json, field_name, section_title=None):
data = json.loads(data_json)
if section_title is None:
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 ''


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')

values = []
for term in terms:
values.append(op.get_field(term, field, section, vault))
return values
65 changes: 65 additions & 0 deletions lib/ansible/plugins/lookup/onepassword_raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# (c) 2018, Scott Buchanan <[email protected]>
# (c) 2016, Andrew Zenk <[email protected]> (lastpass.py used as starting point)
# (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}

DOCUMENTATION = """
lookup: onepassword_raw
author:
- Scott Buchanan <[email protected]>
- Andrew Zenk <[email protected]>
version_added: "2.6"
requirements:
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/)
- must have already logged into 1Password using op CLI
short_description: fetch raw json data from 1Password
description:
- onepassword_raw wraps C(op) command line utility to fetch an entire item from 1Password
options:
_terms:
description: identifier(s) (UUID, name, or domain; case-insensitive) of item(s) to retrieve
required: True
vault:
description: vault containing the item to retrieve (case-insensitive); if absent will search all vaults
default: None
"""

EXAMPLES = """
- name: "retrieve all data about Wintermute"
debug:
msg: "{{ lookup('onepassword_raw', 'Wintermute') }}"
"""

RETURN = """
_raw:
description: field data requested
"""

import json

from ansible.plugins.lookup.onepassword import OnePass
from ansible.plugins.lookup import LookupBase


class LookupModule(LookupBase):

def run(self, terms, variables=None, **kwargs):
op = OnePass()

op.assert_logged_in()

vault = kwargs.get('vault')

values = []
for term in terms:
data = json.loads(op.get_raw(term, vault))
values.append(data)
return values
Loading

0 comments on commit b12cf75

Please sign in to comment.