diff --git a/ansible/module_utils/hashivault.py b/ansible/module_utils/hashivault.py index 1f666bc9..344368c8 100644 --- a/ansible/module_utils/hashivault.py +++ b/ansible/module_utils/hashivault.py @@ -399,3 +399,23 @@ def is_state_changed(desired_state, current_state, ignore=None): :rtype: bool """ return(len(get_keys_updated(desired_state, current_state)) > 0) + + +def parse_duration(duration, fallback=None): + if isinstance(duration, int): + return duration + elif not isinstance(duration, str): + return fallback + + if duration.endswith('d'): + return int(duration[:-1]) * 60 * 60 * 24 + if duration.endswith('h'): + return int(duration[:-1]) * 60 * 60 + if duration.endswith('m'): + return int(duration[:-1]) * 60 + if duration.endswith('s'): + return int(duration[:-1]) + if duration != "": + return int(duration) + + return fallback diff --git a/ansible/modules/hashivault/hashivault_pki_role.py b/ansible/modules/hashivault/hashivault_pki_role.py index 12757e76..fcca9f3d 100755 --- a/ansible/modules/hashivault/hashivault_pki_role.py +++ b/ansible/modules/hashivault/hashivault_pki_role.py @@ -1,11 +1,13 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +import copy + from ansible.module_utils.hashivault import hashivault_argspec from ansible.module_utils.hashivault import hashivault_auth_client from ansible.module_utils.hashivault import hashivault_init from ansible.module_utils.hashivault import hashivault_normalize_from_doc from ansible.module_utils.hashivault import hashiwrapper -from ansible.module_utils.hashivault import is_state_changed +from ansible.module_utils.hashivault import parse_duration ANSIBLE_METADATA = {'status': ['preview'], 'supported_by': 'community', 'version': '1.1'} DOCUMENTATION = r''' @@ -25,7 +27,7 @@ description: - location where secrets engine is mounted. also known as path name: - recuired: true + required: true description: - Specifies the name of the role to create. role_file: @@ -96,6 +98,13 @@ description: - Allows names specified in `allowed_domains` to contain glob patterns (e.g. `ftp*.example.com`) - Clients will be allowed to request certificates with names matching the glob patterns. + allow_wildcard_certificates: + type: bool + default: true + description: + - Allows the issuance of certificates with RFC 6125 wildcards in the CN field. + - When set to false, this prevents wildcards from being issued even if they would've + - been allowed by an option above. allow_any_name: type: bool default: false @@ -124,6 +133,12 @@ - Values can contain glob patterns (e.g. `spiffe://hostname/*`). - Although this parameter could take a string with comma-delimited items, it's highly advised to not do so as it would break idempotency. + allowed_uri_sans_template: + type: bool + default: false + description: + - When set, allowed_uri_sans may contain templates, as with ACL Path Templating. + - Non-templated domains are also still permitted. allowed_other_sans: type: list description: @@ -135,6 +150,14 @@ `(bool)` Specifies if certificates are flagged for server use. - Although this parameter could take a string with comma-delimited items, it's highly advised to not do so as it would break idempotency. + allowed_serial_numbers: + type: list + default: "" + description: + - If set, an array of allowed serial numbers to be requested during certificate issuance. + - These values support shell-style globbing. + - When empty, custom-specified serial numbers will be forbidden. + - It is strongly recommended to allow Vault to generate random serial numbers instead. server_flag: type: bool default: true @@ -165,10 +188,26 @@ keys of either type and with any bit size (subject to > 1024 bits for RSA keys). key_bits: type: int - default: 2048 + default: 0 + description: + - Specifies the number of bits to use for the generated keys. + - Allowed values are 0 (universal default); + - with key_type=rsa, allowed values are: 2048 (default), 3072, 4096 or 8192; + - with key_type=ec, allowed values are: 224, 256 (default), 384, or 521; + - ignored with key_type=ed25519 or in signing operations when key_type=any. + signature_bits: + type: int + default: 0 description: - Specifies the number of bits to use for the generated keys - This will need to be changed for `ec` keys, e.g., 224 or 521. + use_pss: + type: bool + default: false + description: + - Specifies whether or not to use PSS signatures over PKCS#1v1.5 signatures when a RSA-type issuer + - is used. + - Ignored for ECDSA/Ed25519 issuers. key_usage: type: list default: ["DigitalSignature", "KeyAgreement", "KeyEncipherment"] @@ -294,6 +333,25 @@ default: "30s" description: - Specifies the duration by which to backdate the NotBefore property. + not_after: + type: string + description: + - Set the Not After field of the certificate with specified date value. + - The value format should be given in UTC format YYYY-MM-ddTHH:MM:SSZ. + - Supports the Y10K end date for IEEE 802.1AR-2018 standard devices, 9999-12-31T23:59:59Z. + cn_validations: + type: list + default: ["email", "hostname"] + description: + - Validations to run on the Common Name field of the certificate. + allowed_user_ids: + type: string + default: "" + description: + - Comma separated, globbing list of User ID Subject components to allow on requests. + - By default, no user IDs are allowed. + - Use the bare wildcard * value to allow any value. + - See also the user_ids request parameter. extends_documentation_fragment: - hashivault ''' @@ -358,6 +416,24 @@ def hashivault_pki_role(module): except Exception as e: return e.args[0] + # For EC and ED25519 this field is ignored and leads to misleading diff. + if desired_state.get("key_type", None) in ("ed25519", "ec"): + desired_state.pop("signature_bits", None) + + # Normalize some keys. This is a quirk of the vault api that it + # expects a different data format in the PUT/POST endpoint than + # it returns in the GET endpoint. + # Thus we'll keep desired_state_comp for the diff purposes and use + # desired_state as the actual params to be POSTed + desired_state_comp = copy.deepcopy(desired_state) + + if desired_state_comp.get('ttl', None): + desired_state_comp['ttl'] = parse_duration(desired_state_comp['ttl']) + if desired_state_comp.get('max_ttl', None): + desired_state_comp['max_ttl'] = parse_duration(desired_state_comp['max_ttl']) + if desired_state_comp.get('not_before_duration', None): + desired_state_comp['not_before_duration'] = parse_duration(desired_state_comp['not_before_duration']) + changed = False try: current_state = client.secrets.pki.read_role(name=name, mount_point=mount_point).get('data') @@ -369,18 +445,33 @@ def hashivault_pki_role(module): if (exists and state == 'absent') or (not exists and state == 'present'): changed = True - # compare current_state to desired_state - if exists and state == 'present' and not changed: - changed = is_state_changed(desired_state, current_state) + # compare current_state to desired_state_comp + if exists and state == "present" and not changed: + # Update all keys not present in the desired_state_comp with data from the + # current_state, to ensure a proper diff output. + for key in current_state: + if key not in desired_state_comp: + desired_state_comp[key] = current_state[key] + + changed = desired_state_comp != current_state # make the changes! if changed and state == 'present' and not module.check_mode: client.secrets.pki.create_or_update_role(name=name, mount_point=mount_point, extra_params=desired_state) - elif changed and state == 'absent' and not module.check_mode: - client.secrets.pki.delete_role(name=name, mount_point=mount_point) + elif changed and state == 'absent': + if not module.check_mode: + client.secrets.pki.delete_role(name=name, mount_point=mount_point) + # after deleting it the item is no more + desired_state_comp = {} - return {'changed': changed} + return { + "changed": changed, + "diff": { + "before": current_state, + "after": desired_state_comp, + }, + } if __name__ == '__main__': diff --git a/ansible/modules/hashivault/hashivault_secret_engine.py b/ansible/modules/hashivault/hashivault_secret_engine.py index 591dfbf4..44636ee7 100644 --- a/ansible/modules/hashivault/hashivault_secret_engine.py +++ b/ansible/modules/hashivault/hashivault_secret_engine.py @@ -5,6 +5,7 @@ from ansible.module_utils.hashivault import hashivault_auth_client from ansible.module_utils.hashivault import hashivault_init from ansible.module_utils.hashivault import hashiwrapper +from ansible.module_utils.hashivault import parse_duration DEFAULT_TTL = 2764800 ANSIBLE_METADATA = {'status': ['stableinterface'], 'supported_by': 'community', 'version': '1.1'} @@ -125,26 +126,6 @@ def main(): module.exit_json(**result) -def parse_duration(duration): - if isinstance(duration, int): - return duration - elif not isinstance(duration, str): - return DEFAULT_TTL - - if duration.endswith('d'): - return int(duration[:-1]) * 60 * 60 * 24 - if duration.endswith('h'): - return int(duration[:-1]) * 60 * 60 - if duration.endswith('m'): - return int(duration[:-1]) * 60 - if duration.endswith('s'): - return int(duration[:-1]) - if duration != "": - return int(duration) - - return DEFAULT_TTL - - @hashiwrapper def hashivault_secret_engine(module): params = module.params @@ -154,9 +135,10 @@ def hashivault_secret_engine(module): description = params.get('description') config = params.get('config') if 'default_lease_ttl' in config: - config['default_lease_ttl'] = parse_duration(config['default_lease_ttl']) + config['default_lease_ttl'] = parse_duration(config['default_lease_ttl'], DEFAULT_TTL) if 'max_lease_ttl' in config: - config['max_lease_ttl'] = parse_duration(config['max_lease_ttl']) + config['max_lease_ttl'] = parse_duration(config['max_lease_ttl'], + DEFAULT_TTL) if params.get('state') in ['present', 'enabled']: state = 'enabled' else: diff --git a/functional/test_pki.yml b/functional/test_pki.yml index 442eaa1c..42993ad9 100644 --- a/functional/test_pki.yml +++ b/functional/test_pki.yml @@ -28,7 +28,7 @@ - debug: msg: "mount_root:\t{{ mount_root }}\nmount_inter:\t{{ mount_inter }}\nrole_name:\t{{ role }}" - - name: Enabele PKI secrets engine + - name: Enable PKI secrets engine hashivault_secret_engine: name: "{{mount_root}}" backend: "pki" @@ -56,7 +56,7 @@ - response.rc == 0 - response.changed == False - - name: Enabele PKI secrets engine + - name: Enable PKI secrets engine hashivault_secret_engine: name: "{{mount_inter}}" backend: "pki" @@ -253,15 +253,80 @@ that: - response.rc == 0 - response.changed == True + - name: Create/Update Role check_mode + hashivault_pki_role: + mount_point: "{{mount_inter}}" + name: "{{role}}" + check_mode: true + register: response + - assert: + that: + - response.rc == 0 + - response.changed == False + - response.diff.before == response.diff.after + - name: Create/Update Role + hashivault_pki_role: + mount_point: "{{mount_inter}}" + name: "{{role}}" + register: response + - assert: + that: + - response.rc == 0 + - response.changed == False + - response.diff.before == response.diff.after + - name: Create/Update Role + hashivault_pki_role: + mount_point: "{{mount_inter}}" + name: "{{role}}" + config: + max_ttl: "153" + ttl: "150" + not_before_duration: "45s" + register: response + - assert: + that: + - response.rc == 0 + - response.changed == True + - response.diff.before.max_ttl != response.diff.after.max_ttl + - response.diff.before.ttl != response.diff.after.ttl + - response.diff.before.not_before_duration != response.diff.after.not_before_duration - name: Create/Update Role hashivault_pki_role: mount_point: "{{mount_inter}}" name: "{{role}}" + config: + max_ttl: "153" + ttl: "150" + not_before_duration: "45s" register: response - assert: that: - response.rc == 0 - response.changed == False + - response.diff.before == response.diff.after + - name: Create/Update Role check_mode + hashivault_pki_role: + mount_point: "{{mount_inter}}" + name: "{{role}}" + config: + allow_bare_domains: True + allow_subdomains: True + allow_any_name: True + not_before_duration: "15s" + check_mode: true + register: response + - assert: + that: + - response.rc == 0 + - response.changed == True + - response.diff.before != response.diff.after + - response.diff.before.allow_bare_domains == False + - response.diff.after.allow_bare_domains == True + - response.diff.before.allow_subdomains == False + - response.diff.after.allow_subdomains == True + - response.diff.before.allow_any_name == False + - response.diff.after.allow_any_name == True + - response.diff.before.not_before_duration != response.diff.after.not_before_duration - name: Create/Update Role hashivault_pki_role: mount_point: "{{mount_inter}}" @@ -276,6 +341,29 @@ that: - response.rc == 0 - response.changed == True + - response.diff.before != response.diff.after + - response.diff.before.allow_bare_domains == False + - response.diff.after.allow_bare_domains == True + - response.diff.before.allow_subdomains == False + - response.diff.after.allow_subdomains == True + - response.diff.before.allow_any_name == False + - response.diff.after.allow_any_name == True + - response.diff.before.not_before_duration != response.diff.after.not_before_duration + - name: Create/Update Role, no diff + hashivault_pki_role: + mount_point: "{{mount_inter}}" + name: "{{role}}" + config: + allow_bare_domains: True + allow_subdomains: True + allow_any_name: True + not_before_duration: "15s" + register: response + - assert: + that: + - response.rc == 0 + - response.changed == False + - response.diff.before == response.diff.after - name: List Roles check_mode expect_fail hashivault_pki_role_list: