|
| 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 | + |
| 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 |
0 commit comments