Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add diff mode support to hashivault_pki_role and update options list #484

Merged
merged 5 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions ansible/module_utils/hashivault.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for moving this


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
109 changes: 100 additions & 9 deletions ansible/modules/hashivault/hashivault_pki_role.py
Original file line number Diff line number Diff line change
@@ -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'''
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
'''
Expand Down Expand Up @@ -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')
Expand All @@ -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__':
Expand Down
26 changes: 4 additions & 22 deletions ansible/modules/hashivault/hashivault_secret_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
92 changes: 90 additions & 2 deletions functional/test_pki.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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}}"
Expand All @@ -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
hashivault_pki_role_list:
Expand Down
Loading