diff --git a/lib/ansible/modules/identity/onepassword_facts.py b/lib/ansible/modules/identity/onepassword_facts.py index 0b285d6989..71e2efaf05 100644 --- a/lib/ansible/modules/identity/onepassword_facts.py +++ b/lib/ansible/modules/identity/onepassword_facts.py @@ -22,12 +22,16 @@ author: - Ryan Conway (@rylon) version_added: "2.7" requirements: - - C(op) 1Password command line utility (v0.5.1). See U(https://support.1password.com/command-line/) + - C(op) 1Password command line utility. See U(https://support.1password.com/command-line/) notes: - "Based on the C(onepassword) lookup plugin by Scott Buchanan ." -short_description: Fetch facts from 1Password items + - This module stores potentially sensitive data from 1Password as Ansible facts. + Facts are subject to caching if enabled, which means this data could be stored in clear text + on disk or in a database. + - Tested with C(op) version 0.5.3 +short_description: Gather items from 1Password and set them as facts description: - - M(onepassword_facts) wraps the C(op) command line utility to fetch data about one or more 1password items and return as Ansible facts. + - M(onepassword_facts) wraps the C(op) command line utility to fetch data about one or more 1Password items and return as Ansible facts. - A fatal error occurs if any of the items being searched for can not be found. - Recommend using with the C(no_log) option to avoid logging the values of the secrets being retrieved. options: @@ -53,22 +57,28 @@ options: required: True auto_login: description: - - A dictionary containing authentication details. If this is set, M(onepassword_facts) will attempt to login to 1password automatically. - - The required values can be stored in Ansible Vault, and passed to the module securely that way. + - A dictionary containing authentication details. If this is set, M(onepassword_facts) will attempt to sign in to 1Password automatically. - Without this option, you must have already logged in via the 1Password CLI before running Ansible. + - It is B(highly) recommened to store 1Password credentials in an Ansible Vault. Ensure that the key used to encrypt + the Ansible Vault is equal to or greater in strength than the 1Password master password. suboptions: - account: + subdomain: description: - - 1Password account name (.1password.com). + - 1Password subdomain name (.1password.com). + - If this is not specified, the most recent subdomain will be used. username: description: - 1Password username. - masterpassword: + - Only required for initial sign in. + master_password: description: - - The master password for your user. - secretkey: + - The master password for your subdomain. + - This is always required when specifying C(auto_login). + required: True + secret_key: description: - - The secret key for your user. + - The secret key for your subdomain. + - Only required for initial sign in. default: {} required: False cli_path: @@ -82,8 +92,8 @@ EXAMPLES = ''' - name: Get a password onepassword_facts: search_terms: My 1Password item - delegate_to: local - no_log: true # Don't want to log the secrets to the console! + delegate_to: localhost + no_log: true # Don't want to log the secrets to the console! # Gather secrets from 1Password, with more advanced search terms: - name: Get a password @@ -93,8 +103,8 @@ EXAMPLES = ''' field: Custom field name # optional, defaults to 'password' section: Custom section name # optional, defaults to 'None' vault: Name of the vault # optional, only necessary if there is more than 1 Vault available - delegate_to: local - no_log: true # Don't want to log the secrets to the console! + delegate_to: localhost + no_log: True # Don't want to log the secrets to the console! # Gather secrets combining simple and advanced search terms to retrieve two items, one of which we fetch two # fields. In the first 'password' is fetched, as a field name is not specified (default behaviour) and in the @@ -109,8 +119,8 @@ EXAMPLES = ''' section: Custom section name # optional, defaults to 'None' vault: Name of the vault # optional, only necessary if there is more than 1 Vault available - name: A 1Password item with document attachment - delegate_to: local - no_log: true # Don't want to log the secrets to the console! + delegate_to: localhost + no_log: true # Don't want to log the secrets to the console! ''' RETURN = ''' @@ -136,126 +146,41 @@ import errno import json import os import re + from subprocess import Popen, PIPE +from ansible.module_utils._text import to_bytes, to_native from ansible.module_utils.basic import AnsibleModule +class AnsibleModuleError(Exception): + def __init__(self, results): + self.results = results + + def __repr__(self): + return self.results + + class OnePasswordFacts(object): def __init__(self): self.cli_path = module.params.get('cli_path') + self.config_file_path = '~/.op/config' self.auto_login = module.params.get('auto_login') - self.token = {} + self.logged_in = False + self.token = None terms = module.params.get('search_terms') - self.terms = self.parse_search_terms(module.params.get('search_terms')) - - def parse_search_terms(self, terms): - processed_terms = [] - - for term in terms: - if not isinstance(term, dict): - term = {'name': term} - - if 'name' not in term: - module.fail_json(msg="Missing required 'name' field from search term, got: '%s'" % str(term)) - - term['field'] = term.get('field', 'password') - term['section'] = term.get('section', None) - term['vault'] = term.get('vault', None) - - processed_terms.append(term) - - return processed_terms - - def run(self): - result = {} - - self.assert_logged_in() - - for term in self.terms: - value = self.get_field(term['name'], term['field'], term['section'], term['vault']) - - if term['name'] in result: - # If we already have a result for this key, we have to append this result dictionary - # to the existing one. This is only applicable when there is a single item - # in 1Password which has two different fields, and we want to retrieve both of them. - result[term['name']].update(value) - else: - # If this is the first result for this key, simply set it. - result[term['name']] = value - - return result - - def assert_logged_in(self): - try: - self._run(["get", "account"]) - - except OSError as e: - if e.errno == errno.ENOENT: - module.fail_json(msg="1Password CLI tool not installed in path '%s': %s" % (self.cli_path, e)) - else: - module.fail_json(msg="1Password CLI tool failed to execute at path '%s': %s" % (self.cli_path, e)) - - except Exception as e: - # 1Password's CLI doesn't seem to return different error codes, so we need to handle a few of the common - # error cases by searching via regex, so we can provide a clearer error message to the user. - if re.search(".*You are not currently signed in.*", str(e)) is not None: - if (self.auto_login is not None): - try: - token = self._run([ - "signin", "%s.1password.com" % self.auto_login['account'], - self.auto_login['username'], - self.auto_login['secretkey'], - self.auto_login['masterpassword'], - "--shorthand=ansible_%s" % self.auto_login['account'], - "--output=raw" - ]) - self.token = {'OP_SESSION_ansible_%s' % self.auto_login['account']: token[0].strip()} - - except Exception as e: - module.fail_json(msg="Unable to automatically login to 1Password: %s " % e) - else: - module.fail_json(msg=( - "Not logged into 1Password: please run '%s signin' first, or see the module docs for " - "how to configure for automatic login." % self.cli_path) - ) - - def get_raw(self, item_id, vault=None): - try: - args = ["get", "item", item_id] - if vault is not None: - args += ['--vault={0}'.format(vault)] - output, dummy = self._run(args) - return output - - except Exception as e: - if re.search(".*not found.*", str(e)): - module.fail_json(msg="Unable to find an item in 1Password named '%s'." % item_id) - else: - module.fail_json(msg="Unexpected error attempting to find an item in 1Password named '%s': %s" % (item_id, e)) - - def get_field(self, item_id, field, section=None, vault=None): - output = self.get_raw(item_id, vault) - return self._parse_field(output, item_id, field, section) if output != '' else '' - - def _run(self, args, expected_rc=0): - # Duplicates the current shell environment before running 'op', so we get the same PATH the user has, - # but we merge in the auth token dictionary, allowing the auto-login functionality to work (if enabled). - env = {} - env.update(os.environ.copy()) - env.update(self.token) - - p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env) - out, err = p.communicate() + self.terms = self.parse_search_terms(terms) + 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: - raise Exception(err) - - return out, err + if not ignore_errors and rc != expected_rc: + raise AnsibleModuleError(to_native(err)) + return rc, out, err def _parse_field(self, data_json, item_id, field_name, section_title=None): data = json.loads(data_json) @@ -286,6 +211,129 @@ class OnePasswordFacts(object): optional_section_title = '' if section_title is None else " in the section '%s'" % section_title module.fail_json(msg="Unable to find an item in 1Password named '%s' with the field '%s'%s." % (item_id, field_name, optional_section_title)) + def parse_search_terms(self, terms): + processed_terms = [] + + for term in terms: + if not isinstance(term, dict): + term = {'name': term} + + if 'name' not in term: + module.fail_json(msg="Missing required 'name' field from search term, got: '%s'" % to_native(term)) + + term['field'] = term.get('field', 'password') + term['section'] = term.get('section', None) + term['vault'] = term.get('vault', None) + + processed_terms.append(term) + + return processed_terms + + def get_raw(self, item_id, vault=None): + try: + args = ["get", "item", item_id] + if vault is not None: + args += ['--vault={0}'.format(vault)] + if not self.logged_in: + args += [to_bytes('--session=') + self.token] + rc, output, dummy = self._run(args) + return output + + except Exception as e: + if re.search(".*not found.*", to_native(e)): + module.fail_json(msg="Unable to find an item in 1Password named '%s'." % item_id) + else: + module.fail_json(msg="Unexpected error attempting to find an item in 1Password named '%s': %s" % (item_id, to_native(e))) + + def get_field(self, item_id, field, section=None, vault=None): + output = self.get_raw(item_id, vault) + return self._parse_field(output, item_id, field, section) if output != '' else '' + + def full_login(self): + if self.auto_login is not None: + if None in [self.auto_login.get('subdomain'), self.auto_login.get('username'), + self.auto_login.get('secret_key'), self.auto_login.get('master_password')]: + module.fail_json(msg='Unable to perform initial sign in to 1Password. ' + 'subdomain, username, secret_key, and master_password are required to perform initial sign in.') + + args = [ + 'signin', + '{0}.1password.com'.format(self.auto_login['subdomain']), + to_bytes(self.auto_login['username']), + to_bytes(self.auto_login['secret_key']), + '--output=raw', + ] + + try: + rc, out, err = self._run(args, command_input=to_bytes(self.auto_login['master_password'])) + self.token = out.strip() + except AnsibleModuleError as e: + module.fail_json(msg="Failed to perform initial sign in to 1Password: %s" % to_native(e)) + else: + module.fail_json(msg="Unable to perform an initial sign in to 1Password. Please run '%s sigin' " + "or define credentials in 'auto_login'. See the module documentation for details." % self.cli_path) + + def get_token(self): + # If the config file exists, assume an initial signin has taken place and try basic sign in + if os.path.isfile(self.config_file_path): + + if self.auto_login is not None: + + # Since we are not currently signed in, master_password is required at a minimum + if not self.auto_login.get('master_password'): + module.fail_json(msg="Unable to sign in to 1Password. 'auto_login.master_password' is required.") + + # Try signing in using the master_password and a subdomain if one is provided + try: + args = ['signin', '--output=raw'] + + if self.auto_login.get('subdomain'): + args = ['signin', self.auto_login['subdomain'], '--output=raw'] + + rc, out, err = self._run(args, command_input=to_bytes(self.auto_login['master_password'])) + self.token = out.strip() + + except AnsibleModuleError: + self.full_login() + + else: + self.full_login() + + else: + # Attempt a full sign in since there appears to be no existing sign in + self.full_login() + + def assert_logged_in(self): + try: + rc, out, err = self._run(['get', 'account'], ignore_errors=True) + if rc == 0: + self.logged_in = True + if not self.logged_in: + self.get_token() + except OSError as e: + if e.errno == errno.ENOENT: + module.fail_json(msg="1Password CLI tool '%s' not installed in path on control machine" % self.cli_path) + raise e + + def run(self): + result = {} + + self.assert_logged_in() + + for term in self.terms: + value = self.get_field(term['name'], term['field'], term['section'], term['vault']) + + if term['name'] in result: + # If we already have a result for this key, we have to append this result dictionary + # to the existing one. This is only applicable when there is a single item + # in 1Password which has two different fields, and we want to retrieve both of them. + result[term['name']].update(value) + else: + # If this is the first result for this key, simply set it. + result[term['name']] = value + + return result + def main(): global module @@ -293,10 +341,10 @@ def main(): argument_spec=dict( cli_path=dict(type='path', default='op'), auto_login=dict(type='dict', options=dict( - account=dict(required=True, type='str'), - username=dict(required=True, type='str'), - masterpassword=dict(required=True, type='str'), - secretkey=dict(required=True, type='str'), + subdomain=dict(type='str'), + username=dict(type='str'), + master_password=dict(required=True, type='str', no_log=True), + secret_key=dict(type='str', no_log=True), ), default=None), search_terms=dict(required=True, type='list') ), diff --git a/lib/ansible/plugins/lookup/onepassword.py b/lib/ansible/plugins/lookup/onepassword.py index 5e9aebc5f4..c11f4d3c82 100644 --- a/lib/ansible/plugins/lookup/onepassword.py +++ b/lib/ansible/plugins/lookup/onepassword.py @@ -20,10 +20,9 @@ DOCUMENTATION = """ 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 + - C(onepassword) wraps the C(op) command line utility to fetch specific field values from 1Password. options: _terms: description: identifier(s) (UUID, name, or subdomain; case-insensitive) of item(s) to retrieve @@ -31,38 +30,70 @@ DOCUMENTATION = """ field: description: field to return from each matching item (case-insensitive) default: 'password' + master_password: + description: The password used to unlock the specified vault. + default: None + version_added: '2.7' + aliases: ['vault_password'] section: - description: item section containing the field to retrieve (case-insensitive); if absent will return first match from any 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 + username: + description: The username used to sign in. version_added: '2.7' + secret_key: + description: The secret key used when performing an initial sign in. + version_added: '2.7' + vault: + description: Vault containing the item to retrieve (case-insensitive). If absent will search all vaults + default: None + notes: + - This lookup will use an existing 1Password session if one exists. If not, and you have already + performed an initial sign in (meaning C(~/.op/config exists)), then only the C(master_password) is required. + You may optionally specify C(subdomain) in this scenario, otherwise the last used subdomain will be used by C(op). + - This lookup can perform an initial login by providing C(subdomain), C(username), C(secret_key), and C(master_password). + - Due to the B(very) sensitive nature of these credentials, it is B(highly) recommeneded that you only pass in the minial credentials + needed at any given time. Also, store these credentials in an Ansible Vault using a key that is equal to or greater in strength + to the 1Password master password. + - This lookup stores potentially sensitive data from 1Password as Ansible facts. + Facts are subject to caching if enabled, which means this data could be stored in clear text + on disk or in a database. + - Tested with C(op) version 0.5.3 """ EXAMPLES = """ -- name: Retrieve password for KITT +# These examples only work when already signed in to 1Password +- name: Retrieve password for KITT when already signed in to 1Password debug: var: lookup('onepassword', 'KITT') -- name: Retrieve password for Wintermute +- name: Retrieve password for Wintermute when already signed in to 1Password debug: var: lookup('onepassword', 'Tessier-Ashpool', section='Wintermute') -- name: Retrieve username for HAL +- name: Retrieve username for HAL when already signed in to 1Password debug: 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') + var: lookup('onepassword' + 'HAL 9000' + subdomain='Discovery' + master_password=vault_master_password) + +- name: Retrieve password for HAL when never signed in to 1Password + debug: + var: lookup('onepassword' + 'HAL 9000' + subdomain='Discovery' + master_password=vault_master_password + username='tweety@acme.com' + secret_key=vault_secret_key) """ RETURN = """ @@ -70,56 +101,70 @@ RETURN = """ description: field data requested """ -import json import errno +import json +import os from subprocess import Popen, PIPE from ansible.plugins.lookup import LookupBase from ansible.errors import AnsibleLookupError -from ansible.module_utils._text import to_bytes +from ansible.module_utils._text import to_bytes, to_text 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 + self.cli_path = path + self.config_file_path = os.path.expanduser('~/.op/config') + self.logged_in = False + self.token = None + self.subdomain = None + self.username = None + self.secret_key = None + self.master_password = None 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() + # If the config file exists, assume an initial signin has taken place and try basic sign in + if os.path.isfile(self.config_file_path): + + if not self.master_password: + raise AnsibleLookupError('Unable to sign in to 1Password. master_password is required.') + + try: + args = ['signin', '--output=raw'] + + if self.subdomain: + args = ['signin', self.subdomain, '--output=raw'] + + rc, out, err = self._run(args, command_input=to_bytes(self.master_password)) + self.token = out.strip() + + except AnsibleLookupError: + self.full_login() + + else: + # Attempt a full sign in since there appears to be no existing sign in + self.full_login() def assert_logged_in(self): try: rc, out, err = self._run(['get', 'account'], ignore_errors=True) - if rc != 1: - self._logged_in = True - if not self._logged_in: + if rc == 0: + 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 AnsibleLookupError("1Password CLI tool '%s' not installed in path on control machine" % self.cli_path) raise e - except AnsibleLookupError: - 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)] - if not self._logged_in: - args += [to_bytes('--session=') + self._token] + if not self.logged_in: + args += [to_bytes('--session=') + self.token] rc, output, dummy = self._run(args) return output @@ -127,13 +172,29 @@ class OnePass(object): output = self.get_raw(item_id, vault) return self._parse_field(output, field, section) if output != '' else '' + def full_login(self): + if None in [self.subdomain, self.username, self.secret_key, self.master_password]: + raise AnsibleLookupError('Unable to perform initial sign in to 1Password. ' + 'subdomain, username, secret_key, and master_password are required to perform initial sign in.') + + args = [ + 'signin', + '{0}.1password.com'.format(self.subdomain), + to_bytes(self.username), + to_bytes(self.secret_key), + '--output=raw', + ] + + rc, out, err = self._run(args, command_input=to_bytes(self.master_password)) + self.token = out.strip() + 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 not ignore_errors and rc != expected_rc: - raise AnsibleLookupError(err) + raise AnsibleLookupError(to_text(err)) return rc, out, err def _parse_field(self, data_json, field_name, section_title=None): @@ -159,8 +220,10 @@ class LookupModule(LookupBase): 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.subdomain = kwargs.get('subdomain') + op.username = kwargs.get('username') + op.secret_key = kwargs.get('secret_key') + op.master_password = kwargs.get('master_password', kwargs.get('vault_password')) op.assert_logged_in() diff --git a/lib/ansible/plugins/lookup/onepassword_raw.py b/lib/ansible/plugins/lookup/onepassword_raw.py index 9150fe5be3..f7642a5882 100644 --- a/lib/ansible/plugins/lookup/onepassword_raw.py +++ b/lib/ansible/plugins/lookup/onepassword_raw.py @@ -20,25 +20,46 @@ DOCUMENTATION = """ 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 + short_description: fetch an entire item from 1Password description: - - onepassword_raw wraps C(op) command line utility to fetch an entire item from 1Password + - C(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 + master_password: + description: The password used to unlock the specified vault. + default: None + version_added: '2.7' + aliases: ['vault_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 + username: + description: The username used to sign in. version_added: '2.7' + secret_key: + description: The secret key used when performing an initial sign in. + version_added: '2.7' + vault: + description: Vault containing the item to retrieve (case-insensitive). If absent will search all vaults + default: None + notes: + - This lookup will use an existing 1Password session if one exists. If not, and you have already + performed an initial sign in (meaning C(~/.op/config exists)), then only the C(master_password) is required. + You may optionally specify C(subdomain) in this scenario, otherwise the last used subdomain will be used by C(op). + - This lookup can perform an initial login by providing C(subdomain), C(username), C(secret_key), and C(master_password). + - Due to the B(very) sensitive nature of these credentials, it is B(highly) recommeneded that you only pass in the minial credentials + needed at any given time. Also, store these credentials in an Ansible Vault using a key that is equal to or greater in strength + to the 1Password master password. + - This lookup stores potentially sensitive data from 1Password as Ansible facts. + Facts are subject to caching if enabled, which means this data could be stored in clear text + on disk or in a database. + - Tested with C(op) version 0.5.3 """ EXAMPLES = """ @@ -68,8 +89,10 @@ class LookupModule(LookupBase): op = OnePass() vault = kwargs.get('vault') - op._subdomain = kwargs.get('subdomain') - op._vault_password = kwargs.get('vault_password') + op.subdomain = kwargs.get('subdomain') + op.username = kwargs.get('username') + op.secret_key = kwargs.get('secret_key') + op.master_password = kwargs.get('master_password', kwargs.get('vault_password')) op.assert_logged_in() diff --git a/test/units/plugins/lookup/test_onepassword.py b/test/units/plugins/lookup/test_onepassword.py index 374a103b50..955d6a51c5 100644 --- a/test/units/plugins/lookup/test_onepassword.py +++ b/test/units/plugins/lookup/test_onepassword.py @@ -233,45 +233,45 @@ class TestOnePass(unittest.TestCase): def test_onepassword_get(self): op = MockOnePass() - op._logged_in = True + op.logged_in = True query_generator = get_mock_query_generator() for dummy, query, dummy, field_name, field_value in query_generator: self.assertEqual(field_value, op.get_field(query, field_name)) def test_onepassword_get_raw(self): op = MockOnePass() - op._logged_in = True + op.logged_in = True for entry in MOCK_ENTRIES: for query in entry['queries']: self.assertEqual(json.dumps(entry['output']), op.get_raw(query)) def test_onepassword_get_not_found(self): op = MockOnePass() - op._logged_in = True + op.logged_in = True self.assertEqual('', op.get_field('a fake query', 'a fake field')) def test_onepassword_get_with_section(self): op = MockOnePass() - op._logged_in = True + op.logged_in = True dummy, query, section_title, field_name, field_value = get_one_mock_query() self.assertEqual(field_value, op.get_field(query, field_name, section=section_title)) def test_onepassword_get_with_vault(self): op = MockOnePass() - op._logged_in = True + op.logged_in = True entry, query, dummy, field_name, field_value = get_one_mock_query() for vault_query in [entry['vault_name'], entry['output']['vaultUuid']]: self.assertEqual(field_value, op.get_field(query, field_name, vault=vault_query)) def test_onepassword_get_with_wrong_vault(self): op = MockOnePass() - op._logged_in = True + op.logged_in = True dummy, query, dummy, field_name, dummy = get_one_mock_query() self.assertEqual('', op.get_field(query, field_name, vault='a fake vault')) def test_onepassword_get_diff_case(self): op = MockOnePass() - op._logged_in = True + op.logged_in = True entry, query, section_title, field_name, field_value = get_one_mock_query() self.assertEqual( field_value,