diff --git a/lib/ansible/cli/__init__.py b/lib/ansible/cli/__init__.py index 4ee4396d9d6..1ee6186ac7e 100644 --- a/lib/ansible/cli/__init__.py +++ b/lib/ansible/cli/__init__.py @@ -367,7 +367,7 @@ class CLI(with_metaclass(ABCMeta, object)): if self.options.ask_su_pass or self.options.su_user: _dep('su') - def validate_conflicts(self, vault_opts=False, runas_opts=False, fork_opts=False): + def validate_conflicts(self, vault_opts=False, runas_opts=False, fork_opts=False, vault_rekey_opts=False): ''' check for conflicting options ''' op = self.options @@ -377,6 +377,10 @@ class CLI(with_metaclass(ABCMeta, object)): if (op.ask_vault_pass and op.vault_password_files): self.parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive") + if vault_rekey_opts: + if (op.new_vault_id and op.new_vault_password_file): + self.parser.error("--new-vault-password-file and --new-vault-id are mutually exclusive") + if runas_opts: # Check for privilege escalation conflicts if ((op.su or op.su_user) and (op.sudo or op.sudo_user) or @@ -452,8 +456,8 @@ class CLI(with_metaclass(ABCMeta, object)): help='the vault identity to use') if vault_rekey_opts: - 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('--new-vault-password-file', default=None, dest='new_vault_password_file', + help="new vault password file for rekey", action="callback", callback=CLI.unfrack_path, type='string') parser.add_option('--new-vault-id', default=None, dest='new_vault_id', type='string', help='the new vault identity to use for rekey') diff --git a/lib/ansible/cli/vault.py b/lib/ansible/cli/vault.py index e6f36fdbd79..4b7188384bc 100644 --- a/lib/ansible/cli/vault.py +++ b/lib/ansible/cli/vault.py @@ -106,6 +106,12 @@ class VaultCLI(CLI): elif self.action == "rekey": self.parser.set_usage("usage: %prog rekey [options] file_name") + # For encrypting actions, we can also specify which of multiple vault ids should be used for encrypting + if self.action in ['create', 'encrypt', 'encrypt_string', 'rekey']: + self.parser.add_option('--encrypt-vault-id', default=[], dest='encrypt_vault_id', + action='store', type='string', + help='the vault id used to encrypt (required if more than vault-id is provided)') + def parse(self): self.parser = CLI.base_parser( @@ -119,6 +125,7 @@ class VaultCLI(CLI): self.set_action() super(VaultCLI, self).parse() + self.validate_conflicts(vault_opts=True, vault_rekey_opts=True) display.verbosity = self.options.verbosity @@ -174,9 +181,12 @@ class VaultCLI(CLI): if not vault_secrets: raise AnsibleOptionsError("A vault password is required to use Ansible's Vault") - if self.action in ['encrypt', 'encrypt_string', 'create']: - if len(vault_ids) > 1: - raise AnsibleOptionsError("Only one --vault-id can be used for encryption") + if self.action in ['encrypt', 'encrypt_string', 'create', 'edit']: + + encrypt_vault_id = None + # no --encrypt-vault-id self.options.encrypt_vault_id for 'edit' + if self.action not in ['edit']: + encrypt_vault_id = self.options.encrypt_vault_id or C.DEFAULT_VAULT_ENCRYPT_IDENTITY vault_secrets = None vault_secrets = \ @@ -186,36 +196,52 @@ class VaultCLI(CLI): ask_vault_pass=self.options.ask_vault_pass, create_new_password=True) - if len(vault_secrets) > 1: - raise AnsibleOptionsError("Only one --vault-id can be used for encryption. This includes passwords from configuration and cli.") + if len(vault_secrets) > 1 and not encrypt_vault_id: + raise AnsibleOptionsError("The vault-ids %s are available to encrypt. Specify the vault-id to encrypt with --encrypt-vault-id" % + ','.join([x[0] for x in vault_secrets])) if not vault_secrets: raise AnsibleOptionsError("A vault password is required to use Ansible's Vault") - encrypt_secret = match_encrypt_secret(vault_secrets) + encrypt_secret = match_encrypt_secret(vault_secrets, + encrypt_vault_id=encrypt_vault_id) + # 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] + # TODO: exception if more than one? self.encrypt_vault_id = encrypt_secret[0] self.encrypt_secret = encrypt_secret[1] if self.action in ['rekey']: + encrypt_vault_id = self.options.encrypt_vault_id or C.DEFAULT_VAULT_ENCRYPT_IDENTITY + # print('encrypt_vault_id: %s' % encrypt_vault_id) + # print('default_encrypt_vault_id: %s' % default_encrypt_vault_id) + + # new_vault_ids should only ever be one item, from + # load the default vault ids if we are using encrypt-vault-id new_vault_ids = [] + if encrypt_vault_id: + new_vault_ids = default_vault_ids if self.options.new_vault_id: new_vault_ids.append(self.options.new_vault_id) + new_vault_password_files = [] + if self.options.new_vault_password_file: + new_vault_password_files.append(self.options.new_vault_password_file) + new_vault_secrets = \ self.setup_vault_secrets(loader, vault_ids=new_vault_ids, - vault_password_files=self.options.new_vault_password_files, + vault_password_files=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) + # There is only one new_vault_id currently and one new_vault_secret, or we + # use the id specified in --encrypt-vault-id + new_encrypt_secret = match_encrypt_secret(new_vault_secrets, + encrypt_vault_id=encrypt_vault_id) self.new_encrypt_vault_id = new_encrypt_secret[0] self.new_encrypt_secret = new_encrypt_secret[1] diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml index 52debdf1176..7f61046a96e 100644 --- a/lib/ansible/config/base.yml +++ b/lib/ansible/config/base.yml @@ -1102,6 +1102,14 @@ DEFAULT_VAULT_IDENTITY: ini: - {key: vault_identity, section: defaults} yaml: {key: defaults.vault_identity} +DEFAULT_VAULT_ENCRYPT_IDENTITY: + name: Vault id to use for encryption + default: + description: 'The vault_id to use for encrypting by default. If multiple vault_ids are provided, this specifies which to use for encryption. The --encrypt-vault-id cli option overrides the configured value.' + env: [{name: ANSIBLE_VAULT_ENCRYPT_IDENTITY}] + ini: + - {key: vault_encrypt_identity, section: defaults} + yaml: {key: defaults.vault_encrypt_identity} DEFAULT_VAULT_IDENTITY_LIST: name: Default vault ids default: [] diff --git a/lib/ansible/parsing/vault/__init__.py b/lib/ansible/parsing/vault/__init__.py index 0eb14c594c0..c9a54ae3e76 100644 --- a/lib/ansible/parsing/vault/__init__.py +++ b/lib/ansible/parsing/vault/__init__.py @@ -556,12 +556,40 @@ def match_best_secret(secrets, target_vault_ids): return None -def match_encrypt_secret(secrets): +def match_encrypt_vault_id_secret(secrets, encrypt_vault_id=None): + # See if the --encrypt-vault-id matches a vault-id + display.vvvv('encrypt_vault_id=%s' % encrypt_vault_id) + + if encrypt_vault_id is None: + raise AnsibleError('match_encrypt_vault_id_secret requires a non None encrypt_vault_id') + + encrypt_vault_id_matchers = [encrypt_vault_id] + encrypt_secret = match_best_secret(secrets, encrypt_vault_id_matchers) + + # return the best match for --encrypt-vault-id + if encrypt_secret: + return encrypt_secret + + # If we specified a encrypt_vault_id and we couldn't find it, dont + # fallback to using the first/best secret + raise AnsibleVaultError('Did not find a match for --encrypt-vault-id=%s in the known vault-ids %s' % (encrypt_vault_id, + [_v for _v, _vs in secrets])) + + +def match_encrypt_secret(secrets, encrypt_vault_id=None): '''Find the best/first/only secret in secrets to use for encrypting''' + display.vvvv('encrypt_vault_id=%s' % encrypt_vault_id) + # See if the --encrypt-vault-id matches a vault-id + if encrypt_vault_id: + return match_encrypt_vault_id_secret(secrets, + encrypt_vault_id=encrypt_vault_id) + + # Find the best/first secret from secrets since we didnt specify otherwise # ie, consider all of the available secrets as matches _vault_id_matchers = [_vault_id for _vault_id, dummy in secrets] best_secret = match_best_secret(secrets, _vault_id_matchers) + # can be empty list sans any tuple return best_secret @@ -625,7 +653,11 @@ class VaultLib: raise AnsibleError(u"{0} cipher could not be found".format(self.cipher_name)) # encrypt data - display.vvvvv('Encrypting with vault secret %s' % secret) + if vault_id: + display.vvvvv('Encrypting with vault_id "%s" and vault secret %s' % (vault_id, secret)) + else: + display.vvvvv('Encrypting without a vault_id using vault secret %s' % secret) + b_ciphertext = this_cipher.encrypt(b_plaintext, secret) # format the data for output to the file @@ -725,7 +757,10 @@ class VaultLib: b_plaintext = this_cipher.decrypt(b_vaulttext, vault_secret) if b_plaintext is not None: vault_id_used = vault_secret_id - display.vvvvv('decrypt successful with secret=%s and vault_id=%s' % (vault_secret, vault_secret_id)) + file_slug = '' + if filename: + file_slug = ' of "%s"' % filename + display.vvvvv('Decrypt%s successful with secret=%s and vault_id=%s' % (file_slug, vault_secret, vault_secret_id)) break except AnsibleVaultFormatError as exc: msg = "There was a vault format error" @@ -963,7 +998,7 @@ class VaultEditor: vaulttext = to_text(b_vaulttext) try: - plaintext = self.vault.decrypt(vaulttext) + plaintext = self.vault.decrypt(vaulttext, filename=filename) return plaintext except AnsibleError as e: raise AnsibleVaultError("%s for %s" % (to_bytes(e), to_bytes(filename))) @@ -978,8 +1013,10 @@ class VaultEditor: b_vaulttext = self.read_data(filename) vaulttext = to_text(b_vaulttext) + display.vvvvv('Rekeying file "%s" to with new vault-id "%s" and vault secret %s' % + (filename, new_vault_id, new_vault_secret)) try: - plaintext = self.vault.decrypt(vaulttext) + plaintext, vault_id_used = self.vault.decrypt_and_get_vault_id(vaulttext) except AnsibleError as e: raise AnsibleError("%s for %s" % (to_bytes(e), to_bytes(filename))) @@ -1004,6 +1041,9 @@ class VaultEditor: os.chmod(filename, prev.st_mode) os.chown(filename, prev.st_uid, prev.st_gid) + display.vvvvv('Rekeyed file "%s" (decrypted with vault id "%s") was encrypted with new vault-id "%s" and vault secret %s' % + (filename, vault_id_used, new_vault_id, new_vault_secret)) + def read_data(self, filename): try: diff --git a/test/integration/targets/vault/runme.sh b/test/integration/targets/vault/runme.sh index a8338220e44..ad890e58060 100755 --- a/test/integration/targets/vault/runme.sh +++ b/test/integration/targets/vault/runme.sh @@ -185,6 +185,13 @@ WRONG_RC=$? echo "rc was $WRONG_RC (1 is expected)" [ $WRONG_RC -eq 1 ] +# try specifying a --encrypt-vault-id that doesnt exist, should exit with an error indicating +# that --encrypt-vault-id and the known vault-ids +ansible-vault encrypt "$@" --vault-password-file vault-password --encrypt-vault-id doesnt_exist "${TEST_FILE}" && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + # encrypt it ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE}" @@ -252,6 +259,12 @@ ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE}" ansible-vault rekey "$@" --vault-password-file vault-password --new-vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}" +# --new-vault-password-file and --new-vault-id should cause options error +ansible-vault rekey "$@" --vault-password-file vault-password --new-vault-id=foobar --new-vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}" && : +WRONG_RC=$? +echo "rc was $WRONG_RC (2 is expected)" +[ $WRONG_RC -eq 2 ] + ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}" # view with old password file and new password file