diff --git a/lib/ansible/cli/__init__.py b/lib/ansible/cli/__init__.py index 820d549bdf3..bb9c480f409 100644 --- a/lib/ansible/cli/__init__.py +++ b/lib/ansible/cli/__init__.py @@ -34,7 +34,7 @@ from abc import ABCMeta, abstractmethod import ansible from ansible import constants as C -from ansible.errors import AnsibleError, AnsibleOptionsError +from ansible.errors import AnsibleOptionsError from ansible.inventory.manager import InventoryManager from ansible.module_utils.six import with_metaclass, string_types from ansible.module_utils._text import to_bytes, to_text @@ -43,6 +43,7 @@ from ansible.release import __version__ from ansible.utils.path import unfrackpath from ansible.utils.vars import load_extra_vars, load_options_vars from ansible.vars.manager import VariableManager +from ansible.parsing.vault import PromptVaultSecret, get_file_vault_secret try: from __main__ import display @@ -168,37 +169,89 @@ class CLI(with_metaclass(ABCMeta, object)): display.v(u"No config file found; using defaults") @staticmethod - def ask_vault_passwords(): - ''' prompt for vault password and/or password change ''' + def split_vault_id(vault_id): + # return (before_@, after_@) + # if no @, return whole string as after_ + if '@' not in vault_id: + return (None, vault_id) - vault_pass = None - try: - vault_pass = getpass.getpass(prompt="Vault password: ") + parts = vault_id.split('@', 1) + ret = tuple(parts) + return ret - except EOFError: - pass + @staticmethod + def build_vault_ids(vault_ids, vault_password_files=None, ask_vault_pass=None): + vault_password_files = vault_password_files or [] + vault_ids = vault_ids or [] - # enforce no newline chars at the end of passwords - if vault_pass: - vault_pass = to_bytes(vault_pass, errors='surrogate_or_strict', nonstring='simplerepr').strip() + # convert vault_password_files into vault_ids slugs + for password_file in vault_password_files: + id_slug = u'%s@%s' % (C.DEFAULT_VAULT_IDENTITY, password_file) - return vault_pass + # note this makes --vault-id higher precendence than --vault-password-file + # if we want to intertwingle them in order probably need a cli callback to populate vault_ids + # used by --vault-id and --vault-password-file + vault_ids.append(id_slug) + if ask_vault_pass: + id_slug = u'%s@%s' % (C.DEFAULT_VAULT_IDENTITY, u'prompt') + vault_ids.append(id_slug) + + return vault_ids + + # TODO: remove the now unused args @staticmethod - def ask_new_vault_passwords(): - new_vault_pass = None - try: - new_vault_pass = getpass.getpass(prompt="New Vault password: ") - new_vault_pass2 = getpass.getpass(prompt="Confirm New Vault password: ") - if new_vault_pass != new_vault_pass2: - raise AnsibleError("Passwords do not match") - except EOFError: - pass + def setup_vault_secrets(loader, vault_ids, vault_password_files=None, + ask_vault_pass=None, create_new_password=False): + # list of tuples + vault_secrets = [] + + if create_new_password: + prompt_formats = ['New vault password (%s): ', + 'Confirm vew vault password (%s): '] + else: + prompt_formats = ['Vault password (%s): '] + + vault_ids = CLI.build_vault_ids(vault_ids, + vault_password_files, + ask_vault_pass) + + for index, vault_id_slug in enumerate(vault_ids): + vault_id_name, vault_id_value = CLI.split_vault_id(vault_id_slug) + if vault_id_value == 'prompt': + # TODO: we could assume --vault-id=prompt implies --ask-vault-pass + # if not, we need to 'if ask_vault_pass' here + if vault_id_name: + prompted_vault_secret = PromptVaultSecret(prompt_formats=prompt_formats, vault_id=vault_id_name) + prompted_vault_secret.load() + vault_secrets.append((vault_id_name, prompted_vault_secret)) + else: + prompted_vault_secret = PromptVaultSecret(prompt_formats=prompt_formats, + vault_id=C.DEFAULT_VAULT_IDENTITY) + prompted_vault_secret.load() + vault_secrets.append((C.DEFAULT_VAULT_IDENTITY, prompted_vault_secret)) + + # update loader with new secrets incrementally, so we can load a vault password + # that is encrypted with a vault secret provided earlier + loader.set_vault_secrets(vault_secrets) + continue + + # assuming anything else is a password file + display.vvvvv('Reading vault password file: %s' % vault_id_value) + # read vault_pass from a file + file_vault_secret = get_file_vault_secret(filename=vault_id_value, + vault_id_name=vault_id_name, + loader=loader) + file_vault_secret.load() + if vault_id_name: + vault_secrets.append((vault_id_name, file_vault_secret)) + else: + vault_secrets.append((C.DEFAULT_VAULT_IDENTITY, file_vault_secret)) - if new_vault_pass: - new_vault_pass = to_bytes(new_vault_pass, errors='surrogate_or_strict', nonstring='simplerepr').strip() + # update loader with as-yet-known vault secrets + loader.set_vault_secrets(vault_secrets) - return new_vault_pass + return vault_secrets def ask_passwords(self): ''' prompt for connection and become passwords if needed ''' @@ -260,7 +313,7 @@ class CLI(with_metaclass(ABCMeta, object)): if vault_opts: # Check for vault related conflicts - if (op.ask_vault_pass and op.vault_password_file): + if (op.ask_vault_pass and op.vault_password_files): self.parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive") if runas_opts: @@ -278,12 +331,14 @@ class CLI(with_metaclass(ABCMeta, object)): @staticmethod def unfrack_paths(option, opt, value, parser): + paths = getattr(parser.values, option.dest) if isinstance(value, string_types): - setattr(parser.values, option.dest, [unfrackpath(x) for x in value.split(os.pathsep)]) + paths.extend([unfrackpath(x) for x in value.split(os.pathsep)]) elif isinstance(value, list): - setattr(parser.values, option.dest, [unfrackpath(x) for x in value]) + paths.extend([unfrackpath(x) for x in value]) else: pass # FIXME: should we raise options error? + setattr(parser.values, option.dest, paths) @staticmethod def unfrack_path(option, opt, value, parser): @@ -324,13 +379,17 @@ class CLI(with_metaclass(ABCMeta, object)): if vault_opts: parser.add_option('--ask-vault-pass', default=C.DEFAULT_ASK_VAULT_PASS, dest='ask_vault_pass', action='store_true', help='ask for vault password') - parser.add_option('--vault-password-file', default=C.DEFAULT_VAULT_PASSWORD_FILE, dest='vault_password_file', - help="vault password file", action="callback", callback=CLI.unfrack_path, type='string') - parser.add_option('--new-vault-password-file', dest='new_vault_password_file', - help="new vault password file for rekey", action="callback", callback=CLI.unfrack_path, type='string') + parser.add_option('--vault-password-file', default=[], dest='vault_password_files', + help="vault password file", action="callback", callback=CLI.unfrack_paths, type='string') + parser.add_option('--new-vault-password-file', default=[], dest='new_vault_password_files', + help="new vault password file for rekey", action="callback", callback=CLI.unfrack_paths, type='string') parser.add_option('--output', default=None, dest='output_file', help='output file name for encrypt or decrypt; use - for stdout', - action="callback", callback=CLI.unfrack_path, type='string') + action="callback", callback=CLI.unfrack_path, type='string'), + parser.add_option('--vault-id', default=[], dest='vault_ids', action='append', type='string', + help='the vault identity to use') + parser.add_option('--new-vault-id', default=None, dest='new_vault_id', type='string', + help='the new vault identity to use for rekey') if subset_opts: parser.add_option('-t', '--tags', dest='tags', default=[], action='append', @@ -649,54 +708,17 @@ class CLI(with_metaclass(ABCMeta, object)): return t - @staticmethod - def read_vault_password_file(vault_password_file, loader): - """ - Read a vault password from a file or if executable, execute the script and - retrieve password from STDOUT - """ - - this_path = os.path.realpath(os.path.expanduser(vault_password_file)) - if not os.path.exists(this_path): - raise AnsibleError("The vault password file %s was not found" % this_path) - - if loader.is_executable(this_path): - try: - # STDERR not captured to make it easier for users to prompt for input in their scripts - p = subprocess.Popen(this_path, stdout=subprocess.PIPE) - except OSError as e: - raise AnsibleError("Problem running vault password script %s (%s). If this is not a script, " - "remove the executable bit from the file." % (' '.join(this_path), e)) - stdout, stderr = p.communicate() - if p.returncode != 0: - raise AnsibleError("Vault password script %s returned non-zero (%s): %s" % (this_path, p.returncode, p.stderr)) - vault_pass = stdout.strip(b'\r\n') - else: - try: - f = open(this_path, "rb") - vault_pass = f.read().strip() - f.close() - except (OSError, IOError) as e: - raise AnsibleError("Could not read vault password file %s: %s" % (this_path, e)) - - return vault_pass - @staticmethod def _play_prereqs(options): # all needs loader loader = DataLoader() - # vault - b_vault_pass = None - if options.vault_password_file: - # read vault_pass from a file - b_vault_pass = CLI.read_vault_password_file(options.vault_password_file, loader=loader) - elif options.ask_vault_pass: - b_vault_pass = CLI.ask_vault_passwords() - - if b_vault_pass is not None: - loader.set_vault_password(b_vault_pass) + vault_secrets = CLI.setup_vault_secrets(loader, + vault_ids=options.vault_ids, + vault_password_files=options.vault_password_files, + ask_vault_pass=options.ask_vault_pass) + loader.set_vault_secrets(vault_secrets) # create the inventory, and filter it based on the subset specified (if any) inventory = InventoryManager(loader=loader, sources=options.inventory) diff --git a/lib/ansible/cli/console.py b/lib/ansible/cli/console.py index a70f7020e8a..cb15d7970b6 100644 --- a/lib/ansible/cli/console.py +++ b/lib/ansible/cli/console.py @@ -415,6 +415,12 @@ class ConsoleCLI(CLI, cmd.Cmd): self.loader, self.inventory, self.variable_manager = self._play_prereqs(self.options) + vault_secrets = self.setup_vault_secrets(self.loader, + vault_id=self.options.vault_ids, + vault_password_files=self.options.vault_password_files, + ask_vault_pass=self.options.ask_vault_pass) + self.loader.set_vault_secrets(vault_secrets) + no_hosts = False if len(self.inventory.list_hosts()) == 0: # Empty inventory diff --git a/lib/ansible/cli/pull.py b/lib/ansible/cli/pull.py index 98f3a9ebfce..5ebb51881f4 100644 --- a/lib/ansible/cli/pull.py +++ b/lib/ansible/cli/pull.py @@ -226,8 +226,9 @@ class PullCLI(CLI): # Build playbook command cmd = '%s/ansible-playbook %s %s' % (bin_path, base_opts, playbook) - if self.options.vault_password_file: - cmd += " --vault-password-file=%s" % self.options.vault_password_file + if self.options.vault_password_files: + for vault_password_file in self.options.vault_password_files: + cmd += " --vault-password-file=%s" % vault_password_file if inv_opts: cmd += ' %s' % inv_opts for ev in self.options.extra_vars: diff --git a/lib/ansible/cli/vault.py b/lib/ansible/cli/vault.py index ce3da2b8fe1..1f28cfb8ce7 100644 --- a/lib/ansible/cli/vault.py +++ b/lib/ansible/cli/vault.py @@ -23,10 +23,10 @@ import os import sys from ansible.cli import CLI -from ansible.errors import AnsibleError, AnsibleOptionsError +from ansible.errors import AnsibleOptionsError from ansible.module_utils._text import to_text, to_bytes from ansible.parsing.dataloader import DataLoader -from ansible.parsing.vault import VaultEditor +from ansible.parsing.vault import VaultEditor, VaultLib, match_encrypt_secret try: from __main__ import display @@ -59,6 +59,12 @@ class VaultCLI(CLI): self.b_vault_pass = None self.b_new_vault_pass = None self.encrypt_string_read_stdin = False + + self.encrypt_secret = None + self.encrypt_vault_id = None + self.new_encrypt_secret = None + self.new_encrypt_vault_id = None + super(VaultCLI, self).__init__(args) def set_action(self): @@ -108,6 +114,11 @@ class VaultCLI(CLI): can_output = ['encrypt', 'decrypt', 'encrypt_string'] + if self.options.vault_ids: + for vault_id in self.options.vault_ids: + if u';' in vault_id: + raise AnsibleOptionsError("'%s' is not a valid vault id. The character ';' is not allowed in vault ids" % vault_id) + if self.action not in can_output: if self.options.output_file: raise AnsibleOptionsError("The --output option can be used only with ansible-vault %s" % '/'.join(can_output)) @@ -132,43 +143,79 @@ class VaultCLI(CLI): raise AnsibleOptionsError('The --prompt option is not supported if also reading input from stdin') def run(self): - super(VaultCLI, self).run() loader = DataLoader() # set default restrictive umask old_umask = os.umask(0o077) - if self.options.vault_password_file: - # read vault_pass from a file - self.b_vault_pass = CLI.read_vault_password_file(self.options.vault_password_file, loader) - - if self.options.new_vault_password_file: - # for rekey only - self.b_new_vault_pass = CLI.read_vault_password_file(self.options.new_vault_password_file, loader) - - if not self.b_vault_pass or self.options.ask_vault_pass: - # the 'read' options don't need to ask for password confirmation. - # 'edit' is read/write, but the decrypt will confirm. - if self.action in ['decrypt', 'edit', 'view', 'rekey']: - self.b_vault_pass = self.ask_vault_passwords() - else: - self.b_vault_pass = self.ask_new_vault_passwords() - - if not self.b_vault_pass: - raise AnsibleOptionsError("A password is required to use Ansible's Vault") - - if self.action == 'rekey': - if not self.b_new_vault_pass: - self.b_new_vault_pass = self.ask_new_vault_passwords() - if not self.b_new_vault_pass: - raise AnsibleOptionsError("A password is required to rekey Ansible's Vault") - - if self.action == 'encrypt_string': - if self.options.encrypt_string_prompt: - self.encrypt_string_prompt = True - - self.editor = VaultEditor(self.b_vault_pass) + vault_ids = self.options.vault_ids + + # there are 3 types of actions, those that just 'read' (decrypt, view) and only + # need to ask for a password once, and those that 'write' (create, encrypt) that + # ask for a new password and confirm it, and 'read/write (rekey) that asks for the + # old password, then asks for a new one and confirms it. + + # TODO: instead of prompting for these before, we could let VaultEditor + # call a callback when it needs it. + if self.action in ['decrypt', 'view', 'rekey']: + vault_secrets = self.setup_vault_secrets(loader, + vault_ids=vault_ids, + vault_password_files=self.options.vault_password_files, + ask_vault_pass=self.options.ask_vault_pass) + + if not vault_secrets: + raise AnsibleOptionsError("A vault password is required to use Ansible's Vault") + + if self.action in ['encrypt', 'encrypt_string', 'create', 'edit']: + if len(vault_ids) > 1: + raise AnsibleOptionsError("Only one --vault-id can be used for encryption") + + vault_secrets = None + vault_secrets = \ + self.setup_vault_secrets(loader, + vault_ids=vault_ids, + vault_password_files=self.options.vault_password_files, + ask_vault_pass=self.options.ask_vault_pass, + create_new_password=True) + + if not vault_secrets: + raise AnsibleOptionsError("A vault password is required to use Ansible's Vault") + + encrypt_secret = match_encrypt_secret(vault_secrets) + # only one secret for encrypt for now, use the first vault_id and use its first secret + # self.encrypt_vault_id = list(vault_secrets.keys())[0] + # self.encrypt_secret = vault_secrets[self.encrypt_vault_id][0] + self.encrypt_vault_id = encrypt_secret[0] + self.encrypt_secret = encrypt_secret[1] + + if self.action in ['rekey']: + new_vault_ids = [] + if self.options.new_vault_id: + new_vault_ids.append(self.options.new_vault_id) + + new_vault_secrets = \ + self.setup_vault_secrets(loader, + vault_ids=new_vault_ids, + vault_password_files=self.options.new_vault_password_files, + ask_vault_pass=self.options.ask_vault_pass, + create_new_password=True) + + if not new_vault_secrets: + raise AnsibleOptionsError("A new vault password is required to use Ansible's Vault rekey") + + # There is only one new_vault_id currently and one new_vault_secret + new_encrypt_secret = match_encrypt_secret(new_vault_secrets) + + self.new_encrypt_vault_id = new_encrypt_secret[0] + self.new_encrypt_secret = new_encrypt_secret[1] + + loader.set_vault_secrets(vault_secrets) + self.secrets = vault_secrets + + # FIXME: do we need to create VaultEditor here? its not reused + vault = VaultLib(self.secrets) + self.editor = VaultEditor(vault) self.execute() @@ -182,7 +229,10 @@ class VaultCLI(CLI): display.display("Reading plaintext input from stdin", stderr=True) for f in self.args or ['-']: - self.editor.encrypt_file(f, output_file=self.options.output_file) + # Fixme: use the correct vau + self.editor.encrypt_file(f, self.encrypt_secret, + vault_id=self.encrypt_vault_id, + output_file=self.options.output_file) if sys.stdout.isatty(): display.display("Encryption successful", stderr=True) @@ -227,6 +277,8 @@ class VaultCLI(CLI): if name_prompt_response != "": name = name_prompt_response + # TODO: could prompt for which vault_id to use for each plaintext string + # currently, it will just be the default # could use private=True for shadowed input if useful prompt_response = display.prompt(msg) @@ -282,8 +334,9 @@ class VaultCLI(CLI): b_plaintext = to_bytes(plaintext) b_plaintext_list.append((b_plaintext, self.FROM_ARGS, name)) + # TODO: specify vault_id per string? # Format the encrypted strings and any corresponding stderr output - outputs = self._format_output_vault_strings(b_plaintext_list) + outputs = self._format_output_vault_strings(b_plaintext_list, vault_id=self.encrypt_vault_id) for output in outputs: err = output.get('err', None) @@ -297,7 +350,7 @@ class VaultCLI(CLI): # TODO: offer block or string ala eyaml - def _format_output_vault_strings(self, b_plaintext_list): + def _format_output_vault_strings(self, b_plaintext_list, vault_id=None): # If we are only showing one item in the output, we don't need to included commented # delimiters in the text show_delimiter = False @@ -313,7 +366,9 @@ class VaultCLI(CLI): for index, b_plaintext_info in enumerate(b_plaintext_list): # (the text itself, which input it came from, its name) b_plaintext, src, name = b_plaintext_info - b_ciphertext = self.editor.encrypt_bytes(b_plaintext) + + b_ciphertext = self.editor.encrypt_bytes(b_plaintext, self.encrypt_secret, + vault_id=vault_id) # block formatting yaml_text = self.format_ciphertext_yaml(b_ciphertext, name=name) @@ -347,7 +402,8 @@ class VaultCLI(CLI): if len(self.args) > 1: raise AnsibleOptionsError("ansible-vault create can take only one filename argument") - self.editor.create_file(self.args[0]) + self.editor.create_file(self.args[0], self.encrypt_secret, + vault_id=self.encrypt_vault_id) def execute_edit(self): ''' open and decrypt an existing vaulted file in an editor, that will be encryped again when closed''' @@ -363,15 +419,14 @@ class VaultCLI(CLI): # unicode here because we are displaying it and therefore can make # the decision that the display doesn't have to be precisely what # the input was (leave that to decrypt instead) - self.pager(to_text(self.editor.plaintext(f))) + plaintext = self.editor.plaintext(f) + self.pager(to_text(plaintext)) def execute_rekey(self): ''' re-encrypt a vaulted file with a new secret, the previous secret is required ''' for f in self.args: - if not (os.path.isfile(f)): - raise AnsibleError(f + " does not exist") - - for f in self.args: - self.editor.rekey_file(f, self.b_new_vault_pass) + # FIXME: plumb in vault_id, use the default new_vault_secret for now + self.editor.rekey_file(f, self.new_encrypt_secret, + self.new_encrypt_vault_id) display.display("Rekey successful", stderr=True) diff --git a/lib/ansible/config/data/config.yml b/lib/ansible/config/data/config.yml index a1be87bddab..a4a2f9753da 100644 --- a/lib/ansible/config/data/config.yml +++ b/lib/ansible/config/data/config.yml @@ -1072,6 +1072,22 @@ DEFAULT_VAR_COMPRESSION_LEVEL: value_type: integer vars: [] yaml: {key: defaults.var_compression_level} +DEFAULT_VAULT_ID_MATCH: + default: False + desc: 'If true, decrypting vaults with a vault id will only try the password from the matching vault-id' + env: [{name: ANSIBLE_VAULT_ID_MATCH}] + ini: + - {key: vault_id_match, section: defaults} + vars: [] + yaml: {key: defaults.vault_id_match} +DEFAULT_VAULT_IDENTITY: + default: default + desc: 'TODO: write it' + env: [{name: ANSIBLE_VAULT_IDENTITY}] + ini: + - {key: vault_identity, section: defaults} + vars: [] + yaml: {key: defaults.vault_identity} DEFAULT_VAULT_PASSWORD_FILE: default: ~ desc: 'TODO: write it' diff --git a/lib/ansible/parsing/dataloader.py b/lib/ansible/parsing/dataloader.py index 2844a890dda..13aa273db20 100644 --- a/lib/ansible/parsing/dataloader.py +++ b/lib/ansible/parsing/dataloader.py @@ -26,12 +26,13 @@ import re import tempfile from yaml import YAMLError +from ansible.module_utils.six import text_type, string_types from ansible.errors import AnsibleFileNotFound, AnsibleParserError from ansible.errors.yaml_strings import YAML_SYNTAX_ERROR from ansible.module_utils.basic import is_executable from ansible.module_utils.six import binary_type, text_type from ansible.module_utils._text import to_bytes, to_native, to_text -from ansible.parsing.vault import VaultLib, b_HEADER, is_encrypted, is_encrypted_file +from ansible.parsing.vault import VaultLib, b_HEADER, is_encrypted, is_encrypted_file, parse_vaulttext_envelope from ansible.parsing.quoting import unquote from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleUnicode @@ -73,11 +74,16 @@ class DataLoader: self._tempfiles = set() # initialize the vault stuff with an empty password - self.set_vault_password(None) + # TODO: replace with a ref to something that can get the password + # a creds/auth provider + # self.set_vault_password(None) + self._vaults = {} + self._vault = VaultLib() + self.set_vault_secrets(None) - def set_vault_password(self, b_vault_password): - self._b_vault_password = b_vault_password - self._vault = VaultLib(b_password=b_vault_password) + # TODO: since we can query vault_secrets late, we could provide this to DataLoader init + def set_vault_secrets(self, vault_secrets): + self._vault.secrets = vault_secrets def load(self, data, file_name='', show_content=True): ''' @@ -170,7 +176,7 @@ class DataLoader: def _safe_load(self, stream, file_name=None): ''' Implements yaml.safe_load(), except using our custom loader class. ''' - loader = AnsibleLoader(stream, file_name, self._b_vault_password) + loader = AnsibleLoader(stream, file_name, self._vault.secrets) try: return loader.get_single_data() finally: @@ -206,6 +212,8 @@ class DataLoader: with open(b_file_name, 'rb') as f: data = f.read() if is_encrypted(data): + # FIXME: plugin vault selector + b_ciphertext, b_version, cipher_name, vault_id = parse_vaulttext_envelope(data) data = self._vault.decrypt(data, filename=b_file_name) show_content = False @@ -362,7 +370,6 @@ class DataLoader: b_upath = to_bytes(upath, errors='surrogate_or_strict') b_mydir = os.path.dirname(b_upath) - # FIXME: this detection fails with non main.yml roles # if path is in role and 'tasks' not there already, add it into the search if is_role or self._is_role(path): if b_mydir.endswith(b'tasks'): @@ -439,8 +446,8 @@ class DataLoader: # the decrypt call would throw an error, but we check first # since the decrypt function doesn't know the file name data = f.read() - if not self._b_vault_password: - raise AnsibleParserError("A vault password must be specified to decrypt %s" % to_native(file_path)) + if not self._vault.secrets: + raise AnsibleParserError("A vault password or secret must be specified to decrypt %s" % to_native(file_path)) data = self._vault.decrypt(data, filename=real_path) # Make a temp file diff --git a/lib/ansible/parsing/vault/__init__.py b/lib/ansible/parsing/vault/__init__.py index e281df3384b..c69910f2d78 100644 --- a/lib/ansible/parsing/vault/__init__.py +++ b/lib/ansible/parsing/vault/__init__.py @@ -23,6 +23,7 @@ import os import random import shlex import shutil +import subprocess import sys import tempfile import warnings @@ -72,6 +73,7 @@ except ImportError: pass from ansible.errors import AnsibleError +from ansible import constants as C from ansible.module_utils.six import PY3, binary_type from ansible.module_utils.six.moves import zip from ansible.module_utils._text import to_bytes, to_text @@ -142,12 +144,267 @@ def is_encrypted_file(file_obj, start_pos=0, count=-1): file_obj.seek(current_position) -class VaultLib: +def parse_vaulttext_envelope(b_vaulttext_envelope, default_vault_id=None): + """Retrieve information about the Vault and clean the data + + When data is saved, it has a header prepended and is formatted into 80 + character lines. This method extracts the information from the header + and then removes the header and the inserted newlines. The string returned + is suitable for processing by the Cipher classes. + + :arg b_vaulttext: byte str containing the data from a save file + :returns: a byte str suitable for passing to a Cipher class's + decrypt() function. + """ + # used by decrypt + default_vault_id = default_vault_id or C.DEFAULT_VAULT_IDENTITY + + b_tmpdata = b_vaulttext_envelope.split(b'\n') + b_tmpheader = b_tmpdata[0].strip().split(b';') + + b_version = b_tmpheader[1].strip() + cipher_name = to_text(b_tmpheader[2].strip()) + vault_id = default_vault_id + # vault_id = None + + # Only attempt to find vault_id if the vault file is version 1.2 or newer + # if self.b_version == b'1.2': + if len(b_tmpheader) >= 4: + vault_id = to_text(b_tmpheader[3].strip()) + + b_ciphertext = b''.join(b_tmpdata[1:]) + + return b_ciphertext, b_version, cipher_name, vault_id + + +def format_vaulttext_envelope(b_ciphertext, cipher_name, version=None, vault_id=None): + """ Add header and format to 80 columns + + :arg b_ciphertext: the encrypted and hexlified data as a byte string + :arg cipher_name: unicode cipher name (for ex, u'AES256') + :arg version: unicode vault version (for ex, '1.2'). Optional ('1.1' is default) + :arg vault_id: unicode vault identifier. If provided, the version will be bumped to 1.2. + :returns: a byte str that should be dumped into a file. It's + formatted to 80 char columns and has the header prepended + """ + + if not cipher_name: + raise AnsibleError("the cipher must be set before adding a header") + + version = version or '1.1' + + # If we specify a vault_id, use format version 1.2. For no vault_id, stick to 1.1 + if vault_id and vault_id != u'default': + version = '1.2' + + b_version = to_bytes(version, 'utf-8', errors='strict') + b_vault_id = to_bytes(vault_id, 'utf-8', errors='strict') + b_cipher_name = to_bytes(cipher_name, 'utf-8', errors='strict') + + header_parts = [b_HEADER, + b_version, + b_cipher_name] + + if b_version == b'1.2' and b_vault_id: + header_parts.append(b_vault_id) + + header = b';'.join(header_parts) + + b_vaulttext = [header] + b_vaulttext += [b_ciphertext[i:i + 80] for i in range(0, len(b_ciphertext), 80)] + b_vaulttext += [b''] + b_vaulttext = b'\n'.join(b_vaulttext) + + return b_vaulttext + + +class VaultSecret: + '''Opaque/abstract objects for a single vault secret. ie, a password or a key.''' + def __init__(self, _bytes=None): + # FIXME: ? that seems wrong... Unset etc? + self._bytes = _bytes + + @property + def bytes(self): + '''The secret as a bytestring. + + Sub classes that store text types will need to override to encode the text to bytes. + ''' + return self._bytes + + def load(self): + return self._bytes + + +class PromptVaultSecret(VaultSecret): + default_prompt_formats = ["Vault password (%s): "] + + def __init__(self, _bytes=None, vault_id=None, prompt_formats=None): + self._bytes = _bytes + self.vault_id = vault_id + + if prompt_formats is None: + self.prompt_formats = self.default_prompt_formats + else: + self.prompt_formats = prompt_formats + + @property + def bytes(self): + return self._bytes + + def load(self): + self._bytes = self.ask_vault_passwords() + + def ask_vault_passwords(self): + b_vault_passwords = [] + + for prompt_format in self.prompt_formats: + prompt = prompt_format % self.vault_id + try: + vault_pass = display.prompt(prompt, private=True) + except EOFError: + pass + b_vault_pass = to_bytes(vault_pass, errors='strict', nonstring='simplerepr').strip() + b_vault_passwords.append(b_vault_pass) + + # Make sure the passwords match by comparing them all to the first password + for b_vault_password in b_vault_passwords: + self.confirm(b_vault_passwords[0], b_vault_password) + + if b_vault_passwords: + return b_vault_passwords[0] + + return None + + def confirm(self, b_vault_pass_1, b_vault_pass_2): + # enforce no newline chars at the end of passwords + + if b_vault_pass_1 != b_vault_pass_2: + # FIXME: more specific exception + raise AnsibleError("Passwords do not match") + + +def get_file_vault_secret(filename=None, vault_id_name=None, encoding=None, loader=None): + this_path = os.path.realpath(os.path.expanduser(filename)) + + if not os.path.exists(this_path): + raise AnsibleError("The vault password file %s was not found" % this_path) + + if loader.is_executable(this_path): + # TODO: pass vault_id_name to script via cli + return ScriptVaultSecret(filename=this_path, encoding=encoding, loader=loader) + else: + return FileVaultSecret(filename=this_path, encoding=encoding, loader=loader) + + +# TODO: mv these classes to a seperate file so we don't pollute vault with 'subprocess' etc +class FileVaultSecret(VaultSecret): + def __init__(self, filename=None, encoding=None, loader=None): + super(FileVaultSecret, self).__init__() + self.filename = filename + self.loader = loader + + self.encoding = encoding or 'utf8' + + # We could load from file here, but that is eventually a pain to test + self._bytes = None + self._text = None + + @property + def bytes(self): + if self._bytes: + return self._bytes + if self._text: + return self._text.encode(self.encoding) + return None + + def load(self): + self._bytes = self.read_file(self.filename, self.loader) + + @staticmethod + def read_file(filename, loader): + """ + Read a vault password from a file or if executable, execute the script and + retrieve password from STDOUT + """ + + try: + f = open(filename, "rb") + vault_pass = f.read().strip() + f.close() + except (OSError, IOError) as e: + raise AnsibleError("Could not read vault password file %s: %s" % (filename, e)) + + return vault_pass + + def __repr__(self): + if self.filename: + return "%s(filename='%s')" % (self.__class__.__name__, self.filename) + return "%s()" % (self.__class__.__name__) + + +class ScriptVaultSecret(FileVaultSecret): + + @staticmethod + def read_file(filename, loader): + if not loader.is_executable(filename): + raise AnsibleVaultError("The vault password script %s was not executable" % filename) + + try: + # STDERR not captured to make it easier for users to prompt for input in their scripts + p = subprocess.Popen(filename, stdout=subprocess.PIPE) + except OSError as e: + msg_format = "Problem running vault password script %s (%s)." + "If this is not a script, remove the executable bit from the file." + msg = msg_format % (' '.join(filename), e) + + raise AnsibleError(msg) + + stdout, stderr = p.communicate() + + if p.returncode != 0: + raise AnsibleError("Vault password script %s returned non-zero (%s): %s" % (filename, p.returncode, p.stderr)) + + vault_pass = stdout.strip(b'\r\n') + return vault_pass + + +def match_secrets(secrets, target_vault_ids): + '''Find all VaultSecret objects that are mapped to any of the target_vault_ids in secrets''' + if not secrets: + return [] - def __init__(self, b_password): - self.b_password = to_bytes(b_password, errors='strict', encoding='utf-8') + matches = [(vault_id, secret) for vault_id, secret in secrets if vault_id in target_vault_ids] + return matches + + +def match_best_secret(secrets, target_vault_ids): + '''Find the best secret from secrets that matches target_vault_ids + + Since secrets should be ordered so the early secrets are 'better' than later ones, this + just finds all the matches, then returns the first secret''' + matches = match_secrets(secrets, target_vault_ids) + if matches: + return matches[0] + # raise exception? + return None + + +def match_encrypt_secret(secrets): + '''Find the best/first/only secret in secrets to use for encrypting''' + + # ie, consider all of the available secrets as matches + _vault_id_matchers = [_vault_id for _vault_id, _vault_secret in secrets] + best_secret = match_best_secret(secrets, _vault_id_matchers) + # can be empty list sans any tuple + return best_secret + + +class VaultLib: + def __init__(self, secrets=None): + self.secrets = secrets or [] self.cipher_name = None - self.b_version = b'1.1' + self.b_version = b'1.2' @staticmethod def is_encrypted(data): @@ -169,7 +426,7 @@ class VaultLib: display.deprecated(u'vault.VaultLib.is_encrypted_file is deprecated. Use vault.is_encrypted_file instead', version='2.4') return is_encrypted_file(file_obj) - def encrypt(self, plaintext): + def encrypt(self, plaintext, secret=None, vault_id=None): """Vault encrypt a piece of data. :arg plaintext: a text or byte string to encrypt. @@ -181,6 +438,13 @@ class VaultLib: If the string passed in is a text string, it will be encoded to UTF-8 before encryption. """ + + if secret is None: + if self.secrets: + secret_vault_id, secret = match_encrypt_secret(self.secrets) + else: + raise AnsibleVaultError("A vault password must be specified to encrypt data") + b_plaintext = to_bytes(plaintext, errors='surrogate_or_strict') if is_encrypted(b_plaintext): @@ -195,10 +459,13 @@ class VaultLib: raise AnsibleError(u"{0} cipher could not be found".format(self.cipher_name)) # encrypt data - b_ciphertext = this_cipher.encrypt(b_plaintext, self.b_password) + display.vvvvv('Encrypting with vault secret %s' % secret) + b_ciphertext = this_cipher.encrypt(b_plaintext, secret) # format the data for output to the file - b_vaulttext = self._format_output(b_ciphertext) + b_vaulttext = format_vaulttext_envelope(b_ciphertext, + self.cipher_name, + vault_id=vault_id) return b_vaulttext def decrypt(self, vaulttext, filename=None): @@ -213,8 +480,8 @@ class VaultLib: """ b_vaulttext = to_bytes(vaulttext, errors='strict', encoding='utf-8') - if self.b_password is None: - raise AnsibleError("A vault password must be specified to decrypt data") + if self.secrets is None: + raise AnsibleVaultError("A vault password must be specified to decrypt data") if not is_encrypted(b_vaulttext): msg = "input is not vault encrypted data" @@ -222,73 +489,84 @@ class VaultLib: msg += "%s is not a vault encrypted file" % filename raise AnsibleError(msg) - # clean out header - b_vaulttext = self._split_header(b_vaulttext) + b_vaulttext, b_version, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext) - # create the cipher object - if self.cipher_name in CIPHER_WHITELIST: - this_cipher = CIPHER_MAPPING[self.cipher_name]() + # create the cipher object, note that the cipher used for decrypt can + # be different than the cipher used for encrypt + if cipher_name in CIPHER_WHITELIST: + this_cipher = CIPHER_MAPPING[cipher_name]() else: - raise AnsibleError("{0} cipher could not be found".format(self.cipher_name)) - - # try to unencrypt vaulttext - b_plaintext = this_cipher.decrypt(b_vaulttext, self.b_password) - if b_plaintext is None: - msg = "Decryption failed" - if filename: - msg += " on %s" % filename - raise AnsibleError(msg) + raise AnsibleError("{0} cipher could not be found".format(cipher_name)) - return b_plaintext + b_plaintext = None - def _format_output(self, b_ciphertext): - """ Add header and format to 80 columns + if not self.secrets: + raise AnsibleVaultError('Attempting to decrypt but no vault secrets found') - :arg b_vaulttext: the encrypted and hexlified data as a byte string - :returns: a byte str that should be dumped into a file. It's - formatted to 80 char columns and has the header prepended - """ + # WARNING: Currently, the vault id is not required to match the vault id in the vault blob to + # decrypt a vault properly. The vault id in the vault blob is not part of the encrypted + # or signed vault payload. There is no cryptographic checking/verification/validation of the + # vault blobs vault id. It can be tampered with and changed. The vault id is just a nick + # name to use to pick the best secret and provide some ux/ui info. - if not self.cipher_name: - raise AnsibleError("the cipher must be set before adding a header") + # iterate over all the applicable secrets (all of them by default) until one works... + # if we specify a vault_id, only the corresponding vault secret is checked and + # we check it first. - header = b';'.join([b_HEADER, self.b_version, - to_bytes(self.cipher_name, 'utf-8', errors='strict')]) - b_vaulttext = [header] - b_vaulttext += [b_ciphertext[i:i + 80] for i in range(0, len(b_ciphertext), 80)] - b_vaulttext += [b''] - b_vaulttext = b'\n'.join(b_vaulttext) - - return b_vaulttext - - def _split_header(self, b_vaulttext): - """Retrieve information about the Vault and clean the data - - When data is saved, it has a header prepended and is formatted into 80 - character lines. This method extracts the information from the header - and then removes the header and the inserted newlines. The string returned - is suitable for processing by the Cipher classes. - - :arg b_vaulttext: byte str containing the data from a save file - :returns: a byte str suitable for passing to a Cipher class's - decrypt() function. - """ - # used by decrypt + vault_id_matchers = [] - b_tmpdata = b_vaulttext.split(b'\n') - b_tmpheader = b_tmpdata[0].strip().split(b';') + if vault_id: + display.vvvvv('Found a vault_id (%s) in the vaulttext' % (vault_id)) + vault_id_matchers.append(vault_id) + _matches = match_secrets(self.secrets, vault_id_matchers) + if _matches: + display.vvvvv('We have a secret associated with vault id (%s), will try to use to decrypt %s' % (vault_id, filename)) + else: + display.vvvvv('Found a vault_id (%s) in the vault text, but we do not have a associated secret (--vault-id)' % (vault_id)) + + # Not adding the other secrets to vault_secret_ids enforces a match between the vault_id from the vault_text and + # the known vault secrets. + if not C.DEFAULT_VAULT_ID_MATCH: + # Add all of the known vault_ids as candidates for decrypting a vault. + vault_id_matchers.extend([_vault_id for _vault_id, _secret in self.secrets if _vault_id != vault_id]) + + matched_secrets = match_secrets(self.secrets, vault_id_matchers) + + # for vault_secret_id in vault_secret_ids: + for vault_secret_id, vault_secret in matched_secrets: + display.vvvvv('Trying to use vault secret=(%s) id=%s to decrypt %s' % (vault_secret, vault_secret_id, filename)) + + try: + # secret = self.secrets[vault_secret_id] + display.vvvv('Trying secret %s for vault_id=%s' % (vault_secret, vault_secret_id)) + b_plaintext = this_cipher.decrypt(b_vaulttext, vault_secret) + if b_plaintext is not None: + display.vvvvv('decrypt succesful with secret=%s and vault_id=%s' % (vault_secret, vault_secret_id)) + break + except AnsibleError as e: + display.vvvv('Tried to use the vault secret (%s) to decrypt (%s) but it failed. Error: %s' % + (vault_secret_id, filename, e)) + continue + else: + msg = "Decryption failed (no vault secrets would found that could decrypt)" + if filename: + msg += " on %s" % filename + raise AnsibleVaultError(msg) - self.b_version = b_tmpheader[1].strip() - self.cipher_name = to_text(b_tmpheader[2].strip()) - b_ciphertext = b''.join(b_tmpdata[1:]) + if b_plaintext is None: + msg = "Decryption failed" + if filename: + msg += " on %s" % filename + raise AnsibleError(msg) - return b_ciphertext + return b_plaintext class VaultEditor: - def __init__(self, b_password): - self.vault = VaultLib(b_password) + def __init__(self, vault=None): + # TODO: it may be more useful to just make VaultSecrets and index of VaultLib objects... + self.vault = vault or VaultLib() # TODO: mv shred file stuff to it's own class def _shred_file_custom(self, tmp_path): @@ -358,7 +636,8 @@ class VaultEditor: os.remove(tmp_path) - def _edit_file_helper(self, filename, existing_data=None, force_save=False): + def _edit_file_helper(self, filename, secret, + existing_data=None, force_save=False, vault_id=None): # Create a tempfile fd, tmp_path = tempfile.mkstemp() @@ -385,7 +664,7 @@ class VaultEditor: # encrypt new data and write out to tmp # An existing vaultfile will always be UTF-8, # so decode to unicode here - b_ciphertext = self.vault.encrypt(b_tmpdata) + b_ciphertext = self.vault.encrypt(b_tmpdata, secret, vault_id=vault_id) self.write_data(b_ciphertext, tmp_path) # shuffle tmp file into place @@ -399,13 +678,13 @@ class VaultEditor: real_path = os.path.realpath(filename) return real_path - def encrypt_bytes(self, b_plaintext): + def encrypt_bytes(self, b_plaintext, secret, vault_id=None): - b_ciphertext = self.vault.encrypt(b_plaintext) + b_ciphertext = self.vault.encrypt(b_plaintext, secret, vault_id=vault_id) return b_ciphertext - def encrypt_file(self, filename, output_file=None): + def encrypt_file(self, filename, secret, vault_id=None, output_file=None): # A file to be encrypted into a vaultfile could be any encoding # so treat the contents as a byte string. @@ -414,7 +693,7 @@ class VaultEditor: filename = self._real_path(filename) b_plaintext = self.read_data(filename) - b_ciphertext = self.vault.encrypt(b_plaintext) + b_ciphertext = self.vault.encrypt(b_plaintext, secret, vault_id=vault_id) self.write_data(b_ciphertext, output_file or filename) def decrypt_file(self, filename, output_file=None): @@ -425,12 +704,12 @@ class VaultEditor: ciphertext = self.read_data(filename) try: - plaintext = self.vault.decrypt(ciphertext) + plaintext = self.vault.decrypt(ciphertext, filename=filename) except AnsibleError as e: raise AnsibleError("%s for %s" % (to_bytes(e), to_bytes(filename))) self.write_data(plaintext, output_file or filename, shred=False) - def create_file(self, filename): + def create_file(self, filename, secret, vault_id=None): """ create a new encrypted file """ # FIXME: If we can raise an error here, we can probably just make it @@ -438,58 +717,80 @@ class VaultEditor: if os.path.isfile(filename): raise AnsibleError("%s exists, please use 'edit' instead" % filename) - self._edit_file_helper(filename) + self._edit_file_helper(filename, secret, vault_id=vault_id) def edit_file(self, filename): # follow the symlink filename = self._real_path(filename) - ciphertext = self.read_data(filename) + b_vaulttext = self.read_data(filename) + + # vault or yaml files are always utf8 + vaulttext = to_text(b_vaulttext) try: - plaintext = self.vault.decrypt(ciphertext) + # vaulttext gets converted back to bytes, but alas + plaintext = self.vault.decrypt(vaulttext) except AnsibleError as e: raise AnsibleError("%s for %s" % (to_bytes(e), to_bytes(filename))) + # Figure out the vault id from the file, to select the right secret to re-encrypt it + # (duplicates parts of decrypt, but alas...) + b_ciphertext, b_version, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext) + + # if we could decrypt, the vault_id should be in secrets + # though we could have multiple secrets for a given vault_id, pick the first one + secrets = match_secrets(self.vault.secrets, [vault_id]) + secret = secrets[0][1] if self.vault.cipher_name not in CIPHER_WRITE_WHITELIST: # we want to get rid of files encrypted with the AES cipher - self._edit_file_helper(filename, existing_data=plaintext, force_save=True) + self._edit_file_helper(filename, secret, existing_data=plaintext, force_save=True) else: - self._edit_file_helper(filename, existing_data=plaintext, force_save=False) + self._edit_file_helper(filename, secret, existing_data=plaintext, force_save=False) def plaintext(self, filename): - ciphertext = self.read_data(filename) + b_vaulttext = self.read_data(filename) + vaulttext = to_text(b_vaulttext) try: - plaintext = self.vault.decrypt(ciphertext) + plaintext = self.vault.decrypt(vaulttext) + return plaintext except AnsibleError as e: - raise AnsibleError("%s for %s" % (to_bytes(e), to_bytes(filename))) + raise AnsibleVaultError("%s for %s" % (to_bytes(e), to_bytes(filename))) - return plaintext - - def rekey_file(self, filename, b_new_password): + # FIXME/TODO: make this use VaultSecret + def rekey_file(self, filename, new_vault_secret, new_vault_id=None): # follow the symlink filename = self._real_path(filename) prev = os.stat(filename) - ciphertext = self.read_data(filename) + b_vaulttext = self.read_data(filename) + vaulttext = to_text(b_vaulttext) try: - plaintext = self.vault.decrypt(ciphertext) + plaintext = self.vault.decrypt(vaulttext) except AnsibleError as e: raise AnsibleError("%s for %s" % (to_bytes(e), to_bytes(filename))) # This is more or less an assert, see #18247 - if b_new_password is None: + if new_vault_secret is None: raise AnsibleError('The value for the new_password to rekey %s with is not valid' % filename) - new_vault = VaultLib(b_new_password) - new_ciphertext = new_vault.encrypt(plaintext) + # FIXME: VaultContext...? could rekey to a different vault_id in the same VaultSecrets - self.write_data(new_ciphertext, filename) + # Need a new VaultLib because the new vault data can be a different + # vault lib format or cipher (for ex, when we migrate 1.0 style vault data to + # 1.1 style data we change the version and the cipher). This is where a VaultContext might help + + # the new vault will only be used for encrypting, so it doesn't need the vault secrets + # (we will pass one in directly to encrypt) + new_vault = VaultLib(secrets={}) + b_new_vaulttext = new_vault.encrypt(plaintext, new_vault_secret, vault_id=new_vault_id) + + self.write_data(b_new_vaulttext, filename) # preserve permissions os.chmod(filename, prev.st_mode) @@ -565,8 +866,8 @@ class VaultEditor: os.chown(dest, prev.st_uid, prev.st_gid) def _editor_shell_command(self, filename): - EDITOR = os.environ.get('EDITOR', 'vi') - editor = shlex.split(EDITOR) + env_editor = os.environ.get('EDITOR', 'vi') + editor = shlex.split(env_editor) editor.append(filename) return editor @@ -612,15 +913,26 @@ class VaultAES: raise AnsibleError("Encryption disabled for deprecated VaultAES class") + @staticmethod + def _parse_plaintext_envelope(b_envelope): + # split out sha and verify decryption + b_split_data = b_envelope.split(b"\n", 1) + b_this_sha = b_split_data[0] + b_plaintext = b_split_data[1] + b_test_sha = to_bytes(sha256(b_plaintext).hexdigest()) + + return b_plaintext, b_this_sha, b_test_sha + @classmethod def _decrypt_cryptography(cls, b_salt, b_ciphertext, b_password, key_length): + bs = algorithms.AES.block_size // 8 b_key, b_iv = cls._aes_derive_key_and_iv(b_password, b_salt, key_length, bs) cipher = C_Cipher(algorithms.AES(b_key), modes.CBC(b_iv), CRYPTOGRAPHY_BACKEND).decryptor() unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() try: - b_plaintext = unpadder.update( + b_plaintext_envelope = unpadder.update( cipher.update(b_ciphertext) + cipher.finalize() ) + unpadder.finalize() except ValueError: @@ -628,11 +940,7 @@ class VaultAES: # password was given raise AnsibleError("Decryption failed") - # split out sha and verify decryption - b_split_data = b_plaintext.split(b"\n", 1) - b_this_sha = b_split_data[0] - b_plaintext = b_split_data[1] - b_test_sha = to_bytes(sha256(b_plaintext).hexdigest()) + b_plaintext, b_this_sha, b_test_sha = cls._parse_plaintext_envelope(b_plaintext_envelope) if b_this_sha != b_test_sha: raise AnsibleError("Decryption failed") @@ -646,6 +954,7 @@ class VaultAES: out_file = BytesIO() bs = AES_pycrypto.block_size + b_key, b_iv = cls._aes_derive_key_and_iv(b_password, b_salt, key_length, bs) cipher = AES_pycrypto.new(b_key, AES_pycrypto.MODE_CBC, b_iv) b_next_chunk = b'' @@ -667,14 +976,10 @@ class VaultAES: # reset the stream pointer to the beginning out_file.seek(0) - b_out_data = out_file.read() + b_plaintext_envelope = out_file.read() out_file.close() - # split out sha and verify decryption - b_split_data = b_out_data.split(b"\n", 1) - b_this_sha = b_split_data[0] - b_plaintext = b_split_data[1] - b_test_sha = to_bytes(sha256(b_plaintext).hexdigest()) + b_plaintext, b_this_sha, b_test_sha = cls._parse_plaintext_envelope(b_plaintext_envelope) if b_this_sha != b_test_sha: raise AnsibleError("Decryption failed") @@ -682,7 +987,7 @@ class VaultAES: return b_plaintext @classmethod - def decrypt(cls, b_vaulttext, b_password, key_length=32): + def decrypt(cls, b_vaulttext, secret, key_length=32, vault_id=None): """ Decrypt the given data and return it :arg b_data: A byte string containing the encrypted data @@ -700,6 +1005,8 @@ class VaultAES: b_salt = b_vaultdata[len(b'Salted__'):16] b_ciphertext = b_vaultdata[16:] + b_password = secret.bytes + if HAS_CRYPTOGRAPHY: b_plaintext = cls._decrypt_cryptography(b_salt, b_ciphertext, b_password, key_length) elif HAS_PYCRYPTO: @@ -789,7 +1096,7 @@ class VaultAES256: hmac.update(b_ciphertext) b_hmac = hmac.finalize() - return hexlify(b_hmac), hexlify(b_ciphertext) + return to_bytes(hexlify(b_hmac), errors='surrogate_or_strict'), hexlify(b_ciphertext) @staticmethod def _encrypt_pycrypto(b_plaintext, b_salt, b_key1, b_key2, b_iv): @@ -820,8 +1127,11 @@ class VaultAES256: return to_bytes(hmac.hexdigest(), errors='surrogate_or_strict'), hexlify(b_ciphertext) @classmethod - def encrypt(cls, b_plaintext, b_password): + def encrypt(cls, b_plaintext, secret): + if secret is None: + raise AnsibleVaultError('The secret passed to encrypt() was None') b_salt = os.urandom(32) + b_password = secret.bytes b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt) if HAS_CRYPTOGRAPHY: @@ -837,15 +1147,16 @@ class VaultAES256: b_vaulttext = hexlify(b_vaulttext) return b_vaulttext - @staticmethod - def _decrypt_cryptography(b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv): + @classmethod + def _decrypt_cryptography(cls, b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv): + # b_key1, b_key2, b_iv = self._gen_key_initctr(b_password, b_salt) # EXIT EARLY IF DIGEST DOESN'T MATCH hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND) hmac.update(b_ciphertext) try: hmac.verify(unhexlify(b_crypted_hmac)) - except InvalidSignature: - return None + except InvalidSignature as e: + raise AnsibleVaultError('HMAC verification failed: %s' % e) cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND) decryptor = cipher.decryptor() @@ -904,12 +1215,19 @@ class VaultAES256: return b_plaintext @classmethod - def decrypt(cls, b_vaulttext, b_password): + def decrypt(cls, b_vaulttext, secret): # SPLIT SALT, DIGEST, AND DATA b_vaulttext = unhexlify(b_vaulttext) b_salt, b_crypted_hmac, b_ciphertext = b_vaulttext.split(b"\n", 2) b_salt = unhexlify(b_salt) b_ciphertext = unhexlify(b_ciphertext) + + # TODO: would be nice if a VaultSecret could be passed directly to _decrypt_* + # (move _gen_key_initctr() to a AES256 VaultSecret or VaultContext impl?) + # though, likely needs to be python cryptography specific impl that basically + # creates a Cipher() with b_key1, a Mode.CTR() with b_iv, and a HMAC() with sign key b_key2 + b_password = secret.bytes + b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt) if HAS_CRYPTOGRAPHY: @@ -921,6 +1239,7 @@ class VaultAES256: return b_plaintext + # Keys could be made bytes later if the code that gets the data is more # naturally byte-oriented CIPHER_MAPPING = { diff --git a/lib/ansible/parsing/yaml/constructor.py b/lib/ansible/parsing/yaml/constructor.py index 67530892832..52ad8fc0f04 100644 --- a/lib/ansible/parsing/yaml/constructor.py +++ b/lib/ansible/parsing/yaml/constructor.py @@ -23,9 +23,10 @@ from yaml.constructor import SafeConstructor, ConstructorError from yaml.nodes import MappingNode from ansible.module_utils._text import to_bytes -from ansible.parsing.vault import VaultLib -from ansible.parsing.yaml.objects import AnsibleMapping, AnsibleSequence, AnsibleUnicode, AnsibleVaultEncryptedUnicode +from ansible.parsing.yaml.objects import AnsibleMapping, AnsibleSequence, AnsibleUnicode +from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode from ansible.utils.unsafe_proxy import wrap_var +from ansible.parsing.vault import VaultLib, parse_vaulttext_envelope try: @@ -36,12 +37,12 @@ except ImportError: class AnsibleConstructor(SafeConstructor): - def __init__(self, file_name=None, b_vault_password=None): - self._b_vault_password = b_vault_password + def __init__(self, file_name=None, vault_secrets=None): self._ansible_file_name = file_name super(AnsibleConstructor, self).__init__() self._vaults = {} - self._vaults['default'] = VaultLib(b_password=self._b_vault_password) + self.vault_secrets = vault_secrets or [] + self._vaults['default'] = VaultLib(secrets=self.vault_secrets) def construct_yaml_map(self, node): data = AnsibleMapping() @@ -96,17 +97,16 @@ class AnsibleConstructor(SafeConstructor): def construct_vault_encrypted_unicode(self, node): value = self.construct_scalar(node) - ciphertext_data = to_bytes(value) - - if self._b_vault_password is None: + b_ciphertext_data = to_bytes(value) + # could pass in a key id here to choose the vault to associate with + # TODO/FIXME: plugin vault selector + vault = self._vaults['default'] + if vault.secrets is None: raise ConstructorError(context=None, context_mark=None, problem="found !vault but no vault password provided", problem_mark=node.start_mark, note=None) - - # could pass in a key id here to choose the vault to associate with - vault = self._vaults['default'] - ret = AnsibleVaultEncryptedUnicode(ciphertext_data) + ret = AnsibleVaultEncryptedUnicode(b_ciphertext_data) ret.vault = vault return ret diff --git a/lib/ansible/parsing/yaml/loader.py b/lib/ansible/parsing/yaml/loader.py index 7ff309d82c2..b6650041c41 100644 --- a/lib/ansible/parsing/yaml/loader.py +++ b/lib/ansible/parsing/yaml/loader.py @@ -32,9 +32,9 @@ from ansible.parsing.yaml.constructor import AnsibleConstructor if HAVE_PYYAML_C: class AnsibleLoader(CParser, AnsibleConstructor, Resolver): - def __init__(self, stream, file_name=None, vault_password=None): + def __init__(self, stream, file_name=None, vault_secrets=None): CParser.__init__(self, stream) - AnsibleConstructor.__init__(self, file_name=file_name, b_vault_password=vault_password) + AnsibleConstructor.__init__(self, file_name=file_name, vault_secrets=vault_secrets) Resolver.__init__(self) else: from yaml.composer import Composer @@ -43,10 +43,10 @@ else: from yaml.parser import Parser class AnsibleLoader(Reader, Scanner, Parser, Composer, AnsibleConstructor, Resolver): - def __init__(self, stream, file_name=None, vault_password=None): + def __init__(self, stream, file_name=None, vault_secrets=None): Reader.__init__(self, stream) Scanner.__init__(self) Parser.__init__(self) Composer.__init__(self) - AnsibleConstructor.__init__(self, file_name=file_name, b_vault_password=vault_password) + AnsibleConstructor.__init__(self, file_name=file_name, vault_secrets=vault_secrets) Resolver.__init__(self) diff --git a/lib/ansible/parsing/yaml/objects.py b/lib/ansible/parsing/yaml/objects.py index bd02090c01d..45cbb4d5dd0 100644 --- a/lib/ansible/parsing/yaml/objects.py +++ b/lib/ansible/parsing/yaml/objects.py @@ -76,11 +76,11 @@ class AnsibleVaultEncryptedUnicode(yaml.YAMLObject, AnsibleBaseYAMLObject): yaml_tag = u'!vault' @classmethod - def from_plaintext(cls, seq, vault): + def from_plaintext(cls, seq, vault, secret): if not vault: raise vault.AnsibleVaultError('Error creating AnsibleVaultEncryptedUnicode, invalid vault (%s) provided' % vault) - ciphertext = vault.encrypt(seq) + ciphertext = vault.encrypt(seq, secret) avu = cls(ciphertext) avu.vault = vault return avu diff --git a/test/integration/targets/vault/encrypted_file_encrypted_var_password b/test/integration/targets/vault/encrypted_file_encrypted_var_password new file mode 100644 index 00000000000..57bc06e3c1a --- /dev/null +++ b/test/integration/targets/vault/encrypted_file_encrypted_var_password @@ -0,0 +1 @@ +test-encrypted-file-password diff --git a/test/integration/targets/vault/example1_password b/test/integration/targets/vault/example1_password new file mode 100644 index 00000000000..e723c8f9ae6 --- /dev/null +++ b/test/integration/targets/vault/example1_password @@ -0,0 +1 @@ +example1 diff --git a/test/integration/targets/vault/example2_password b/test/integration/targets/vault/example2_password new file mode 100644 index 00000000000..7b010f8743b --- /dev/null +++ b/test/integration/targets/vault/example2_password @@ -0,0 +1 @@ +example2 diff --git a/test/integration/targets/vault/example3_password b/test/integration/targets/vault/example3_password new file mode 100644 index 00000000000..f5bc5a8c41e --- /dev/null +++ b/test/integration/targets/vault/example3_password @@ -0,0 +1 @@ +example3 diff --git a/test/integration/targets/vault/format_1_2_AES256.yml b/test/integration/targets/vault/format_1_2_AES256.yml new file mode 100644 index 00000000000..1e3795fba20 --- /dev/null +++ b/test/integration/targets/vault/format_1_2_AES256.yml @@ -0,0 +1,6 @@ +$ANSIBLE_VAULT;1.2;AES256;test_vault_id +30383835613535356232333534303264656530633664616233386138396563623939626136366537 +3635323530646538626138383136636437616637616430610a386661346563346136326637656461 +64393364343964633364336666333630383164643662343930663432316333633537353938376437 +6134656262373731390a363166356461376663313532343733326438386632623930313366643038 +6133 diff --git a/test/integration/targets/vault/password-script.py b/test/integration/targets/vault/password-script.py new file mode 100755 index 00000000000..ceb2ac1713f --- /dev/null +++ b/test/integration/targets/vault/password-script.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +# ansible-vault is a script that encrypts/decrypts YAML files. See +# http://docs.ansible.com/playbooks_vault.html for more details. + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys + +PASSWORD = 'test-vault-password' + + +def main(args): + print(PASSWORD) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[:])) diff --git a/test/integration/targets/vault/roles/test_vault_embedded_ids/tasks/main.yml b/test/integration/targets/vault/roles/test_vault_embedded_ids/tasks/main.yml new file mode 100644 index 00000000000..9aeaf24060a --- /dev/null +++ b/test/integration/targets/vault/roles/test_vault_embedded_ids/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- name: set a fact from vault_encrypted_example1_releases + set_fact: + example1_releases: "{{ vault_encrypted_example1_releases }}" + +- name: Assert that a embedded vault of a multiline string with a vault id works + assert: + that: + - "vault_encrypted_example1_releases is defined" + - "example1_releases is defined" + - "example1_releases.startswith('Ansible Releases')" + # - '"{{ vault_encrypted_example1_releases }}" == "Setec Astronomy"' + +- name: Assert that a embedded vault with a different vault id works + assert: + that: + - "vault_encrypted_example2_hello == 'Hello world'" + +- name: Assert that a embedded vault with no vault id and format 1.2 works + assert: + that: + - "vault_encrypted_example3_foobar == 'Foobar'" + #- name: Assert that a multi line embedded vault works, including new line + # assert: + # that: + # - vault_encrypted_var == "Setec Astronomy\n" + +# TODO: add a expected fail here +# - debug: var=vault_encrypted_one_line_var_with_embedded_template diff --git a/test/integration/targets/vault/roles/test_vault_embedded_ids/vars/main.yml b/test/integration/targets/vault/roles/test_vault_embedded_ids/vars/main.yml new file mode 100644 index 00000000000..9c8fa4b2258 --- /dev/null +++ b/test/integration/targets/vault/roles/test_vault_embedded_ids/vars/main.yml @@ -0,0 +1,194 @@ +vault_encrypted_example2_hello: !vault | + $ANSIBLE_VAULT;1.2;AES256;example2 + 30383930326535616363383537613266376364323738313835353566633533353364363837383638 + 3737633764613862343666346337353964613138653036610a313663393231386139343835626436 + 66633336303866323335616661366363333463616530326635383836656432396665313338313737 + 6539616630663262650a383762303362356438616261646564303230633930336563373566623235 + 3566 +vault_encrypted_example1_releases: !vault | + $ANSIBLE_VAULT;1.2;AES256;example1 + 63643833646565393535303862343135326261343362396234656137313731313864316539616462 + 3333313439353638393963643535633835643035383331340a393639386166313838326336363032 + 65396565616531663839316132646230316561613865333437653666323034396337626431663931 + 3339363233356438350a363734616337306136376139346162376334343537613032633563666361 + 36386437356463616563646336393064626131363963643434376439346331663836663961353533 + 62343663623863663830663531663930636532653165636238636433373835623435313632313030 + 33333734343566393739393661383430623063323132303132306361666433386166633564626434 + 62666361653465616636646335353230373961393863373261633461303233313965346565643434 + 63383633303131643730366233383264373865376562623962636562343732343266636535356362 + 62396635613231336162393630343136663731366665623835303762636161393163373361383634 + 65333739326264346136333337363666396336353065366161316130653738356133646364316130 + 32346636386665633131376662356238386161373565336430623263353036323561633235303135 + 35333031316366373636326665656230343934383334303863643364613364663436383030373237 + 35323964376564313636643633303262633033363633663966393535613064343364313161383061 + 66393733366463393936663033633038653465636539356266353936373162303661613962393662 + 61313534643064366432333166666130663730653333613964316130363135646532303531376537 + 63313339623337363464343637323431336438636337386264303961333139326666306365363937 + 36386437343036346165366439636533666237393535316536333966376536623030643663343561 + 64626362363736316234356639663039396634653766646237376636653062383530366562323138 + 61343537616263373137613232393731363866653038633932643163633732326463656365346535 + 63316337346636326631326134633339363133393337393035333730663133646332343536636337 + 36626566633162333463613735656564393764356337346535646539373536363933326139626239 + 35386434663636343366303830663531616530616563343737653761616232303865626634646537 + 38383430366131396133636530383865356430343965633062373366383261383231663162323566 + 30373061366533643938383363333266636463383134393264343662623465323164356464666364 + 35636135316333636266313038613239616638343761326332663933356164323635653861346430 + 65616661353162633765666633393139613830626535633462633166376563313236623465626339 + 38663138633664613738656166356431343438653833623132383330656637343661616432623362 + 66643466343663306434353237343737633535343233653765356134373739316234353836303034 + 37336435376135363362323130316338316135633633303861303665393766616537356666653238 + 63366461383334356666633134616436663731633666323261393761363264333430366234353732 + 66333732373236303338333862626537326638393964363965303532353465613638393934313538 + 66323366353064666334626461313933333961613637663332656131383038393264636537643730 + 35626265346363393665663431663036633461613362343330643133333232326664623833626336 + 65353363373962383561396163653361663736383235376661626132386131353137303764623231 + 63326538623231396366356432663537333331343335633531326331616531313039393335313139 + 65376461323434383065383834626535393063363432326233383930626437343961313538303135 + 39386561623662333335313661636637656336353537313466386239613166396436626630376337 + 36633739326336366530643733393962633737343035346536366336643266346162333931633235 + 66643966626262343862393832663132356435343561646634373835306130623637633836633166 + 30313732333963383565373261306232663365363033376431313437326366656264346532666561 + 63386231636634613235333363326166616238613734643739343237303963663539633535356232 + 66393365616165393130356561363733313735336132336166353839303230643437643165353338 + 39663138313130366635386365663830336365646562666635323361373362626339306536313664 + 32383934623533373361666536326131316630616661623839666137656330306433326637386134 + 34393162343535633438643036613831303265646632383231306239646132393338663564653939 + 63613232646230616338316434376663613266303362386631353733623335643034356631383139 + 62613932396132636339393337383065613061306162633831386236323163633439303263393663 + 38616237313761306533636361386161666264333839616463386631633233343132373732636639 + 61326239383961656437646236656336303638656665316633643630393063373964323534643961 + 39383538303234343438363736373136316464643165383361336262303231353937316432366639 + 36613662393736386433356532626162643462313234316230643639333535653064303830373166 + 31393332336539313362373136326639386566343637623633396134643533393839353934613064 + 65396233353363393763363231633462663537626165646666633937343733653932633733313237 + 31323633326463333938343062626361313761646133633865623130323665336634356364366566 + 31626562373662313064306239356336376136306336643961323839313964393734343265306137 + 62663563306665636463356465663432346331323832666163623530666265393164336466383936 + 64653831316162313861373462643264373965623632653430373439656535636365383066643464 + 61366436613631386161306631386331656632636337653864343261643433363438396361373831 + 37363532346564343562356132306432303933643431636539303039306638356537353237323036 + 63366334623438393838383561383937313330303832326330326366303264303437646666613638 + 37653266633362636330656666303437323138346666373265663466616635326366313233323430 + 62616165626239363833613565326264373063376232303837363062616663333461373062323266 + 32626636316465666230626634396431323032323962313437323837336562313438346634656335 + 33613566636461663334623966646465623531653631653565333836613261633534393439613738 + 66356364383637666465336666333962393735643766633836383833396533626635633734326136 + 65656562366337326161303466336232646533346135353332643030383433643662363465633931 + 63323761623537383438333837333733363263663630336264376239336234663866633131376463 + 66663438313439643565316138383439353839366365393238376439626537656535643739373237 + 66666266366533393738363138613437666435366163643835383830643333323730303537313139 + 32313436663932633933353265356431336138306437353936363638643539383236323232326630 + 62323963626138633865376238666264666531613237636232373938303030393632643230336138 + 38663237646637616232343664396136376534313533613364663062356535313766343331616431 + 36616237336532333239386663643538643239613866393631393364306463303131643863363533 + 31356436373062666266656431643038323766383632613939616539663637623164323161633464 + 39666663353339383164363534616330323936333865663564646334373438303061656662656331 + 37633530663666323834383333623136633164326632313938643234326235616461323734353638 + 63393365313334646538373631643266383936333533383630623861343764373863346161316333 + 38356466626234653336326433353234613430623135343739323433326435373663363237643531 + 36626238613832633661343263383962373536353766653631323431393330623634656166333437 + 66376537643836626264383961303465363035336666306165316631316661366637303361656332 + 36616463626135653235393562343464353262616331326539316361393036623134623361383635 + 39383565313433653139663963306362373233313738613933626563333230656239613462363164 + 65396539333833633137313163396635373433303164633463383935663939343266396366666231 + 30353434323837343563613662643632386662616363646630353530386466643939623866626331 + 63613266366135646562653064333166356561626138343364373631376336393931313262323063 + 32653938333837366231343865656239353433663537313763376132613366363333313137323065 + 31666663656539333438343664323062323238353061663439326333366162303636626634313037 + 38366631306438393333356138393730316161336233656239626565366134643535383536613034 + 37343733663631663863643337373462633462666234393063336330306465366637653136393533 + 63336535316438303564613366343565363831666233626466623161356635363464343634303136 + 61616561393861393036353433356364376533656334326433323934643236346133363535613334 + 32626332653362313731643035653335383164303534616537333132356535376233343566313736 + 39353037636530376338383739366230346134643738313037386438613461323934663537666164 + 66353330303730336435313735343333316364373432313030396361343061343632653765646336 + 39666537366537343635396235373433363438393637663166666530356339316334313834363938 + 33393837336265353265303635663363353439343062316363643637623564353261643637306434 + 36393662363737316234323461373763663364356535313165656661613137396366386464663866 + 63653562313539313839613436653137663262346233626464616237373737373736306231383265 + 35323532373631613762616234386162643035613838376264343532396263626562623262363532 + 36303530353137616134346262646464633462646662323262633366393736383834616665666466 + 34393363353135616437346332386634396635363130623337653230666334303630653738633334 + 33316162326335373838643261656561303736363331316134363736393362313734346236306638 + 65343163646264643539643635633761393665623039653232623435383062363462346336613238 + 38306138353832306263356265316236303065626566643134373836303933323130303634393931 + 31633334373064353263353135656433623863636261633664646439336539343636656464306531 + 36373364323637393634623666353730626532613534343638663966313332636437383233303864 + 33356432613638303936653134373338626261353662653930333534643732656130653636316433 + 33653364373636613739353439383066646530303565383432356134396436306134643030643034 + 63323433396238636330383836396364613738616338356563633565613537313138346661636164 + 34333566393738343661663062346433396532613032663331313566333161396230343336346264 + 66333935316630653936346336366336303363376633623034346536643731313136363835303964 + 37346537373236343832306637653563386435363435333537393733333966643461623064316639 + 65323363343338326435633631303037623234303334353366303936373664383762316364663036 + 61353638376335333663343066303961616234336664313732366630343331613537633336316534 + 31656561626430383338353231376263383362333966666363316435373533613138323039363463 + 33363031373035316431353930626632666165376538303638353631303931326262386363376330 + 36333531303235306532363763313233616165646234343235306332383262663261366164623130 + 66613232636264636336313230303261626639316465383265373762346434616362383562633533 + 64346438653161306266663634623666646239383363313862383563386461626264383165373561 + 64383431653061393132623833653337643266663462666462366339363233353335386264383936 + 38396264373833343935653264373631626662653962353438313262633339316537306463663930 + 31613634613535346364643930613739383035336164303064653736663031633135613966656463 + 64333539643534376662666539653766666532333832333430346333613236356534643964383135 + 38326235626164663364366163353434613530306531343735353761396563326536636335326336 + 34613835333362346363623235316564363934333732646435373033613863346565353034306333 + 33643763363838656339396435316162616539623764366163376438656266353137633262613464 + 31393434646435623032383934373262666430616262353165343231666631666238653134396539 + 32323137616639306262366638366536366665633331653363643234643238656338316133613166 + 38343566623137353566306538616639363935303766633732633638356362373463616563663438 + 66346133636562373031316363616662663132636263653037343962313630313535396563313230 + 34613735663838613130346461343166663830623861393634353438376336363961326263333634 + 34646465326238636630316164316339333961333939363139623262396531303665383230363562 + 63626431333365663337323430653230613837396133636431303863366239303531653966653932 + 65363139366637623531306333363465386636366334383734353330626566346532653263633238 + 39383434346665323730366261316433303739313032653638636232666432323930653837643831 + 63393565306538663365616364326334306333346463343330316161616362323063666666373035 + 66383938383238353134386333343437623030363032303531643736353636643165373362363666 + 31363037613064633164346638306231663161626265663535363634336665656163636637393161 + 64313363373965396262386337613533393639353332316234643666613065343939393336366633 + 64303637323531393936386365316366656432346230653066306334626431366335353130663233 + 62303961663362623637303535333432313635303936363462336438663232333862303934383166 + 31626438623963346262376135633434643533316162376633353661356463616538363733346464 + 65646563626139356264363132616161303438653133353961636135333833376364333138353263 + 36613437373365666665643664343666366234636164626437396139393864653031396331303938 + 35323839646265393232326434616233323535396134346465363131366165373163353932363538 + 39353764623463393732346134656539353966643366653765663038323631373432663839396239 + 35623665623661326231643734346134623961663539363436323134333630306663653039653062 + 36623730663538666166363436616131363233643739393966333437643637303737383733356138 + 34343733623137326265343332326437316365346439316137663361373066333166383032396636 + 35623561626139666264373363363965383633653633656464393932666634353962623637643262 + 32323663303861376166656266653962643166326535363237316333663631323235333833636361 + 31633038353265386439313766313966633536346230646566633333646632383938363761373363 + 38353931343136633062303366643930323034616265653030643062333461616637366666336437 + 36346330636666313833346534363461336366393533346338653061356333653839623364336266 + 32373965346363613165383639366365396665353966393262393562353664623231326132363735 + 38386238336135306464366332353035613938313262323739326638623733663030656533383438 + 38316364393030376436313031613936363435633562633862323063643035383030313865396666 + 66646338316262653734633431393862626633643163313732343638313066646163353264653531 + 64346265656363323666656239333466313666373234626261633630653133316639313233303466 + 62353735626634616661396238356138343064386332366361643530613364366365663764393037 + 31613730313234393263653964376262373131383064393133636533656534343431613964663634 + 65656365393439306433313333346234333332346230666462633132313863623765306665306461 + 65633862656637646134353030393637353339646265613731646564333561313431346135626532 + 66646363383932636562343731626164633138386463356634353062323965376235383130633231 + 61623537333030383130623064356662356463646532613339303336666631366539613835646364 + 37636634353430386632656331313936393261643638326162376238326139643939636333366364 + 31626163376436336631 +vault_encrypted_example3_foobar: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 37336431373836376339373763306436396334623061366266353763363766313063363230636138 + 3665663061366436306232323636376261303064616339620a333365323266643364396136626665 + 62363862653134623665326635396563643832636234386266616436626334363839326434383431 + 3330373333366233380a363431386334636164643936313430623661633265346632343331373866 + 3732 +# We dont have a secret for this vaulttext, but nothing references it +# so nothing should ever try to decrypt it. So this is testing that +# we dont require all vaulted vars to be decrypted. +vault_encrypted_example4_unknown_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 64316436303566666563393931613833316533346539373635663031376664366131353264366132 + 3637623935356263643639313562366434383234633232660a353636666134353030646539643139 + 65376235333932353531356666363434313066366161383532363166653762326533323233623431 + 3934393962633637330a356337626634343736313339316365373239663031663938353063326665 + 30643339386131663336366531663031383030313936356631613432336338313962 diff --git a/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/README.md b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/README.md new file mode 100644 index 00000000000..4a75cece977 --- /dev/null +++ b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/README.md @@ -0,0 +1 @@ +file is encrypted with password of 'test-encrypted-file-password' diff --git a/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml new file mode 100644 index 00000000000..e09004a1d9e --- /dev/null +++ b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml @@ -0,0 +1,13 @@ +--- +- name: Assert that a vault encrypted file with embedded vault of a string with no newline works + assert: + that: + - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"' + +- name: Assert that a vault encrypted file with multi line embedded vault works, including new line + assert: + that: + - vault_file_encrypted_with_encrypted_var == "Setec Astronomy\n" + +# TODO: add a expected fail here +# - debug: var=vault_encrypted_one_line_var_with_embedded_template diff --git a/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/vars/main.yml b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/vars/main.yml new file mode 100644 index 00000000000..89cc4a0f8bd --- /dev/null +++ b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/vars/main.yml @@ -0,0 +1,76 @@ +$ANSIBLE_VAULT;1.1;AES256 +31613535653961393639346266636234373833316530373965356161373735666662613137386466 +3365303539306132613861646362396161323962373839640a653030376530316136643961623665 +65643665616338363432383264363730386538353635663339633932353933653132343430613332 +6136663837306333370a643139336230663465346637663032613231656364316533613235623532 +65643738663735636662363565313561646162343865393733663838393239646634633936336262 +39626235616537663934363932323831376539666331353334386636663738643932306239663265 +64646664616331643663326561386638393764313737303865326166373031336665663533373431 +35353736346264616135656164636337363966323935643032646138366166636537333565306230 +65646533623134393633623663336263393533613632663464653663313835306265333139646563 +35393061343266343138333936646364333735373930666262376137396562356231393330313731 +36363164623939393436363564353162373364626536376434626463343161646437316665613662 +38343534363965373735316339643061333931666264353566316235616433666536313065306132 +31623933633533366162323961343662323364353065316235303162306635663435663066393865 +64356634363761333838326331343865653633396665353638633730663134313565653166656131 +33366464396532313635326237363135316230663838393030303963616161393966393836633237 +30333338343031366235396438663838633136666563646161363332663533626662663531653439 +63643435383931663038613637346637383365336431646663366436626333313536396135636566 +31373133363661636338376166356664353366343730373164663361623338383636336464373038 +36306437363139346233623036636330333664323165636538666138306465653435666132623835 +30363266333666626363366465313165643761396562653761313764616562666439366437623766 +33343666623866653461376137353731356530363732386261383863666439333735666638653533 +38393430323961356333383464643036383739663064633461363937336538373539666662653764 +36376266333230666232396665616434303432653562353131383430643533623932363537346435 +33326335663561643564663936323832376634336363373531363666333732643363646130383464 +30656366633863643966656134653833343634383136363539366330336261313736343838663936 +39333835353035386664633331303264356339613933393162393037306565636563386436633532 +34376564343237303166613461383963353030383166326538643932323130643830376165366564 +30366432623761623366653966313865653262363064316130393339393366323539373338306265 +31626564393065303032383161343137636432353061333964613935363865356139313766303039 +32333863353465306265653237396232383330333438303866316362353161383266316633663364 +66353130326237376331656334633965633339303138656263616239323261663864666236323662 +33643463303965313264396463333963376464313838373765633463396534363836366132653437 +30303132633232623265303966316639373664656262636166653438323534326435363966616133 +33663463626536643930623034343237613933623462346635306565623834346532613539383838 +39356339303930663739333236316234666633623961323362323537313833383538363132636165 +31396433386664356532383432666464613137376561396534316665386134333665626430373064 +30626561363731326635393334633837303934653062616461303732316239663764633565353633 +33336161623332383064376538353531343534333836313139376439316564313436623462396134 +31643831656135653234396362653861643933346433646633383130323139353465616430383061 +34623164376436326466333765353037323630356662646364366265303534313764393862653238 +66376365323561643030343534636263386338333566613436383630613561646639616265313465 +66336239303432666361383038323038383663346561356664626634333037313838363732643463 +33373734663933373238363635623336323232313161353861306430323334353836616265623639 +65613436323939643932383537666530306134633435373331623963633436386162306565656433 +35383962633163643837343436383664313565656134646633393237353065666535316561613266 +64653234366462623764313438666466616664303138656565663036376230323763393135323330 +35383861306262356430656531343938643763306663323031636638383762626564616366393434 +33373035363633396230396161623433336530326432343666346332613262376338313731626462 +63616463363831333239643535383936646264336466616635353063383163306564373263656265 +65383466653162626132633463613037343865316639653931633965323637373733653131666233 +35643831646638383232616538656265663365306136343733633535323537653165636665383832 +65303162656238303665346232353136346639316263636264346533356263353066353438323535 +36303236326663303763653137656264336566646161663538383361306138323064336235616438 +32373731643331373239383339326365366337646237643836373238656339646362366239623533 +33306531353863653834666361393161366465626632643061363266353465653964363263613430 +32323132613866343733376437643239316661313330323661633234343630626132383434343461 +61663765383134666330316237633963323463363762383666323866386336316438373461306138 +38613266346532313134386236386131626262663534313935623635343533383831386332343534 +65333963353861656232383134396438613034663333633661346465636436373533346561306661 +33656535613963663938313233333736343036393734373363316236373765343736633635386336 +30323036393431363636316466393561626365366333623431353435633963613935346239666534 +33623037306334343464633932313430616666633631313366356532643938333835333231313039 +65363734336630303861626636613139663130616362333662616532313734393636353963643032 +39626162623933616561383736636466316331346135613063383261373865366232376562316237 +65393563633131653761646365313831646265316233343833653363626465363863363936316664 +63363863363761353264316662643338656432356336326339623961396538643838666330303934 +62343537653262353737316266366134623961323637613338303164383734613034383964623135 +35646130363038356530383638663431663238336337313034303631366538326361646530626138 +34653533383964353866653562666463333961313434373063333163346537636631393138316465 +62656361613365366137346337363830356263633162623466373564346437653036386136333333 +32323863393866373932353534343133306333303265336564383132616365363439393364336562 +62333130343664343436356338623336643735373164373962313762333763343137626238316536 +36376539666331376162376361646631396231306165316362343164616232393864656161393735 +63313439643865346231346363376137306464396637356539353139343932333438323964323035 +326532383066643037653036333166346238 diff --git a/test/integration/targets/vault/runme.sh b/test/integration/targets/vault/runme.sh index c06e50788eb..e0a99844548 100755 --- a/test/integration/targets/vault/runme.sh +++ b/test/integration/targets/vault/runme.sh @@ -9,6 +9,9 @@ trap 'rm -rf "${MYTMPDIR}"' EXIT TEST_FILE="${MYTMPDIR}/test_file" echo "This is a test file" > "${TEST_FILE}" +TEST_FILE_1_2="${MYTMPDIR}/test_file_1_2" +echo "This is a test file for format 1.2" > "${TEST_FILE_1_2}" + TEST_FILE_OUTPUT="${MYTMPDIR}/test_file_output" # old format @@ -38,13 +41,111 @@ set -eux # new format, view ansible-vault view "$@" --vault-password-file vault-password format_1_1_AES256.yml +# new format, view with vault-id +ansible-vault view "$@" --vault-id=vault-password format_1_1_AES256.yml + +# new format, view, using password script +ansible-vault view "$@" --vault-password-file password-script.py format_1_1_AES256.yml + +# new format, view, using password script with vault-id +ansible-vault view "$@" --vault-id password-script.py format_1_1_AES256.yml + +# new 1.2 format, view +ansible-vault view "$@" --vault-password-file vault-password format_1_2_AES256.yml + +# new 1.2 format, view with vault-id +ansible-vault view "$@" --vault-id=test_vault_id@vault-password format_1_2_AES256.yml + +# new 1,2 format, view, using password script +ansible-vault view "$@" --vault-password-file password-script.py format_1_2_AES256.yml + +# new 1.2 format, view, using password script with vault-id +ansible-vault view "$@" --vault-id password-script.py format_1_2_AES256.yml + +# new 1.2 format, view, ENFORCE_IDENTITY_MATCH=true, should fail, no 'test_vault_id' vault_id +ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-password-file vault-password format_1_2_AES256.yml && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + +# new 1.2 format, view with vault-id, ENFORCE_IDENTITY_MATCH=true, should work, 'test_vault_id' is provided +ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-id=test_vault_id@vault-password format_1_2_AES256.yml + +# new 1,2 format, view, using password script, ENFORCE_IDENTITY_MATCH=true, should fail, no 'test_vault_id' +ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-password-file password-script.py format_1_2_AES256.yml && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + + +# new 1.2 format, view, using password script with vault-id, ENFORCE_IDENTITY_MATCH=true, should fail +ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-id password-script.py format_1_2_AES256.yml && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + +# new 1.2 format, view, using password script with vault-id, ENFORCE_IDENTITY_MATCH=true, 'test_vault_id' provided should work +ANSIBLE_VAULT_ID_MATCH=1 ansible-vault view "$@" --vault-id=test_vault_id@password-script.py format_1_2_AES256.yml + # encrypt it ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE}" ansible-vault view "$@" --vault-password-file vault-password "${TEST_FILE}" +# view with multiple vault-password files, including a wrong one +ansible-vault view "$@" --vault-password-file vault-password --vault-password-file vault-password-wrong "${TEST_FILE}" + +# view with multiple vault-password files, including a wrong one, using vault-id +ansible-vault view "$@" --vault-id vault-password --vault-id vault-password-wrong "${TEST_FILE}" + +# And with the password files specified in a different order +ansible-vault view "$@" --vault-password-file vault-password-wrong --vault-password-file vault-password "${TEST_FILE}" + +# And with the password files specified in a different order, using vault-id +ansible-vault view "$@" --vault-id vault-password-wrong --vault-id vault-password "${TEST_FILE}" + +# And with the password files specified in a different order, using --vault-id and non default vault_ids +ansible-vault view "$@" --vault-id test_vault_id@vault-password-wrong --vault-id test_vault_id@vault-password "${TEST_FILE}" + ansible-vault decrypt "$@" --vault-password-file vault-password "${TEST_FILE}" +# encrypt it, using a vault_id so we write a 1.2 format file +ansible-vault encrypt "$@" --vault-id test_vault_1_2@vault-password "${TEST_FILE_1_2}" + +ansible-vault view "$@" --vault-id vault-password "${TEST_FILE_1_2}" +ansible-vault view "$@" --vault-id test_vault_1_2@vault-password "${TEST_FILE_1_2}" + +# view with multiple vault-password files, including a wrong one +ansible-vault view "$@" --vault-id vault-password --vault-id wrong_password@vault-password-wrong "${TEST_FILE_1_2}" + +# And with the password files specified in a different order, using vault-id +ansible-vault view "$@" --vault-id vault-password-wrong --vault-id vault-password "${TEST_FILE_1_2}" + +# And with the password files specified in a different order, using --vault-id and non default vault_ids +ansible-vault view "$@" --vault-id test_vault_id@vault-password-wrong --vault-id test_vault_id@vault-password "${TEST_FILE_1_2}" + +ansible-vault decrypt "$@" --vault-id test_vault_1_2@vault-password "${TEST_FILE_1_2}" + +# multiple vault passwords +ansible-vault view "$@" --vault-password-file vault-password --vault-password-file vault-password-wrong format_1_1_AES256.yml + +# multiple vault passwords, --vault-id +ansible-vault view "$@" --vault-id test_vault_id@vault-password --vault-id test_vault_id@vault-password-wrong format_1_1_AES256.yml + +# encrypt it, with password from password script +ansible-vault encrypt "$@" --vault-password-file password-script.py "${TEST_FILE}" + +ansible-vault view "$@" --vault-password-file password-script.py "${TEST_FILE}" + +ansible-vault decrypt "$@" --vault-password-file password-script.py "${TEST_FILE}" + +# encrypt it, with password from password script +ansible-vault encrypt "$@" --vault-id test_vault_id@password-script.py "${TEST_FILE}" + +ansible-vault view "$@" --vault-id test_vault_id@password-script.py "${TEST_FILE}" + +ansible-vault decrypt "$@" --vault-id test_vault_id@password-script.py "${TEST_FILE}" + # new password file for rekeyed file NEW_VAULT_PASSWORD="${MYTMPDIR}/new-vault-password" echo "newpassword" > "${NEW_VAULT_PASSWORD}" @@ -55,6 +156,18 @@ ansible-vault rekey "$@" --vault-password-file vault-password --new-vault-passwo ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}" +# view with old password file and new password file +ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --vault-password-file vault-password "${TEST_FILE}" + +# view with old password file and new password file, different order +ansible-vault view "$@" --vault-password-file vault-password --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}" + +# view with old password file and new password file and another wrong +ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --vault-password-file vault-password-wrong --vault-password-file vault-password "${TEST_FILE}" + +# view with old password file and new password file and another wrong, using --vault-id +ansible-vault view "$@" --vault-id "tmp_new_password@${NEW_VAULT_PASSWORD}" --vault-id wrong_password@vault-password-wrong --vault-id myorg@vault-password "${TEST_FILE}" + ansible-vault decrypt "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}" # reading/writing to/from stdin/stdin (See https://github.com/ansible/ansible/issues/23567) @@ -66,6 +179,10 @@ ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --name "blippy" "a test string names blippy" +ansible-vault encrypt_string "$@" --vault-id "${NEW_VAULT_PASSWORD}" "a test string" + +ansible-vault encrypt_string "$@" --vault-id "${NEW_VAULT_PASSWORD}" --name "blippy" "a test string names blippy" + # from stdin ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" < "${TEST_FILE}" @@ -85,3 +202,69 @@ ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-pass ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password ansible-playbook test_vaulted_inventory.yml -i vaulted.inventory -v "$@" --vault-password-file vault-password ansible-playbook test_vaulted_template.yml -i ../../inventory -v "$@" --vault-password-file vault-password + +# test with password from password script +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file password-script.py +ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file password-script.py + +# with multiple password files +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password --vault-password-file vault-password-wrong +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-password-file vault-password + +ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password --vault-password-file vault-password-wrong --syntax-check +ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-password-file vault-password + +# test that we can have a vault encrypted yaml file that includes embedded vault vars +# that were encrypted with a different vault secret +ansible-playbook test_vault_file_encrypted_embedded.yml -i ../../inventory "$@" --vault-id encrypted_file_encrypted_var_password --vault-id vault-password + +# with multiple password files, --vault-id, ordering +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id vault-password --vault-id vault-password-wrong +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id vault-password-wrong --vault-id vault-password + +ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-id vault-password --vault-id vault-password-wrong --syntax-check +ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-id vault-password-wrong --vault-id vault-password + +# test with multiple password files, including a script, and a wrong password +ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-password-file password-script.py --vault-password-file vault-password + +# test with multiple password files, including a script, and a wrong password, and a mix of --vault-id and --vault-password-file +ansible-playbook test_vault_embedded.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-id password-script.py --vault-id vault-password + +# test with multiple password files, including a script, and a wrong password, and a mix of --vault-id and --vault-password-file +ansible-playbook test_vault_embedded_ids.yml -i ../../inventory -v "$@" \ + --vault-password-file vault-password-wrong \ + --vault-id password-script.py --vault-id example1@example1_password \ + --vault-id example2@example2_password --vault-password-file example3_password \ + --vault-id vault-password + +# with wrong password +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + +# with multiple wrong passwords +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password-wrong --vault-password-file vault-password-wrong && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + +# with wrong password, --vault-id +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id vault-password-wrong && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + +# with multiple wrong passwords with --vault-id +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id vault-password-wrong --vault-id vault-password-wrong && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + +# with multiple wrong passwords with --vault-id +ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-id wrong1@vault-password-wrong --vault-id wrong2@vault-password-wrong && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + diff --git a/test/integration/targets/vault/test_vault_embedded_ids.yml b/test/integration/targets/vault/test_vault_embedded_ids.yml new file mode 100644 index 00000000000..23ebbb969f2 --- /dev/null +++ b/test/integration/targets/vault/test_vault_embedded_ids.yml @@ -0,0 +1,4 @@ +- hosts: testhost + gather_facts: False + roles: + - { role: test_vault_embedded_ids, tags: test_vault_embedded_ids} diff --git a/test/integration/targets/vault/test_vault_file_encrypted_embedded.yml b/test/integration/targets/vault/test_vault_file_encrypted_embedded.yml new file mode 100644 index 00000000000..685d20ef7de --- /dev/null +++ b/test/integration/targets/vault/test_vault_file_encrypted_embedded.yml @@ -0,0 +1,4 @@ +- hosts: testhost + gather_facts: False + roles: + - { role: test_vault_file_encrypted_embedded, tags: test_vault_file_encrypted_embedded} diff --git a/test/units/cli/test_cli.py b/test/units/cli/test_cli.py index 69e929b61b4..4692f128baa 100644 --- a/test/units/cli/test_cli.py +++ b/test/units/cli/test_cli.py @@ -20,8 +20,12 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch, MagicMock + +from units.mock.loader import DictDataLoader from ansible.release import __version__ +from ansible.parsing import vault from ansible import cli @@ -39,3 +43,43 @@ class TestCliVersion(unittest.TestCase): def test_version_info_gitinfo(self): version_info = cli.CLI.version_info(gitinfo=True) self.assertIn('python version', version_info['string']) + + +class TestCliSetupVaultSecrets(unittest.TestCase): + def test(self): + res = cli.CLI.setup_vault_secrets(None, None) + self.assertIsInstance(res, list) + + @patch('ansible.cli.get_file_vault_secret') + def test_password_file(self, mock_file_secret): + filename = '/dev/null/secret' + mock_file_secret.return_value = MagicMock(bytes=b'file1_password', + vault_id='file1', + filename=filename) + fake_loader = DictDataLoader({}) + res = cli.CLI.setup_vault_secrets(loader=fake_loader, + vault_ids=['secret1@%s' % filename, 'secret2'], + vault_password_files=[filename]) + self.assertIsInstance(res, list) + matches = vault.match_secrets(res, ['secret1']) + self.assertIn('secret1', [x[0] for x in matches]) + match = matches[0][1] + self.assertEqual(match.bytes, b'file1_password') + + @patch('ansible.cli.PromptVaultSecret') + def test_prompt(self, mock_prompt_secret): + mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password', + vault_id='prompt1') + + fake_loader = DictDataLoader({}) + res = cli.CLI.setup_vault_secrets(loader=fake_loader, + vault_ids=['prompt1@prompt'], + ask_vault_pass=True) + + self.assertIsInstance(res, list) + matches = vault.match_secrets(res, ['prompt1']) + self.assertIn('prompt1', [x[0] for x in matches]) + # self.assertIn('prompt1', res) + # self.assertIn('secret1', res) + match = matches[0][1] + self.assertEqual(match.bytes, b'prompt1_password') diff --git a/test/units/cli/test_vault.py b/test/units/cli/test_vault.py new file mode 100644 index 00000000000..4f703121a5d --- /dev/null +++ b/test/units/cli/test_vault.py @@ -0,0 +1,166 @@ +# (c) 2017, Adrian Likins +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch +from units.mock.vault_helper import TextVaultSecret + +from ansible import errors +from ansible.cli.vault import VaultCLI + + +# TODO: make these tests assert something, likely by verifing +# mock calls + + +class TestVaultCli(unittest.TestCase): + def test_parse_empty(self): + cli = VaultCLI([]) + self.assertRaisesRegexp(errors.AnsibleOptionsError, + '.*Missing required action.*', + cli.parse) + + # FIXME: something weird seems to be afoot when parsing actions + # cli = VaultCLI(args=['view', '/dev/null/foo', 'mysecret3']) + # will skip '/dev/null/foo'. something in cli.CLI.set_action() ? + # maybe we self.args gets modified in a loop? + def test_parse_view_file(self): + cli = VaultCLI(args=['ansible-vault', 'view', '/dev/null/foo']) + cli.parse() + + def test_view_missing_file_no_secret(self): + cli = VaultCLI(args=['ansible-vault', 'view', '/dev/null/foo']) + cli.parse() + self.assertRaisesRegexp(errors.AnsibleOptionsError, + "A vault password is required to use Ansible's Vault", + cli.run) + + def test_encrypt_missing_file_no_secret(self): + cli = VaultCLI(args=['ansible-vault', 'encrypt', '/dev/null/foo']) + cli.parse() + self.assertRaisesRegexp(errors.AnsibleOptionsError, + "A vault password is required to use Ansible's Vault", + cli.run) + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_encrypt(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'encrypt', '/dev/null/foo']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_encrypt_string(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'encrypt_string', + 'some string to encrypt']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + @patch('ansible.cli.vault.display.prompt', return_value='a_prompt') + def test_encrypt_string_prompt(self, mock_display, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', + 'encrypt_string', + '--prompt', + 'some string to encrypt']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + @patch('ansible.cli.vault.sys.stdin.read', return_value='This is data from stdin') + def test_encrypt_string_stdin(self, mock_stdin_read, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', + 'encrypt_string', + '--stdin-name', + 'the_var_from_stdin', + '-']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_encrypt_string_names(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'encrypt_string', + '--name', 'foo1', + '--name', 'foo2', + 'some string to encrypt']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_encrypt_string_more_args_than_names(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'encrypt_string', + '--name', 'foo1', + 'some string to encrypt', + 'other strings', + 'a few more string args']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_create(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'create', '/dev/null/foo']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_edit(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'edit', '/dev/null/foo']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_decrypt(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'decrypt', '/dev/null/foo']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_view(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'view', '/dev/null/foo']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_rekey(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'rekey', '/dev/null/foo']) + cli.parse() + cli.run() diff --git a/test/units/mock/loader.py b/test/units/mock/loader.py index e120fe00eea..d05dd145051 100644 --- a/test/units/mock/loader.py +++ b/test/units/mock/loader.py @@ -35,6 +35,7 @@ class DictDataLoader(DataLoader): self._file_mapping = file_mapping self._build_known_directories() + self._vault_secrets = None def load_from_file(self, path, unsafe=False): if path in self._file_mapping: @@ -98,3 +99,6 @@ class DictDataLoader(DataLoader): def get_basedir(self): return os.getcwd() + + def set_vault_secrets(self, vault_secrets): + self._vault_secrets = vault_secrets diff --git a/test/units/mock/vault_helper.py b/test/units/mock/vault_helper.py new file mode 100644 index 00000000000..dcce9c78415 --- /dev/null +++ b/test/units/mock/vault_helper.py @@ -0,0 +1,39 @@ +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils._text import to_bytes + +from ansible.parsing.vault import VaultSecret + + +class TextVaultSecret(VaultSecret): + '''A secret piece of text. ie, a password. Tracks text encoding. + + The text encoding of the text may not be the default text encoding so + we keep track of the encoding so we encode it to the same bytes.''' + + def __init__(self, text, encoding=None, errors=None, _bytes=None): + super(TextVaultSecret, self).__init__() + self.text = text + self.encoding = encoding or 'utf-8' + self._bytes = _bytes + self.errors = errors or 'strict' + + @property + def bytes(self): + '''The text encoded with encoding, unless we specifically set _bytes.''' + return self._bytes or to_bytes(self.text, encoding=self.encoding, errors=self.errors) diff --git a/test/units/parsing/test_dataloader.py b/test/units/parsing/test_dataloader.py index 92d930cfbf2..a7a16157c3f 100644 --- a/test/units/parsing/test_dataloader.py +++ b/test/units/parsing/test_dataloader.py @@ -26,6 +26,8 @@ from ansible.compat.tests.mock import patch, mock_open from ansible.errors import AnsibleParserError, yaml_strings from ansible.module_utils._text import to_text from ansible.module_utils.six import PY3 + +from units.mock.vault_helper import TextVaultSecret from ansible.parsing.dataloader import DataLoader from units.mock.path import mock_unfrackpath_noop @@ -118,7 +120,8 @@ class TestDataLoaderWithVault(unittest.TestCase): def setUp(self): self._loader = DataLoader() - self._loader.set_vault_password('ansible') + vault_secrets = [('default', TextVaultSecret('ansible'))] + self._loader.set_vault_secrets(vault_secrets) def tearDown(self): pass diff --git a/test/units/parsing/vault/test_vault.py b/test/units/parsing/vault/test_vault.py index c657e829368..befb4a0e5ac 100644 --- a/test/units/parsing/vault/test_vault.py +++ b/test/units/parsing/vault/test_vault.py @@ -24,6 +24,7 @@ __metaclass__ = type import binascii import io import os +import tempfile from binascii import hexlify import pytest @@ -33,9 +34,141 @@ from ansible.compat.tests import unittest from ansible import errors from ansible.module_utils import six from ansible.module_utils._text import to_bytes, to_text -from ansible.parsing.vault import VaultLib from ansible.parsing import vault +from units.mock.loader import DictDataLoader +from units.mock.vault_helper import TextVaultSecret + + +class TestVaultSecret(unittest.TestCase): + def test(self): + secret = vault.VaultSecret() + secret.load() + self.assertIsNone(secret._bytes) + + def test_bytes(self): + some_text = u'私はガラスを食べられます。それは私を傷つけません。' + _bytes = to_bytes(some_text) + secret = vault.VaultSecret(_bytes) + secret.load() + self.assertEqual(secret.bytes, _bytes) + + +class TestPromptVaultSecret(unittest.TestCase): + def test_empty_prompt_formats(self): + secret = vault.PromptVaultSecret(vault_id='test_id', prompt_formats=[]) + secret.load() + self.assertIsNone(secret._bytes) + + +class TestFileVaultSecret(unittest.TestCase): + def test(self): + secret = vault.FileVaultSecret() + self.assertIsNone(secret._bytes) + self.assertIsNone(secret._text) + + def test_repr_empty(self): + secret = vault.FileVaultSecret() + self.assertEqual(repr(secret), "FileVaultSecret()") + + def test_repr(self): + tmp_file = tempfile.NamedTemporaryFile(delete=False) + fake_loader = DictDataLoader({tmp_file.name: 'sdfadf'}) + + secret = vault.FileVaultSecret(loader=fake_loader, filename=tmp_file.name) + filename = tmp_file.name + tmp_file.close() + self.assertEqual(repr(secret), "FileVaultSecret(filename='%s')" % filename) + + def test_empty_bytes(self): + secret = vault.FileVaultSecret() + self.assertIsNone(secret.bytes) + + def test_file(self): + password = 'some password' + + tmp_file = tempfile.NamedTemporaryFile(delete=False) + tmp_file.write(to_bytes(password)) + tmp_file.close() + + fake_loader = DictDataLoader({tmp_file.name: 'sdfadf'}) + + secret = vault.FileVaultSecret(loader=fake_loader, filename=tmp_file.name) + secret.load() + + os.unlink(tmp_file.name) + + self.assertEqual(secret.bytes, to_bytes(password)) + + def test_file_not_a_directory(self): + filename = '/dev/null/foobar' + fake_loader = DictDataLoader({filename: 'sdfadf'}) + + secret = vault.FileVaultSecret(loader=fake_loader, filename=filename) + self.assertRaisesRegexp(errors.AnsibleError, + '.*Could not read vault password file.*/dev/null/foobar.*Not a directory', + secret.load) + + def test_file_not_found(self): + tmp_file = tempfile.NamedTemporaryFile() + filename = tmp_file.name + tmp_file.close() + + fake_loader = DictDataLoader({filename: 'sdfadf'}) + + secret = vault.FileVaultSecret(loader=fake_loader, filename=filename) + self.assertRaisesRegexp(errors.AnsibleError, + '.*Could not read vault password file.*%s.*' % filename, + secret.load) + + +class TestScriptVaultSecret(unittest.TestCase): + def test(self): + secret = vault.ScriptVaultSecret() + self.assertIsNone(secret._bytes) + self.assertIsNone(secret._text) + + +class TestGetFileVaultSecret(unittest.TestCase): + def test_file(self): + password = 'some password' + + tmp_file = tempfile.NamedTemporaryFile(delete=False) + tmp_file.write(to_bytes(password)) + tmp_file.close() + + fake_loader = DictDataLoader({tmp_file.name: 'sdfadf'}) + + secret = vault.get_file_vault_secret(filename=tmp_file.name, loader=fake_loader) + secret.load() + + os.unlink(tmp_file.name) + + self.assertEqual(secret.bytes, to_bytes(password)) + + def test_file_not_a_directory(self): + filename = '/dev/null/foobar' + fake_loader = DictDataLoader({filename: 'sdfadf'}) + + self.assertRaisesRegexp(errors.AnsibleError, + '.*The vault password file %s was not found.*' % filename, + vault.get_file_vault_secret, + filename=filename, + loader=fake_loader) + + def test_file_not_found(self): + tmp_file = tempfile.NamedTemporaryFile() + filename = tmp_file.name + tmp_file.close() + + fake_loader = DictDataLoader({filename: 'sdfadf'}) + + self.assertRaisesRegexp(errors.AnsibleError, + '.*The vault password file %s was not found.*' % filename, + vault.get_file_vault_secret, + filename=filename, + loader=fake_loader) + class TestVaultIsEncrypted(unittest.TestCase): def test_bytes_not_encrypted(self): @@ -251,11 +384,59 @@ class TestVaultCipherAes256PyCrypto(TestVaultCipherAes256): super(TestVaultCipherAes256PyCrypto, self).tearDown() +class TestMatchSecrets(unittest.TestCase): + def test_empty_tuple(self): + secrets = [tuple()] + vault_ids = ['vault_id_1'] + self.assertRaises(ValueError, + vault.match_secrets, + secrets, vault_ids) + + def test_empty_secrets(self): + matches = vault.match_secrets([], ['vault_id_1']) + self.assertEqual(matches, []) + + def test_single_match(self): + secret = TextVaultSecret('password') + matches = vault.match_secrets([('default', secret)], ['default']) + self.assertEquals(matches, [('default', secret)]) + + def test_no_matches(self): + secret = TextVaultSecret('password') + matches = vault.match_secrets([('default', secret)], ['not_default']) + self.assertEquals(matches, []) + + def test_multiple_matches(self): + secrets = [('vault_id1', TextVaultSecret('password1')), + ('vault_id2', TextVaultSecret('password2')), + ('vault_id1', TextVaultSecret('password3')), + ('vault_id4', TextVaultSecret('password4'))] + vault_ids = ['vault_id1', 'vault_id4'] + matches = vault.match_secrets(secrets, vault_ids) + + self.assertEqual(len(matches), 3) + expected = [('vault_id1', TextVaultSecret('password1')), + ('vault_id1', TextVaultSecret('password3')), + ('vault_id4', TextVaultSecret('password4'))] + self.assertEqual([x for x, y in matches], + [a for a, b in expected]) + + @pytest.mark.skipif(not vault.HAS_CRYPTOGRAPHY, reason="Skipping cryptography tests because cryptography is not installed") class TestVaultLib(unittest.TestCase): def setUp(self): - self.v = VaultLib('test-vault-password') + self.vault_password = "test-vault-password" + text_secret = TextVaultSecret(self.vault_password) + self.vault_secrets = [('default', text_secret), + ('test_id', text_secret)] + self.v = vault.VaultLib(self.vault_secrets) + + def _vault_secrets(self, vault_id, secret): + return [(vault_id, secret)] + + def _vault_secrets_from_password(self, vault_id, password): + return [(vault_id, TextVaultSecret(password))] def test_encrypt(self): plaintext = u'Some text to encrypt in a café' @@ -266,6 +447,15 @@ class TestVaultLib(unittest.TestCase): b_header = b'$ANSIBLE_VAULT;1.1;AES256\n' self.assertEqual(b_vaulttext[:len(b_header)], b_header) + def test_encrypt_vault_id(self): + plaintext = u'Some text to encrypt in a café' + b_vaulttext = self.v.encrypt(plaintext, vault_id='test_id') + + self.assertIsInstance(b_vaulttext, six.binary_type) + + b_header = b'$ANSIBLE_VAULT;1.2;AES256;test_id\n' + self.assertEqual(b_vaulttext[:len(b_header)], b_header) + def test_encrypt_bytes(self): plaintext = to_bytes(u'Some text to encrypt in a café') @@ -276,38 +466,60 @@ class TestVaultLib(unittest.TestCase): b_header = b'$ANSIBLE_VAULT;1.1;AES256\n' self.assertEqual(b_vaulttext[:len(b_header)], b_header) + def test_encrypt_no_secret_empty_secrets(self): + vault_secrets = [] + v = vault.VaultLib(vault_secrets) + + plaintext = u'Some text to encrypt in a café' + self.assertRaisesRegexp(vault.AnsibleVaultError, + '.*A vault password must be specified to encrypt data.*', + v.encrypt, + plaintext) + def test_is_encrypted(self): self.assertFalse(self.v.is_encrypted(b"foobar"), msg="encryption check on plaintext yielded false positive") b_data = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible") self.assertTrue(self.v.is_encrypted(b_data), msg="encryption check on headered text failed") - def test_format_output(self): - self.v.cipher_name = "TEST" + def test_format_vaulttext_envelope(self): + cipher_name = "TEST" b_ciphertext = b"ansible" - b_vaulttext = self.v._format_output(b_ciphertext) + b_vaulttext = vault.format_vaulttext_envelope(b_ciphertext, + cipher_name, + version=self.v.b_version, + vault_id='default') b_lines = b_vaulttext.split(b'\n') self.assertGreater(len(b_lines), 1, msg="failed to properly add header") b_header = b_lines[0] - self.assertTrue(b_header.endswith(b';TEST'), msg="header does not end with cipher name") + # self.assertTrue(b_header.endswith(b';TEST'), msg="header does not end with cipher name") b_header_parts = b_header.split(b';') - self.assertEqual(len(b_header_parts), 3, msg="header has the wrong number of parts") + self.assertEqual(len(b_header_parts), 4, msg="header has the wrong number of parts") self.assertEqual(b_header_parts[0], b'$ANSIBLE_VAULT', msg="header does not start with $ANSIBLE_VAULT") self.assertEqual(b_header_parts[1], self.v.b_version, msg="header version is incorrect") self.assertEqual(b_header_parts[2], b'TEST', msg="header does not end with cipher name") - def test_split_header(self): + # And just to verify, lets parse the results and compare + b_ciphertext2, b_version2, cipher_name2, vault_id2 = \ + vault.parse_vaulttext_envelope(b_vaulttext) + self.assertEqual(b_ciphertext, b_ciphertext2) + self.assertEqual(self.v.b_version, b_version2) + self.assertEqual(cipher_name, cipher_name2) + self.assertEqual('default', vault_id2) + + def test_parse_vaulttext_envelope(self): b_vaulttext = b"$ANSIBLE_VAULT;9.9;TEST\nansible" - b_ciphertext = self.v._split_header(b_vaulttext) + b_ciphertext, b_version, cipher_name, vault_id = vault.parse_vaulttext_envelope(b_vaulttext) b_lines = b_ciphertext.split(b'\n') self.assertEqual(b_lines[0], b"ansible", msg="Payload was not properly split from the header") - self.assertEqual(self.v.cipher_name, u'TEST', msg="cipher name was not properly set") - self.assertEqual(self.v.b_version, b"9.9", msg="version was not properly set") + self.assertEqual(cipher_name, u'TEST', msg="cipher name was not properly set") + self.assertEqual(b_version, b"9.9", msg="version was not properly set") def test_encrypt_decrypt_aes(self): self.v.cipher_name = u'AES' - self.v.b_password = b'ansible' + vault_secrets = self._vault_secrets_from_password('default', 'ansible') + self.v.secrets = vault_secrets # AES encryption code has been removed, so this is old output for # AES-encrypted 'foobar' with password 'ansible'. b_vaulttext = b'''$ANSIBLE_VAULT;1.1;AES @@ -326,6 +538,67 @@ fe3db930508b65e0ff5947e4386b79af8ab094017629590ef6ba486814cf70f8e4ab0ed0c7d2587e self.assertNotEqual(b_vaulttext, b"foobar", msg="encryption failed") self.assertEqual(b_plaintext, b"foobar", msg="decryption failed") + def test_encrypt_decrypt_aes256_none_secrets(self): + vault_secrets = self._vault_secrets_from_password('default', 'ansible') + v = vault.VaultLib(vault_secrets) + + plaintext = u"foobar" + b_vaulttext = v.encrypt(plaintext) + + # VaultLib will default to empty {} if secrets is None + v_none = vault.VaultLib(None) + # so set secrets None explicitly + v_none.secrets = None + self.assertRaisesRegexp(vault.AnsibleVaultError, + '.*A vault password must be specified to decrypt data.*', + v_none.decrypt, + b_vaulttext) + + def test_encrypt_decrypt_aes256_empty_secrets(self): + vault_secrets = self._vault_secrets_from_password('default', 'ansible') + v = vault.VaultLib(vault_secrets) + + plaintext = u"foobar" + b_vaulttext = v.encrypt(plaintext) + + vault_secrets_empty = [] + v_none = vault.VaultLib(vault_secrets_empty) + + self.assertRaisesRegexp(vault.AnsibleVaultError, + '.*Attempting to decrypt but no vault secrets found.*', + v_none.decrypt, + b_vaulttext) + + def test_encrypt_decrypt_aes256_multiple_secrets_all_wrong(self): + plaintext = u'Some text to encrypt in a café' + b_vaulttext = self.v.encrypt(plaintext) + + vault_secrets = [('default', TextVaultSecret('another-wrong-password')), + ('wrong-password', TextVaultSecret('wrong-password'))] + + v_multi = vault.VaultLib(vault_secrets) + self.assertRaisesRegexp(errors.AnsibleError, + '.*Decryption failed.*', + v_multi.decrypt, + b_vaulttext, + filename='/dev/null/fake/filename') + + def test_encrypt_decrypt_aes256_multiple_secrets_one_valid(self): + plaintext = u'Some text to encrypt in a café' + b_vaulttext = self.v.encrypt(plaintext) + + correct_secret = TextVaultSecret(self.vault_password) + wrong_secret = TextVaultSecret('wrong-password') + + vault_secrets = [('default', wrong_secret), + ('corect_secret', correct_secret), + ('wrong_secret', wrong_secret)] + + v_multi = vault.VaultLib(vault_secrets) + b_plaintext = v_multi.decrypt(b_vaulttext) + self.assertNotEqual(b_vaulttext, to_bytes(plaintext), msg="encryption failed") + self.assertEqual(b_plaintext, to_bytes(plaintext), msg="decryption failed") + def test_encrypt_decrypt_aes256_existing_vault(self): self.v.cipher_name = u'AES256' b_orig_plaintext = b"Setec Astronomy" @@ -380,6 +653,27 @@ fe3db930508b65e0ff5947e4386b79af8ab094017629590ef6ba486814cf70f8e4ab0ed0c7d2587e # assert we throw an error self.v.decrypt(b_invalid_ciphertext) + def test_decrypt_non_default_1_2(self): + b_expected_plaintext = to_bytes('foo bar\n') + vaulttext = '''$ANSIBLE_VAULT;1.2;AES256;ansible_devel +65616435333934613466373335363332373764363365633035303466643439313864663837393234 +3330656363343637313962633731333237313636633534630a386264363438363362326132363239 +39363166646664346264383934393935653933316263333838386362633534326664646166663736 +6462303664383765650a356637643633366663643566353036303162386237336233393065393164 +6264''' + + vault_secrets = self._vault_secrets_from_password('default', 'ansible') + v = vault.VaultLib(vault_secrets) + + b_vaulttext = to_bytes(vaulttext) + + b_plaintext = v.decrypt(b_vaulttext) + self.assertEqual(b_expected_plaintext, b_plaintext) + + b_ciphertext, b_version, cipher_name, vault_id = vault.parse_vaulttext_envelope(b_vaulttext) + self.assertEqual('ansible_devel', vault_id) + self.assertEqual(b'1.2', b_version) + def test_encrypt_encrypted(self): self.v.cipher_name = u'AES' b_vaulttext = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible") diff --git a/test/units/parsing/vault/test_vault_editor.py b/test/units/parsing/vault/test_vault_editor.py index e216b67a540..30c68f440dc 100644 --- a/test/units/parsing/vault/test_vault_editor.py +++ b/test/units/parsing/vault/test_vault_editor.py @@ -30,8 +30,11 @@ from ansible.compat.tests.mock import patch from ansible import errors from ansible.parsing import vault +from ansible.parsing.vault import VaultLib, VaultEditor, match_encrypt_secret + from ansible.module_utils._text import to_bytes, to_text +from units.mock.vault_helper import TextVaultSecret v10_data = """$ANSIBLE_VAULT;1.0;AES 53616c7465645f5fd0026926a2d415a28a2622116273fbc90e377225c12a347e1daf4456d36a77f9 @@ -52,6 +55,14 @@ class TestVaultEditor(unittest.TestCase): def setUp(self): self._test_dir = None + self.vault_password = "test-vault-password" + vault_secret = TextVaultSecret(self.vault_password) + self.vault_secrets = [('vault_secret', vault_secret), + ('default', vault_secret)] + + @property + def vault_secret(self): + return match_encrypt_secret(self.vault_secrets)[1] def tearDown(self): if self._test_dir: @@ -59,6 +70,11 @@ class TestVaultEditor(unittest.TestCase): # shutil.rmtree(self._test_dir) self._test_dir = None + def _secrets(self, password): + vault_secret = TextVaultSecret(password) + vault_secrets = [('default', vault_secret)] + return vault_secrets + def test_methods_exist(self): v = vault.VaultEditor(None) slots = ['create_file', @@ -83,6 +99,11 @@ class TestVaultEditor(unittest.TestCase): opened_file.close() return file_path + def _vault_editor(self, vault_secrets=None): + if vault_secrets is None: + vault_secrets = self._secrets(self.vault_password) + return VaultEditor(VaultLib(vault_secrets)) + @patch('ansible.parsing.vault.call') def test_edit_file_helper_empty_target(self, mock_sp_call): self._test_dir = self._create_test_dir() @@ -91,9 +112,9 @@ class TestVaultEditor(unittest.TestCase): src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents) mock_sp_call.side_effect = self._faux_command - ve = vault.VaultEditor('password') + ve = self._vault_editor() - b_ciphertext = ve._edit_file_helper(src_file_path) + b_ciphertext = ve._edit_file_helper(src_file_path, self.vault_secret) self.assertNotEqual(src_contents, b_ciphertext) @@ -107,12 +128,13 @@ class TestVaultEditor(unittest.TestCase): error_txt = 'calling editor raised an exception' mock_sp_call.side_effect = errors.AnsibleError(error_txt) - ve = vault.VaultEditor('password') + ve = self._vault_editor() self.assertRaisesRegexp(errors.AnsibleError, error_txt, ve._edit_file_helper, - src_file_path) + src_file_path, + self.vault_secret) @patch('ansible.parsing.vault.call') def test_edit_file_helper_symlink_target(self, mock_sp_call): @@ -126,9 +148,9 @@ class TestVaultEditor(unittest.TestCase): os.symlink(src_file_path, src_file_link_path) mock_sp_call.side_effect = self._faux_command - ve = vault.VaultEditor('password') + ve = self._vault_editor() - b_ciphertext = ve._edit_file_helper(src_file_link_path) + b_ciphertext = ve._edit_file_helper(src_file_link_path, self.vault_secret) self.assertNotEqual(src_file_contents, b_ciphertext, 'b_ciphertext should be encrypted and not equal to src_contents') @@ -160,9 +182,9 @@ class TestVaultEditor(unittest.TestCase): self._faux_editor(editor_args, src_file_contents) mock_sp_call.side_effect = faux_editor - ve = vault.VaultEditor('password') + ve = self._vault_editor() - ve._edit_file_helper(src_file_path, existing_data=src_file_contents) + ve._edit_file_helper(src_file_path, self.vault_secret, existing_data=src_file_contents) new_target_file = open(src_file_path, 'rb') new_target_file_contents = new_target_file.read() @@ -193,13 +215,17 @@ class TestVaultEditor(unittest.TestCase): src_file_contents = to_bytes("some info in a file\nyup.") src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents) - ve = vault.VaultEditor('password') - ve.encrypt_file(src_file_path) + ve = self._vault_editor() + ve.encrypt_file(src_file_path, self.vault_secret) + # FIXME: update to just set self._secrets or just a new vault secret id new_password = 'password2:electricbugaloo' - ve.rekey_file(src_file_path, new_password) + new_vault_secret = TextVaultSecret(new_password) + new_vault_secrets = [('default', new_vault_secret)] + ve.rekey_file(src_file_path, vault.match_encrypt_secret(new_vault_secrets)[1]) - new_ve = vault.VaultEditor(new_password) + # FIXME: can just update self._secrets here + new_ve = vault.VaultEditor(VaultLib(new_vault_secrets)) self._assert_file_is_encrypted(new_ve, src_file_path, src_file_contents) def test_rekey_file_no_new_password(self): @@ -208,8 +234,8 @@ class TestVaultEditor(unittest.TestCase): src_file_contents = to_bytes("some info in a file\nyup.") src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents) - ve = vault.VaultEditor('password') - ve.encrypt_file(src_file_path) + ve = self._vault_editor() + ve.encrypt_file(src_file_path, self.vault_secret) self.assertRaisesRegexp(errors.AnsibleError, 'The value for the new_password to rekey', @@ -223,7 +249,7 @@ class TestVaultEditor(unittest.TestCase): src_file_contents = to_bytes("some info in a file\nyup.") src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents) - ve = vault.VaultEditor('password') + ve = self._vault_editor() new_password = 'password2:electricbugaloo' self.assertRaisesRegexp(errors.AnsibleError, @@ -237,11 +263,11 @@ class TestVaultEditor(unittest.TestCase): src_file_contents = to_bytes("some info in a file\nyup.") src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents) - ve = vault.VaultEditor('password') - ve.encrypt_file(src_file_path) + ve = self._vault_editor() + ve.encrypt_file(src_file_path, self.vault_secret) res = ve.plaintext(src_file_path) - self.assertEquals(src_file_contents, res) + self.assertEqual(src_file_contents, res) def test_plaintext_not_encrypted(self): self._test_dir = self._create_test_dir() @@ -249,7 +275,7 @@ class TestVaultEditor(unittest.TestCase): src_file_contents = to_bytes("some info in a file\nyup.") src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents) - ve = vault.VaultEditor('password') + ve = self._vault_editor() self.assertRaisesRegexp(errors.AnsibleError, 'input is not vault encrypted data', ve.plaintext, @@ -260,8 +286,8 @@ class TestVaultEditor(unittest.TestCase): src_file_contents = to_bytes("some info in a file\nyup.") src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents) - ve = vault.VaultEditor('password') - ve.encrypt_file(src_file_path) + ve = self._vault_editor() + ve.encrypt_file(src_file_path, self.vault_secret) self._assert_file_is_encrypted(ve, src_file_path, src_file_contents) @@ -274,8 +300,8 @@ class TestVaultEditor(unittest.TestCase): src_file_link_path = os.path.join(self._test_dir, 'a_link_to_dest_file') os.symlink(src_file_path, src_file_link_path) - ve = vault.VaultEditor('password') - ve.encrypt_file(src_file_link_path) + ve = self._vault_editor() + ve.encrypt_file(src_file_link_path, self.vault_secret) self._assert_file_is_encrypted(ve, src_file_path, src_file_contents) self._assert_file_is_encrypted(ve, src_file_link_path, src_file_contents) @@ -296,9 +322,9 @@ class TestVaultEditor(unittest.TestCase): mock_sp_call.side_effect = faux_editor - ve = vault.VaultEditor('password') + ve = self._vault_editor() - ve.encrypt_file(src_file_path) + ve.encrypt_file(src_file_path, self.vault_secret) ve.edit_file(src_file_path) new_src_file = open(src_file_path, 'rb') @@ -308,7 +334,6 @@ class TestVaultEditor(unittest.TestCase): self.assertEqual(src_file_plaintext, new_src_contents) new_stat = os.stat(src_file_path) - print(new_stat) @patch('ansible.parsing.vault.call') def test_edit_file_symlink(self, mock_sp_call): @@ -324,9 +349,9 @@ class TestVaultEditor(unittest.TestCase): mock_sp_call.side_effect = faux_editor - ve = vault.VaultEditor('password') + ve = self._vault_editor() - ve.encrypt_file(src_file_path) + ve.encrypt_file(src_file_path, self.vault_secret) src_file_link_path = os.path.join(self._test_dir, 'a_link_to_dest_file') @@ -360,7 +385,7 @@ class TestVaultEditor(unittest.TestCase): mock_sp_call.side_effect = faux_editor - ve = vault.VaultEditor('password') + ve = self._vault_editor() self.assertRaisesRegexp(errors.AnsibleError, 'input is not vault encrypted data', ve.edit_file, @@ -371,18 +396,19 @@ class TestVaultEditor(unittest.TestCase): src_contents = to_bytes("some info in a file\nyup.") src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents) - ve = vault.VaultEditor('password') + ve = self._vault_editor() self.assertRaisesRegexp(errors.AnsibleError, 'please use .edit. instead', ve.create_file, - src_file_path) + src_file_path, + self.vault_secret) def test_decrypt_file_exception(self): self._test_dir = self._create_test_dir() src_contents = to_bytes("some info in a file\nyup.") src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents) - ve = vault.VaultEditor('password') + ve = self._vault_editor() self.assertRaisesRegexp(errors.AnsibleError, 'input is not vault encrypted data', ve.decrypt_file, @@ -398,8 +424,9 @@ class TestVaultEditor(unittest.TestCase): tmp_file = tempfile.NamedTemporaryFile() os.unlink(tmp_file.name) - ve = vault.VaultEditor("ansible") - ve.create_file(tmp_file.name) + _secrets = self._secrets('ansible') + ve = self._vault_editor(_secrets) + ve.create_file(tmp_file.name, vault.match_encrypt_secret(_secrets)[1]) self.assertTrue(os.path.exists(tmp_file.name)) @@ -409,7 +436,7 @@ class TestVaultEditor(unittest.TestCase): with v10_file as f: f.write(to_bytes(v10_data)) - ve = vault.VaultEditor("ansible") + ve = self._vault_editor(self._secrets("ansible")) # make sure the password functions for the cipher error_hit = False @@ -417,6 +444,7 @@ class TestVaultEditor(unittest.TestCase): ve.decrypt_file(v10_file.name) except errors.AnsibleError: error_hit = True + raise # verify decrypted content f = open(v10_file.name, "rb") @@ -426,7 +454,7 @@ class TestVaultEditor(unittest.TestCase): os.unlink(v10_file.name) assert error_hit is False, "error decrypting 1.0 file" - self.assertEquals(fdata.strip(), "foo") + self.assertEqual(fdata.strip(), "foo") assert fdata.strip() == "foo", "incorrect decryption of 1.0 file: %s" % fdata.strip() def test_decrypt_1_1(self): @@ -434,13 +462,14 @@ class TestVaultEditor(unittest.TestCase): with v11_file as f: f.write(to_bytes(v11_data)) - ve = vault.VaultEditor("ansible") + ve = self._vault_editor(self._secrets("ansible")) # make sure the password functions for the cipher error_hit = False try: ve.decrypt_file(v11_file.name) except errors.AnsibleError: + raise error_hit = True # verify decrypted content @@ -450,21 +479,23 @@ class TestVaultEditor(unittest.TestCase): os.unlink(v11_file.name) - assert error_hit is False, "error decrypting 1.0 file" - assert fdata.strip() == "foo", "incorrect decryption of 1.0 file: %s" % fdata.strip() + assert error_hit is False, "error decrypting 1.1 file" + assert fdata.strip() == "foo", "incorrect decryption of 1.1 file: %s" % fdata.strip() def test_rekey_migration(self): v10_file = tempfile.NamedTemporaryFile(delete=False) with v10_file as f: f.write(to_bytes(v10_data)) - ve = vault.VaultEditor("ansible") + ve = self._vault_editor(self._secrets("ansible")) # make sure the password functions for the cipher error_hit = False + new_secrets = self._secrets("ansible2") try: - ve.rekey_file(v10_file.name, 'ansible2') + ve.rekey_file(v10_file.name, vault.match_encrypt_secret(new_secrets)[1]) except errors.AnsibleError: + raise error_hit = True # verify decrypted content @@ -475,30 +506,31 @@ class TestVaultEditor(unittest.TestCase): assert error_hit is False, "error rekeying 1.0 file to 1.1" # ensure filedata can be decrypted, is 1.1 and is AES256 - vl = vault.VaultLib("ansible2") + vl = VaultLib(new_secrets) dec_data = None error_hit = False try: dec_data = vl.decrypt(fdata) except errors.AnsibleError: + raise error_hit = True os.unlink(v10_file.name) - assert vl.cipher_name == "AES256", "wrong cipher name set after rekey: %s" % vl.cipher_name + self.assertIn(b'AES256', fdata, 'AES256 was not found in vault file %s' % to_text(fdata)) assert error_hit is False, "error decrypting migrated 1.0 file" assert dec_data.strip() == b"foo", "incorrect decryption of rekeyed/migrated file: %s" % dec_data def test_real_path_dash(self): filename = '-' - ve = vault.VaultEditor('password') + ve = self._vault_editor() res = ve._real_path(filename) self.assertEqual(res, '-') def test_real_path_dev_null(self): filename = '/dev/null' - ve = vault.VaultEditor('password') + ve = self._vault_editor() res = ve._real_path(filename) self.assertEqual(res, '/dev/null') @@ -510,7 +542,7 @@ class TestVaultEditor(unittest.TestCase): os.symlink(file_path, file_link_path) - ve = vault.VaultEditor('password') + ve = self._vault_editor() res = ve._real_path(file_link_path) self.assertEqual(res, file_path) diff --git a/test/units/parsing/yaml/test_dumper.py b/test/units/parsing/yaml/test_dumper.py index 62948fc16a0..ba8fa523fba 100644 --- a/test/units/parsing/yaml/test_dumper.py +++ b/test/units/parsing/yaml/test_dumper.py @@ -19,12 +19,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import io -import yaml - -try: - from _yaml import ParserError -except ImportError: - from yaml.parser import ParserError from ansible.compat.tests import unittest from ansible.parsing import vault @@ -32,12 +26,15 @@ from ansible.parsing.yaml import dumper, objects from ansible.parsing.yaml.loader import AnsibleLoader from units.mock.yaml_helper import YamlTestUtils +from units.mock.vault_helper import TextVaultSecret class TestAnsibleDumper(unittest.TestCase, YamlTestUtils): def setUp(self): self.vault_password = "hunter42" - self.good_vault = vault.VaultLib(self.vault_password) + vault_secret = TextVaultSecret(self.vault_password) + self.vault_secrets = [('vault_secret', vault_secret)] + self.good_vault = vault.VaultLib(self.vault_secrets) self.vault = self.good_vault self.stream = self._build_stream() self.dumper = dumper.AnsibleDumper @@ -48,11 +45,12 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils): return stream def _loader(self, stream): - return AnsibleLoader(stream, vault_password=self.vault_password) + return AnsibleLoader(stream, vault_secrets=self.vault.secrets) def test(self): plaintext = 'This is a string we are going to encrypt.' - avu = objects.AnsibleVaultEncryptedUnicode.from_plaintext(plaintext, vault=self.vault) + avu = objects.AnsibleVaultEncryptedUnicode.from_plaintext(plaintext, vault=self.vault, + secret=vault.match_secrets(self.vault_secrets, ['vault_secret'])[0][1]) yaml_out = self._dump_string(avu, dumper=self.dumper) stream = self._build_stream(yaml_out) @@ -60,4 +58,4 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils): data_from_yaml = loader.get_single_data() - self.assertEquals(plaintext, data_from_yaml.data) + self.assertEqual(plaintext, data_from_yaml.data) diff --git a/test/units/parsing/yaml/test_loader.py b/test/units/parsing/yaml/test_loader.py index e78bce1ed57..6865d690921 100644 --- a/test/units/parsing/yaml/test_loader.py +++ b/test/units/parsing/yaml/test_loader.py @@ -34,6 +34,7 @@ from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode from ansible.parsing.yaml.dumper import AnsibleDumper from units.mock.yaml_helper import YamlTestUtils +from units.mock.vault_helper import TextVaultSecret try: from _yaml import ParserError @@ -176,25 +177,35 @@ class TestAnsibleLoaderBasic(unittest.TestCase): class TestAnsibleLoaderVault(unittest.TestCase, YamlTestUtils): def setUp(self): self.vault_password = "hunter42" - self.vault = vault.VaultLib(self.vault_password) + vault_secret = TextVaultSecret(self.vault_password) + self.vault_secrets = [('vault_secret', vault_secret), + ('default', vault_secret)] + self.vault = vault.VaultLib(self.vault_secrets) + + @property + def vault_secret(self): + return vault.match_encrypt_secret(self.vault_secrets)[1] def test_wrong_password(self): plaintext = u"Ansible" bob_password = "this is a different password" - bobs_vault = vault.VaultLib(bob_password) + bobs_secret = TextVaultSecret(bob_password) + bobs_secrets = [('default', bobs_secret)] + + bobs_vault = vault.VaultLib(bobs_secrets) - ciphertext = bobs_vault.encrypt(plaintext) + ciphertext = bobs_vault.encrypt(plaintext, vault.match_encrypt_secret(bobs_secrets)[1]) try: self.vault.decrypt(ciphertext) except Exception as e: self.assertIsInstance(e, errors.AnsibleError) - self.assertEqual(e.message, 'Decryption failed') + self.assertEqual(e.message, 'Decryption failed (no vault secrets would found that could decrypt)') def _encrypt_plaintext(self, plaintext): # Construct a yaml repr of a vault by hand - vaulted_var_bytes = self.vault.encrypt(plaintext) + vaulted_var_bytes = self.vault.encrypt(plaintext, self.vault_secret) # add yaml tag vaulted_var = vaulted_var_bytes.decode() @@ -213,7 +224,7 @@ class TestAnsibleLoaderVault(unittest.TestCase, YamlTestUtils): return stream def _loader(self, stream): - return AnsibleLoader(stream, vault_password=self.vault_password) + return AnsibleLoader(stream, vault_secrets=self.vault.secrets) def _load_yaml(self, yaml_text, password): stream = self._build_stream(yaml_text) @@ -224,11 +235,11 @@ class TestAnsibleLoaderVault(unittest.TestCase, YamlTestUtils): return data_from_yaml def test_dump_load_cycle(self): - avu = AnsibleVaultEncryptedUnicode.from_plaintext('The plaintext for test_dump_load_cycle.', vault=self.vault) + avu = AnsibleVaultEncryptedUnicode.from_plaintext('The plaintext for test_dump_load_cycle.', self.vault, self.vault_secret) self._dump_load_cycle(avu) def test_embedded_vault_from_dump(self): - avu = AnsibleVaultEncryptedUnicode.from_plaintext('setec astronomy', vault=self.vault) + avu = AnsibleVaultEncryptedUnicode.from_plaintext('setec astronomy', self.vault, self.vault_secret) blip = {'stuff1': [{'a dict key': 24}, {'shhh-ssh-secrets': avu, 'nothing to see here': 'move along'}], @@ -239,7 +250,6 @@ class TestAnsibleLoaderVault(unittest.TestCase, YamlTestUtils): self._dump_stream(blip, stream, dumper=AnsibleDumper) - print(stream.getvalue()) stream.seek(0) stream.seek(0) @@ -247,6 +257,7 @@ class TestAnsibleLoaderVault(unittest.TestCase, YamlTestUtils): loader = self._loader(stream) data_from_yaml = loader.get_data() + stream2 = NameStringIO(u'') # verify we can dump the object again self._dump_stream(data_from_yaml, stream2, dumper=AnsibleDumper) @@ -266,20 +277,20 @@ class TestAnsibleLoaderVault(unittest.TestCase, YamlTestUtils): data_from_yaml = self._load_yaml(yaml_text, self.vault_password) vault_string = data_from_yaml['the_secret'] - self.assertEquals(plaintext_var, data_from_yaml['the_secret']) + self.assertEqual(plaintext_var, data_from_yaml['the_secret']) test_dict = {} test_dict[vault_string] = 'did this work?' - self.assertEquals(vault_string.data, vault_string) + self.assertEqual(vault_string.data, vault_string) # This looks weird and useless, but the object in question has a custom __eq__ - self.assertEquals(vault_string, vault_string) + self.assertEqual(vault_string, vault_string) another_vault_string = data_from_yaml['another_secret'] different_vault_string = data_from_yaml['different_secret'] - self.assertEquals(vault_string, another_vault_string) + self.assertEqual(vault_string, another_vault_string) self.assertNotEquals(vault_string, different_vault_string) # More testing of __eq__/__ne__ @@ -288,8 +299,8 @@ class TestAnsibleLoaderVault(unittest.TestCase, YamlTestUtils): # Note this is a compare of the str/unicode of these, they are different types # so we want to test self == other, and other == self etc - self.assertEquals(plaintext_var, vault_string) - self.assertEquals(vault_string, plaintext_var) + self.assertEqual(plaintext_var, vault_string) + self.assertEqual(vault_string, plaintext_var) self.assertFalse(plaintext_var != vault_string) self.assertFalse(vault_string != plaintext_var) diff --git a/test/units/parsing/yaml/test_objects.py b/test/units/parsing/yaml/test_objects.py index 99d27ad8b72..71fa7670b88 100644 --- a/test/units/parsing/yaml/test_objects.py +++ b/test/units/parsing/yaml/test_objects.py @@ -21,6 +21,7 @@ __metaclass__ = type from ansible.compat.tests import unittest +from ansible.errors import AnsibleError from ansible.parsing import vault from ansible.parsing.yaml.loader import AnsibleLoader @@ -29,6 +30,7 @@ from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml import objects from units.mock.yaml_helper import YamlTestUtils +from units.mock.vault_helper import TextVaultSecret class TestAnsibleVaultUnicodeNoVault(unittest.TestCase, YamlTestUtils): @@ -68,16 +70,22 @@ class TestAnsibleVaultUnicodeNoVault(unittest.TestCase, YamlTestUtils): class TestAnsibleVaultEncryptedUnicode(unittest.TestCase, YamlTestUtils): def setUp(self): - self.vault_password = "hunter42" - self.good_vault = vault.VaultLib(self.vault_password) + self.good_vault_password = "hunter42" + good_vault_secret = TextVaultSecret(self.good_vault_password) + self.good_vault_secrets = [('good_vault_password', good_vault_secret)] + self.good_vault = vault.VaultLib(self.good_vault_secrets) + # TODO: make this use two vault secret identities instead of two vaultSecrets self.wrong_vault_password = 'not-hunter42' - self.wrong_vault = vault.VaultLib(self.wrong_vault_password) + wrong_vault_secret = TextVaultSecret(self.wrong_vault_password) + self.wrong_vault_secrets = [('wrong_vault_password', wrong_vault_secret)] + self.wrong_vault = vault.VaultLib(self.wrong_vault_secrets) self.vault = self.good_vault + self.vault_secrets = self.good_vault_secrets def _loader(self, stream): - return AnsibleLoader(stream, vault_password=self.vault_password) + return AnsibleLoader(stream, vault_secrets=self.vault_secrets) def test_dump_load_cycle(self): aveu = self._from_plaintext('the test string for TestAnsibleVaultEncryptedUnicode.test_dump_load_cycle') @@ -86,12 +94,13 @@ class TestAnsibleVaultEncryptedUnicode(unittest.TestCase, YamlTestUtils): def assert_values(self, avu, seq): self.assertIsInstance(avu, objects.AnsibleVaultEncryptedUnicode) - self.assertEquals(avu, seq) + self.assertEqual(avu, seq) self.assertTrue(avu.vault is self.vault) self.assertIsInstance(avu.vault, vault.VaultLib) def _from_plaintext(self, seq): - return objects.AnsibleVaultEncryptedUnicode.from_plaintext(seq, vault=self.vault) + id_secret = vault.match_encrypt_secret(self.good_vault_secrets) + return objects.AnsibleVaultEncryptedUnicode.from_plaintext(seq, vault=self.vault, secret=id_secret[1]) def _from_ciphertext(self, ciphertext): avu = objects.AnsibleVaultEncryptedUnicode(ciphertext) @@ -126,7 +135,7 @@ class TestAnsibleVaultEncryptedUnicode(unittest.TestCase, YamlTestUtils): avu = self._from_plaintext(seq) b_avu = avu.encode('utf-8', 'strict') self.assertIsInstance(avu, objects.AnsibleVaultEncryptedUnicode) - self.assertEquals(b_avu, seq.encode('utf-8', 'strict')) + self.assertEqual(b_avu, seq.encode('utf-8', 'strict')) self.assertTrue(avu.vault is self.vault) self.assertIsInstance(avu.vault, vault.VaultLib) @@ -135,4 +144,8 @@ class TestAnsibleVaultEncryptedUnicode(unittest.TestCase, YamlTestUtils): seq = '' self.vault = self.wrong_vault avu = self._from_plaintext(seq) - self.assert_values(avu, seq) + + def compare(avu, seq): + return avu == seq + + self.assertRaises(AnsibleError, compare, avu, seq)