From 934b64519191b0df70495deecbb349532af48597 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Fri, 28 Jul 2017 15:20:58 -0400 Subject: [PATCH] Support multiple vault passwords (#22756) Fixes #13243 ** Add --vault-id to name/identify multiple vault passwords Use --vault-id to indicate id and path/type --vault-id=prompt # prompt for default vault id password --vault-id=myorg@prompt # prompt for a vault_id named 'myorg' --vault-id=a_password_file # load ./a_password_file for default id --vault-id=myorg@a_password_file # load file for 'myorg' vault id vault_id's are created implicitly for existing --vault-password-file and --ask-vault-pass options. Vault ids are just for UX purposes and bookkeeping. Only the vault payload and the password bytestring is needed to decrypt a vault blob. Replace passing password around everywhere with a VaultSecrets object. If we specify a vault_id, mention that in password prompts Specifying multiple -vault-password-files will now try each until one works ** Rev vault format in a backwards compatible way The 1.2 vault format adds the vault_id to the header line of the vault text. This is backwards compatible with older versions of ansible. Old versions will just ignore it and treat it as the default (and only) vault id. Note: only 2.4+ supports multiple vault passwords, so while earlier ansible versions can read the vault-1.2 format, it does not make them magically support multiple vault passwords. use 1.1 format for 'default' vault_id Vaulted items that need to include a vault_id will be written in 1.2 format. If we set a new DEFAULT_VAULT_IDENTITY, then the default will use version 1.2 vault will only use a vault_id if one is specified. So if none is specified and C.DEFAULT_VAULT_IDENTITY is 'default' we use the old format. ** Changes/refactors needed to implement multiple vault passwords raise exceptions on decrypt fail, check vault id early split out parsing the vault plaintext envelope (with the sha/original plaintext) to _split_plaintext_envelope() some cli fixups for specifying multiple paths in the unfrack_paths optparse callback fix py3 dict.keys() 'dict_keys object is not indexable' error pluralize cli.options.vault_password_file -> vault_password_files pluralize cli.options.new_vault_password_file -> new_vault_password_files pluralize cli.options.vault_id -> cli.options.vault_ids ** Add a config option (vault_id_match) to force vault id matching. With 'vault_id_match=True' and an ansible vault that provides a vault_id, then decryption will require that a matching vault_id is required. (via --vault-id=my_vault_id@password_file, for ex). In other words, if the config option is true, then only the vault secrets with matching vault ids are candidates for decrypting a vault. If option is false (the default), then all of the provided vault secrets will be selected. If a user doesn't want all vault secrets to be tried to decrypt any vault content, they can enable this option. Note: The vault id used for the match is not encrypted or cryptographically signed. It is just a label/id/nickname used for referencing a specific vault secret. --- lib/ansible/cli/__init__.py | 170 +++--- lib/ansible/cli/console.py | 6 + lib/ansible/cli/pull.py | 5 +- lib/ansible/cli/vault.py | 143 +++-- lib/ansible/config/data/config.yml | 16 + lib/ansible/parsing/dataloader.py | 25 +- lib/ansible/parsing/vault/__init__.py | 537 ++++++++++++++---- lib/ansible/parsing/yaml/constructor.py | 24 +- lib/ansible/parsing/yaml/loader.py | 8 +- lib/ansible/parsing/yaml/objects.py | 4 +- .../encrypted_file_encrypted_var_password | 1 + .../targets/vault/example1_password | 1 + .../targets/vault/example2_password | 1 + .../targets/vault/example3_password | 1 + .../targets/vault/format_1_2_AES256.yml | 6 + .../targets/vault/password-script.py | 33 ++ .../test_vault_embedded_ids/tasks/main.yml | 29 + .../test_vault_embedded_ids/vars/main.yml | 194 +++++++ .../README.md | 1 + .../tasks/main.yml | 13 + .../vars/main.yml | 76 +++ test/integration/targets/vault/runme.sh | 183 ++++++ .../targets/vault/test_vault_embedded_ids.yml | 4 + .../test_vault_file_encrypted_embedded.yml | 4 + test/units/cli/test_cli.py | 44 ++ test/units/cli/test_vault.py | 166 ++++++ test/units/mock/loader.py | 4 + test/units/mock/vault_helper.py | 39 ++ test/units/parsing/test_dataloader.py | 5 +- test/units/parsing/vault/test_vault.py | 318 ++++++++++- test/units/parsing/vault/test_vault_editor.py | 124 ++-- test/units/parsing/yaml/test_dumper.py | 18 +- test/units/parsing/yaml/test_loader.py | 41 +- test/units/parsing/yaml/test_objects.py | 29 +- 34 files changed, 1925 insertions(+), 348 deletions(-) create mode 100644 test/integration/targets/vault/encrypted_file_encrypted_var_password create mode 100644 test/integration/targets/vault/example1_password create mode 100644 test/integration/targets/vault/example2_password create mode 100644 test/integration/targets/vault/example3_password create mode 100644 test/integration/targets/vault/format_1_2_AES256.yml create mode 100755 test/integration/targets/vault/password-script.py create mode 100644 test/integration/targets/vault/roles/test_vault_embedded_ids/tasks/main.yml create mode 100644 test/integration/targets/vault/roles/test_vault_embedded_ids/vars/main.yml create mode 100644 test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/README.md create mode 100644 test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml create mode 100644 test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/vars/main.yml create mode 100644 test/integration/targets/vault/test_vault_embedded_ids.yml create mode 100644 test/integration/targets/vault/test_vault_file_encrypted_embedded.yml create mode 100644 test/units/cli/test_vault.py create mode 100644 test/units/mock/vault_helper.py 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)