From 78b635ae78346fdfb298dd0d0c82ae1ff34b754a Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Tue, 23 Jun 2020 17:53:47 -0300 Subject: [PATCH] Add suppport for changing password of symmetric vaults. Allows changing passwords of symmetric waults, using a new variable `new_password` (or the file-base version, `new_password_file`). The old password must be passed using the `password` or `password_file` variables that also received new aliases `old_password` and `old_password_file`, respectively. Tests were modyfied to reflect the changes. --- README-vault.md | 23 +++- .../vault/change-password-symmetric-vault.yml | 2 +- plugins/modules/ipavault.py | 129 +++++++++++++++--- tests/vault/test_vault_symmetric.yml | 64 +++++++++ 4 files changed, 194 insertions(+), 24 deletions(-) diff --git a/README-vault.md b/README-vault.md index c7ae6916..fa1d3e11 100644 --- a/README-vault.md +++ b/README-vault.md @@ -165,6 +165,22 @@ Example playbook to make sure vault data is absent in a symmetric vault: state: absent ``` +Example playbook to change the password of a symmetric: + +```yaml +--- +- name: Playbook to handle vaults + hosts: ipaserver + become: true + + tasks: + - ipavault: + ipaadmin_password: SomeADMINpassword + name: symvault + old_password: SomeVAULTpassword + new_password: SomeNEWpassword +``` + Example playbook to make sure vault is absent: ```yaml @@ -197,8 +213,11 @@ Variable | Description | Required `name` \| `cn` | The list of vault name strings. | yes `description` | The vault description string. | no `nomembers` | Suppress processing of membership attributes. (bool) | no -`password ` \| `vault_password` \| `ipavaultpassword` | Vault password. | no -`public_key ` \| `vault_public_key` \| `ipavaultpublickey` | Base64 encoded vault public key. | no +`password` \| `vault_password` \| `ipavaultpassword` \| `old_password`| Vault password. | no +`password_file` \| `vault_password_file` \| `old_password_file`| File containing Base64 encoded Vault password. | no +`new_password` | Vault new password. | no +`new_password_file` | File containing Base64 encoded new Vault password. | no +`public_key ` \| `vault_public_key` \| `old_password_file` | Base64 encoded vault public key. | no `public_key_file` \| `vault_public_key_file` | Path to file with public key. | no `private_key `\| `vault_private_key` | Base64 encoded vault private key. Used only to retrieve data. | no `private_key_file` \| `vault_private_key_file` | Path to file with private key. Used only to retrieve data. | no diff --git a/playbooks/vault/change-password-symmetric-vault.yml b/playbooks/vault/change-password-symmetric-vault.yml index 3871f45d..396a79f6 100644 --- a/playbooks/vault/change-password-symmetric-vault.yml +++ b/playbooks/vault/change-password-symmetric-vault.yml @@ -10,7 +10,7 @@ ipaadmin_password: SomeADMINpassword name: symvault password: SomeVAULTpassword - - name: Change vault passord. + - name: Change vault password. ipavault: ipaadmin_password: SomeADMINpassword name: symvault diff --git a/plugins/modules/ipavault.py b/plugins/modules/ipavault.py index ad5dd413..46c6fcdb 100644 --- a/plugins/modules/ipavault.py +++ b/plugins/modules/ipavault.py @@ -69,12 +69,20 @@ description: password to be used on symmetric vault. required: false type: string - aliases: ["ipavaultpassword", "vault_password"] + aliases: ["ipavaultpassword", "vault_password", "old_password"] password_file: description: file with password to be used on symmetric vault. required: false type: string - aliases: ["vault_password_file"] + aliases: ["vault_password_file", "old_password_file"] + new_password: + description: new password to be used on symmetric vault. + required: false + type: string + new_password_file: + description: file with new password to be used on symmetric vault. + required: false + type: string salt: description: Vault salt. required: false @@ -235,7 +243,15 @@ state: retrieved register: result - debug: - msg: "{{ result.data | b64decode }}" + msg: "{{ result.data }}" + +# Change password of a symmetric vault +- ipavault: + ipaadmin_password: SomeADMINpassword + name: symvault + username: admin + old_password: SomeVAULTpassword + new_password: SomeNEWpassword # Ensure vault symvault is absent - ipavault: @@ -416,18 +432,29 @@ def check_parameters(module, state, action, description, username, service, shared, users, groups, services, owners, ownergroups, ownerservices, vault_type, salt, password, password_file, public_key, public_key_file, private_key, - private_key_file, vault_data, datafile_in, datafile_out): + private_key_file, vault_data, datafile_in, datafile_out, + new_password, new_password_file): invalid = [] if state == "present": invalid = ['private_key', 'private_key_file', 'datafile_out'] + if all([password, password_file]) \ + or all([new_password, new_password_file]): + module.fail_json(msg="Password specified multiple times.") + + if any([new_password, new_password_file]) \ + and not any([password, password_file]): + module.fail_json( + msg="Either `password` or `password_file` must be provided to " + "change symmetric vault password.") + if action == "member": invalid.extend(['description']) elif state == "absent": invalid = ['description', 'salt', 'vault_type', 'private_key', 'private_key_file', 'datafile_in', 'datafile_out', - 'vault_data'] + 'vault_data', 'new_password', 'new_password_file'] if action == "vault": invalid.extend(['users', 'groups', 'services', 'owners', @@ -437,7 +464,7 @@ def check_parameters(module, state, action, description, username, service, elif state == "retrieved": invalid = ['description', 'salt', 'datafile_in', 'users', 'groups', 'owners', 'ownergroups', 'public_key', 'public_key_file', - 'vault_data'] + 'vault_data', 'new_password', 'new_password_file'] if action == 'member': module.fail_json( msg="State `retrieved` do not support action `member`.") @@ -458,11 +485,17 @@ def check_parameters(module, state, action, description, username, service, def check_encryption_params(module, state, action, vault_type, salt, password, password_file, public_key, public_key_file, private_key, private_key_file, - vault_data, datafile_in, datafile_out, res_find): + vault_data, datafile_in, datafile_out, + new_password, new_password_file, res_find): vault_type_invalid = [] + + if res_find is not None: + vault_type = res_find['ipavaulttype'] + if vault_type == "standard": vault_type_invalid = ['public_key', 'public_key_file', 'password', - 'password_file', 'salt'] + 'password_file', 'salt', 'new_password', + 'new_password_file'] if vault_type is None or vault_type == "symmetric": vault_type_invalid = ['public_key', 'public_key_file', @@ -473,8 +506,14 @@ def check_encryption_params(module, state, action, vault_type, salt, msg="Symmetric vault requires password or password_file " "to store data or change `salt`.") + if any([new_password, new_password_file]) and res_find is None: + module.fail_json( + msg="Cannot modify password of inexistent vault.") + if vault_type == "asymmetric": - vault_type_invalid = ['password', 'password_file'] + vault_type_invalid = [ + 'password', 'password_file', 'new_password', 'new_password_file' + ] if not any([public_key, public_key_file]) and res_find is None: module.fail_json( msg="Assymmetric vault requires public_key " @@ -487,6 +526,43 @@ def check_encryption_params(module, state, action, vault_type, salt, (param, vault_type or 'symmetric')) +def change_password(module, res_find, password, password_file, new_password, + new_password_file): + """ + Change the password of a symmetric vault. + + To change the password of a vault, it is needed to retrieve the stored + data with the current password, and store the data again, with the new + password, forcing it to override the old one. + """ + # verify parameters. + if not any([new_password, new_password_file]): + return [] + if res_find["ipavaulttype"][0] != "symmetric": + module.fail_json(msg="Cannot change password of `%s` vault." + % res_find["ipavaulttype"]) + + # prepare arguments to retrieve data. + name = res_find["cn"][0] + args = {} + if password: + args["password"] = password + if password_file: + args["password"] = password_file + # retrieve current stored data + result = api_command(module, 'vault_retrieve', name, args) + args['data'] = result['result']['data'] + + # modify arguments to store data with new password. + if password: + args["password"] = new_password + if password_file: + args["password"] = new_password_file + args["override_password"] = True + # return the command to store data with the new password. + return [(name, "vault_archive", args)] + + def main(): ansible_module = AnsibleModule( argument_spec=dict( @@ -533,10 +609,18 @@ def main(): datafile_out=dict(type="str", required=False, default=None, aliases=['out']), vault_password=dict(type="str", required=False, default=None, - aliases=['ipavaultpassword', 'password'], - no_log=True), + no_log=True, + aliases=['ipavaultpassword', 'password', + "old_password"]), vault_password_file=dict(type="str", required=False, default=None, - no_log=False, aliases=['password_file']), + no_log=False, + aliases=[ + 'password_file', "old_password_file" + ]), + new_password=dict(type="str", required=False, default=None, + no_log=True), + new_password_file=dict(type="str", required=False, default=None, + no_log=False), # state action=dict(type="str", default="vault", choices=["vault", "data", "member"]), @@ -546,6 +630,7 @@ def main(): supports_check_mode=True, mutually_exclusive=[['username', 'service', 'shared'], ['datafile_in', 'vault_data'], + ['new_password', 'new_password_file'], ['vault_password', 'vault_password_file'], ['vault_public_key', 'vault_public_key_file']], ) @@ -576,6 +661,8 @@ def main(): salt = module_params_get(ansible_module, "vault_salt") password = module_params_get(ansible_module, "vault_password") password_file = module_params_get(ansible_module, "vault_password_file") + new_password = module_params_get(ansible_module, "new_password") + new_password_file = module_params_get(ansible_module, "new_password_file") public_key = module_params_get(ansible_module, "vault_public_key") public_key_file = module_params_get(ansible_module, "vault_public_key_file") @@ -614,7 +701,8 @@ def main(): service, shared, users, groups, services, owners, ownergroups, ownerservices, vault_type, salt, password, password_file, public_key, public_key_file, private_key, - private_key_file, vault_data, datafile_in, datafile_out) + private_key_file, vault_data, datafile_in, datafile_out, + new_password, new_password_file) # Init changed = False @@ -660,7 +748,7 @@ def main(): ansible_module, state, action, vault_type, salt, password, password_file, public_key, public_key_file, private_key, private_key_file, vault_data, datafile_in, datafile_out, - res_find) + new_password, new_password_file, res_find) # Found the vault if action == "vault": @@ -721,7 +809,6 @@ def main(): owner_add_args = gen_member_args( args, owner_add, ownergroups_add, ownerservice_add) if owner_add_args is not None: - # ansible_module.warn("OWNER ADD: %s" % owner_add_args) commands.append( [name, 'vault_add_owner', owner_add_args]) @@ -729,7 +816,6 @@ def main(): owner_del_args = gen_member_args( args, owner_del, ownergroups_del, ownerservice_del) if owner_del_args is not None: - # ansible_module.warn("OWNER DEL: %s" % owner_del_args) commands.append( [name, 'vault_remove_owner', owner_del_args]) @@ -758,19 +844,22 @@ def main(): if any([vault_data, datafile_in]): commands.append([name, "vault_archive", pwdargs]) + cmds = change_password( + ansible_module, res_find, password, password_file, + new_password, new_password_file) + commands.extend(cmds) + elif state == "retrieved": if res_find is None: ansible_module.fail_json( msg="Vault `%s` not found to retrieve data." % name) - vault_type = res_find['cn'] - # verify data encription args check_encryption_params( ansible_module, state, action, vault_type, salt, password, password_file, public_key, public_key_file, private_key, private_key_file, vault_data, datafile_in, datafile_out, - res_find) + new_password, new_password_file, res_find) pwdargs = data_storage_args( args, vault_data, password, password_file, private_key, @@ -813,7 +902,6 @@ def main(): errors = [] for name, command, args in commands: try: - # ansible_module.warn("RUN: %s %s %s" % (command, name, args)) result = api_command(ansible_module, command, name, args) if command == 'vault_archive': @@ -829,7 +917,6 @@ def main(): raise Exception("No data retrieved.") changed = False else: - # ansible_module.warn("RESULT: %s" % (result)) if "completed" in result: if result["completed"] > 0: changed = True diff --git a/tests/vault/test_vault_symmetric.yml b/tests/vault/test_vault_symmetric.yml index c9429f4f..a6072d88 100644 --- a/tests/vault/test_vault_symmetric.yml +++ b/tests/vault/test_vault_symmetric.yml @@ -178,6 +178,61 @@ register: result failed_when: result.data != 'Hello World.' or result.changed + - name: Change vault password. + ipavault: + ipaadmin_password: SomeADMINpassword + name: symvault + password: SomeVAULTpassword + new_password: SomeNEWpassword + register: result + failed_when: not result.changed + + - name: Retrieve data from symmetric vault, with wrong password. + ipavault: + ipaadmin_password: SomeADMINpassword + name: symvault + password: SomeVAULTpassword + state: retrieved + register: result + failed_when: not result.failed or "Invalid credentials" not in result.msg + + - name: Change vault password, with wrong `old_password`. + ipavault: + ipaadmin_password: SomeADMINpassword + name: symvault + password: SomeVAULTpassword + new_password: SomeNEWpassword + register: result + failed_when: not result.failed or "Invalid credentials" not in result.msg + + - name: Retrieve data from symmetric vault, with new password. + ipavault: + ipaadmin_password: SomeADMINpassword + name: symvault + password: SomeNEWpassword + state: retrieved + register: result + failed_when: result.data != 'Hello World.' or result.changed + + - name: Try to add vault with multiple passwords. + ipavault: + ipaadmin_password: SomeADMINpassword + name: inexistentvault + password: SomeVAULTpassword + password_file: "{{ ansible_env.HOME }}/password.txt" + register: result + failed_when: not result.failed or "parameters are mutually exclusive" not in result.msg + + - name: Try to add vault with multiple new passwords. + ipavault: + ipaadmin_password: SomeADMINpassword + name: inexistentvault + password: SomeVAULTpassword + new_password: SomeVAULTpassword + new_password_file: "{{ ansible_env.HOME }}/password.txt" + register: result + failed_when: not result.failed or "parameters are mutually exclusive" not in result.msg + - name: Ensure symmetric vault is absent ipavault: ipaadmin_password: SomeADMINpassword @@ -194,5 +249,14 @@ register: result failed_when: result.changed + - name: Try to change password of inexistent vault. + ipavault: + ipaadmin_password: SomeADMINpassword + name: inexistentvault + password: SomeVAULTpassword + new_password: SomeNEWpassword + register: result + failed_when: not result.failed or "Cannot modify password of inexistent vault" not in result.msg + - name: Cleanup testing environment. import_tasks: env_cleanup.yml