From d4d956d017a37b8108e6614caad2def34921653c Mon Sep 17 00:00:00 2001 From: Ruben Smits Date: Mon, 15 Jul 2024 14:26:12 +0000 Subject: [PATCH 1/4] Validate username --- plugins/modules/pfsense_user.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/modules/pfsense_user.py b/plugins/modules/pfsense_user.py index 6ca12837..3cefd815 100644 --- a/plugins/modules/pfsense_user.py +++ b/plugins/modules/pfsense_user.py @@ -160,6 +160,10 @@ def _validate_params(self): else: self.module.fail_json(msg='Password (%s) does not appear to be a bcrypt hash' % password) del params['password'] + if 'name' in params and params['name'] is not None: + name = params['name'] + if not re.match(r'^[a-zA-Z0-9._-]{1,16}$', str(name)): + self.module.fail_json(msg='Name (%s) invalid, must be 16 characters or less and may only contain letters, numbers, and a period, hyphen, or underscore' % name) def _params_to_obj(self): """ return a dict from module params """ From 87ca4ff81565214e65226cd9b4e73dd09aeb2407 Mon Sep 17 00:00:00 2001 From: Ruben Smits Date: Mon, 15 Jul 2024 14:27:29 +0000 Subject: [PATCH 2/4] Move Cert module to utils to enable reuse --- plugins/module_utils/cert.py | 304 ++++++++++++++++++++++++++++++++ plugins/modules/pfsense_cert.py | 294 +----------------------------- 2 files changed, 305 insertions(+), 293 deletions(-) create mode 100644 plugins/module_utils/cert.py diff --git a/plugins/module_utils/cert.py b/plugins/module_utils/cert.py new file mode 100644 index 00000000..96896e07 --- /dev/null +++ b/plugins/module_utils/cert.py @@ -0,0 +1,304 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Carlos Rodrigues +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +CERT_ARGUMENT_SPEC = dict( + name=dict(required=True, type='str'), + ca=dict(type='str'), + keytype=dict(type='str', default='RSA', choices=['RSA', 'ECDSA']), + digestalg=dict(type='str', default='sha256', choices=['sha1', 'sha224', 'sha256', 'sha384', 'sha512']), + ecname=dict( + type='str', + default='prime256v1', + choices=[ + 'secp112r1', 'secp112r2', 'secp128r1', 'secp128r2', 'secp160k1', 'secp160r1', 'secp160r2', + 'secp192k1', 'secp224k1', 'secp224r1', 'secp256k1', 'secp384r1', 'secp521r1', 'prime192v1', 'prime192v2', 'prime192v3', 'prime239v1', + 'prime239v2', 'prime239v3', 'prime256v1', 'sect113r1', 'sect113r2', 'sect131r1', 'sect131r2', 'sect163k1', 'sect163r1', 'sect163r2', + 'sect193r1', 'sect193r2', 'sect233k1', 'sect233r1', 'sect239k1', 'sect283k1', 'sect283r1', 'sect409k1', 'sect409r1', 'sect571k1', 'sect571r1', + 'c2pnb163v1', 'c2pnb163v2', 'c2pnb163v3', 'c2pnb176v1', 'c2tnb191v1', 'c2tnb191v2', 'c2tnb191v3', 'c2pnb208w1', 'c2tnb239v1', 'c2tnb239v2', + 'c2tnb239v3', 'c2pnb272w1', 'c2pnb304w1', 'c2tnb359v1', 'c2pnb368w1', 'c2tnb431r1', 'wap-wsg-idm-ecid-wtls1', 'wap-wsg-idm-ecid-wtls3', + 'wap-wsg-idm-ecid-wtls4', 'wap-wsg-idm-ecid-wtls5', 'wap-wsg-idm-ecid-wtls6', 'wap-wsg-idm-ecid-wtls7', 'wap-wsg-idm-ecid-wtls8', + 'wap-wsg-idm-ecid-wtls9', 'wap-wsg-idm-ecid-wtls10', 'wap-wsg-idm-ecid-wtls11', 'wap-wsg-idm-ecid-wtls12', 'Oakley-EC2N-3', 'Oakley-EC2N-4', + 'brainpoolP160r1', 'brainpoolP160t1', 'brainpoolP192r1', 'brainpoolP192t1', 'brainpoolP224r1', 'brainpoolP224t1', 'brainpoolP256r1', + 'brainpoolP256t1', 'brainpoolP320r1', 'brainpoolP320t1', 'brainpoolP384r1', 'brainpoolP384t1', 'brainpoolP512r1', 'brainpoolP512t1', 'SM2']), + keylen=dict(type='str', default='2048'), + lifetime=dict(type='str', default='3650'), + dn_country=dict(type='str'), + dn_state=dict(type='str'), + dn_city=dict(type='str'), + dn_organization=dict(type='str'), + dn_organizationalunit=dict(type='str'), + altnames=dict(type='str'), + certificate=dict(type='str'), + key=dict(type='str', no_log=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + method=dict(type='str', default='internal', choices=['internal', 'import']), + certtype=dict(type='str', default='user', choices=['user', 'server']), +) + +CERT_PHP_COMMAND_PREFIX = """ +require_once('certs.inc'); +init_config_arr(array('system', 'cert')); +""" + + +class PFSenseCertModule(PFSenseModuleBase): + """ module managing pfsense certificates """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return CERT_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseCertModule, self).__init__(module, pfsense) + self.name = "pfsense_cert" + self.root_elt = self.pfsense.root + self.certs = self.pfsense.get_elements('cert') + + ############################## + # params processing + # + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + + if params['state'] == 'absent': + return + + if params['method'] == 'internal': + # CA is required for internal certificate + if params['ca'] is None: + self.module.fail_json(msg='CA is required.') + + # validate Certificate + if params['certificate'] is not None: + cert = params['certificate'] + lines = cert.splitlines() + if lines[0] == '-----BEGIN CERTIFICATE-----' and lines[-1] == '-----END CERTIFICATE-----': + params['certificate'] = base64.b64encode(cert.encode()).decode() + elif not re.match('LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t', cert): + self.module.fail_json(msg='Could not recognize certificate format: %s' % (cert)) + + # validate key + if params['key'] is not None: + key = params['key'] + lines = key.splitlines() + if re.match('^-----BEGIN ((EC|RSA) )?PRIVATE KEY-----$', lines[0]) and re.match('^-----END ((EC|RSA) )?PRIVATE KEY-----$', lines[-1]): + params['key'] = base64.b64encode(key.encode()).decode() + elif not re.match('LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t', key): + self.module.fail_json(msg='Could not recognize key format: %s' % (key)) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + self.obj = obj + + # certificate description + obj['descr'] = params['name'] + if params['state'] == 'present': + + if params['ca'] is not None: + # found CA + ca = self._find_ca(params['ca']) + if ca is not None: + # get CA refid + obj['caref'] = ca.find('refid').text + else: + self.module.fail_json(msg='CA (%s) not found' % params['ca']) + + if 'certificate' in params and params['certificate'] is not None: + obj['crt'] = params['certificate'] + if 'key' in params and params['key'] is not None: + obj['prv'] = params['key'] + + return obj + + ############################## + # XML processing + # + def _find_target(self): + result = self.root_elt.findall("cert[descr='{0}']".format(self.obj['descr'])) + if len(result) == 1: + return result[0] + elif len(result) > 1: + self.module.fail_json(msg='Found multiple certificates for descr {0}.'.format(self.obj['descr'])) + else: + return None + + def _find_this_cert_index(self): + return self.certs.index(self.target_elt) + + def _find_last_cert_index(self): + return list(self.root_elt).index(self.certs[len(self.certs) - 1]) + + def _find_ca(self, caref): + result = self.root_elt.findall("ca[descr='{0}']".format(caref)) + if len(result) == 1: + return result[0] + elif len(result) > 1: + self.module.fail_json(msg='Found multiple CAs for caref {0}.'.format(caref)) + else: + result = self.root_elt.findall("ca[refid='{0}']".format(caref)) + if len(result) == 1: + return result[0] + elif len(result) > 1: + self.module.fail_json(msg='Found multiple CAs for caref {0}.'.format(caref)) + else: + return None + + def _create_target(self): + """ create the XML target_elt """ + return self.pfsense.new_element('cert') + + def _copy_and_add_target(self): + """ populate the XML target_elt """ + obj = self.obj + + obj['refid'] = self.pfsense.uniqid() + self.diff['after'] = obj + self.pfsense.copy_dict_to_element(self.obj, self.target_elt) + self.root_elt.insert(self._find_last_cert_index(), self.target_elt) + # Reset certs list + self.certs = self.root_elt.findall('cert') + + def _copy_and_update_target(self): + """ update the XML target_elt """ + + before = self.pfsense.element_to_dict(self.target_elt) + self.diff['before'] = before + + changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) + self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) + + return (before, changed) + + ############################## + # Logging + # + def _get_obj_name(self): + """ return obj's name """ + return "'" + self.obj['descr'] + "'" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + if before is None: + values += self.format_cli_field(self.params, 'descr') + else: + values += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(values)) + return values + + ############################## + # run + # + def _update(self): + if self.params['state'] == 'present': + if self.params['method'] == 'import': + # import certificate + return self.pfsense.phpshell(""" + require_once('certs.inc'); + init_config_arr(array('cert')); + $cert =& lookup_cert('{refid}'); + cert_import($cert, '{cert}', '{key}'); + $savemsg = sprintf(gettext("Imported certificate %s"), $cert['descr']); + write_config($savemsg);""".format(refid=self.target_elt.find('refid').text, + cert=base64.b64decode(self.target_elt.find('crt').text.encode()).decode(), + key=base64.b64decode(self.target_elt.find('prv').text.encode()).decode())) + else: + # generate internal certificate + return self.pfsense.phpshell(""" + require_once('certs.inc'); + init_config_arr(array('cert')); + $cert =& lookup_cert('{refid}'); + + $pconfig = array( 'dn_commonname' => '{dn_commonname}', + 'dn_country' => '{dn_country}', + 'dn_state' => '{dn_state}', + 'dn_city' => '{dn_city}', + 'dn_organization' => '{dn_organization}', + 'dn_organizationalunit' => '{dn_organizationalunit}', + 'dn_altnames' => '{altnames}' ); + + /* Create an internal certificate */ + $dn = array('commonName' => $pconfig['dn_commonname']); + if (!empty($pconfig['dn_country']) && ($pconfig['dn_country']!=='None')) {{ + $dn['countryName'] = $pconfig['dn_country']; + }} + if (!empty($pconfig['dn_state']) && ($pconfig['dn_state']!=='None')) {{ + $dn['stateOrProvinceName'] = $pconfig['dn_state']; + }} + if (!empty($pconfig['dn_city']) && ($pconfig['dn_city']!=='None')) {{ + $dn['localityName'] = $pconfig['dn_city']; + }} + if (!empty($pconfig['dn_organization']) && ($pconfig['dn_organization']!=='None')) {{ + $dn['organizationName'] = $pconfig['dn_organization']; + }} + if (!empty($pconfig['dn_organizationalunit']) && ($pconfig['dn_organizationalunit']!=='None')) {{ + $dn['organizationalUnitName'] = $pconfig['dn_organizationalunit']; + }} + $altnames_tmp = array(); + $cn_altname = cert_add_altname_type($pconfig['dn_commonname']); + if (!empty($cn_altname)) {{ + $altnames_tmp[] = $cn_altname; + }} + if (!empty($pconfig['dn_altnames']) && ($pconfig['dn_altnames']!=='None')) {{ + $altnames_tmp[] = $pconfig['dn_altnames']; + }} + if (!empty($altnames_tmp)) {{ + $dn['subjectAltName'] = implode(",", $altnames_tmp); + }} + + if (!cert_create($cert, '{caref}', '{keylen}', '{lifetime}', $dn, '{certtype}', '{digest_alg}', '{keytype}', '{ecname}')) {{ + $input_errors = array(); + while ($ssl_err = openssl_error_string()) {{ + if (strpos($ssl_err, 'NCONF_get_string:no value') === false) {{ + $input_errors[] = sprintf(gettext("OpenSSL Library Error: %s"), $ssl_err); + }} + }} + print_r($input_errors); + }} + $savemsg = sprintf(gettext("Created internal certificate %s"), $cert['descr']); + write_config($savemsg);""".format(refid=self.target_elt.find('refid').text, + dn_commonname=self.params['name'], + dn_country=self.params['dn_country'], + dn_state=self.params['dn_state'], + dn_city=self.params['dn_city'], + dn_organization=self.params['dn_organization'], + dn_organizationalunit=self.params['dn_organizationalunit'], + altnames=self.params['altnames'], + caref=self.target_elt.find('caref').text, + keylen=self.params['keylen'], + lifetime=self.params['lifetime'], + certtype=self.params['certtype'], + keytype=self.params['keytype'], + digest_alg=self.params['digestalg'], + ecname=self.params['ecname'])) + else: + return (None, '', '') + + def _pre_remove_target_elt(self): + self.diff['after'] = {} + if self.target_elt is not None: + self.diff['before'] = self.pfsense.element_to_dict(self.target_elt) + self.certs.remove(self.target_elt) + else: + self.diff['before'] = {} + diff --git a/plugins/modules/pfsense_cert.py b/plugins/modules/pfsense_cert.py index 0dca97cc..5e86bb54 100644 --- a/plugins/modules/pfsense_cert.py +++ b/plugins/modules/pfsense_cert.py @@ -149,299 +149,8 @@ """ -import base64 -import re - from ansible.module_utils.basic import AnsibleModule -from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase - -CERT_ARGUMENT_SPEC = dict( - name=dict(required=True, type='str'), - ca=dict(type='str'), - keytype=dict(type='str', default='RSA', choices=['RSA', 'ECDSA']), - digestalg=dict(type='str', default='sha256', choices=['sha1', 'sha224', 'sha256', 'sha384', 'sha512']), - ecname=dict( - type='str', - default='prime256v1', - choices=[ - 'secp112r1', 'secp112r2', 'secp128r1', 'secp128r2', 'secp160k1', 'secp160r1', 'secp160r2', - 'secp192k1', 'secp224k1', 'secp224r1', 'secp256k1', 'secp384r1', 'secp521r1', 'prime192v1', 'prime192v2', 'prime192v3', 'prime239v1', - 'prime239v2', 'prime239v3', 'prime256v1', 'sect113r1', 'sect113r2', 'sect131r1', 'sect131r2', 'sect163k1', 'sect163r1', 'sect163r2', - 'sect193r1', 'sect193r2', 'sect233k1', 'sect233r1', 'sect239k1', 'sect283k1', 'sect283r1', 'sect409k1', 'sect409r1', 'sect571k1', 'sect571r1', - 'c2pnb163v1', 'c2pnb163v2', 'c2pnb163v3', 'c2pnb176v1', 'c2tnb191v1', 'c2tnb191v2', 'c2tnb191v3', 'c2pnb208w1', 'c2tnb239v1', 'c2tnb239v2', - 'c2tnb239v3', 'c2pnb272w1', 'c2pnb304w1', 'c2tnb359v1', 'c2pnb368w1', 'c2tnb431r1', 'wap-wsg-idm-ecid-wtls1', 'wap-wsg-idm-ecid-wtls3', - 'wap-wsg-idm-ecid-wtls4', 'wap-wsg-idm-ecid-wtls5', 'wap-wsg-idm-ecid-wtls6', 'wap-wsg-idm-ecid-wtls7', 'wap-wsg-idm-ecid-wtls8', - 'wap-wsg-idm-ecid-wtls9', 'wap-wsg-idm-ecid-wtls10', 'wap-wsg-idm-ecid-wtls11', 'wap-wsg-idm-ecid-wtls12', 'Oakley-EC2N-3', 'Oakley-EC2N-4', - 'brainpoolP160r1', 'brainpoolP160t1', 'brainpoolP192r1', 'brainpoolP192t1', 'brainpoolP224r1', 'brainpoolP224t1', 'brainpoolP256r1', - 'brainpoolP256t1', 'brainpoolP320r1', 'brainpoolP320t1', 'brainpoolP384r1', 'brainpoolP384t1', 'brainpoolP512r1', 'brainpoolP512t1', 'SM2']), - keylen=dict(type='str', default='2048'), - lifetime=dict(type='str', default='3650'), - dn_country=dict(type='str'), - dn_state=dict(type='str'), - dn_city=dict(type='str'), - dn_organization=dict(type='str'), - dn_organizationalunit=dict(type='str'), - altnames=dict(type='str'), - certificate=dict(type='str'), - key=dict(type='str', no_log=True), - state=dict(type='str', default='present', choices=['present', 'absent']), - method=dict(type='str', default='internal', choices=['internal', 'import']), - certtype=dict(type='str', default='user', choices=['user', 'server']), -) - -CERT_PHP_COMMAND_PREFIX = """ -require_once('certs.inc'); -init_config_arr(array('system', 'cert')); -""" - - -class PFSenseCertModule(PFSenseModuleBase): - """ module managing pfsense certificates """ - - @staticmethod - def get_argument_spec(): - """ return argument spec """ - return CERT_ARGUMENT_SPEC - - ############################## - # init - # - def __init__(self, module, pfsense=None): - super(PFSenseCertModule, self).__init__(module, pfsense) - self.name = "pfsense_cert" - self.root_elt = self.pfsense.root - self.certs = self.pfsense.get_elements('cert') - - ############################## - # params processing - # - def _validate_params(self): - """ do some extra checks on input parameters """ - params = self.params - - if params['state'] == 'absent': - return - - if params['method'] == 'internal': - # CA is required for internal certificate - if params['ca'] is None: - self.module.fail_json(msg='CA is required.') - - # validate Certificate - if params['certificate'] is not None: - cert = params['certificate'] - lines = cert.splitlines() - if lines[0] == '-----BEGIN CERTIFICATE-----' and lines[-1] == '-----END CERTIFICATE-----': - params['certificate'] = base64.b64encode(cert.encode()).decode() - elif not re.match('LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t', cert): - self.module.fail_json(msg='Could not recognize certificate format: %s' % (cert)) - - # validate key - if params['key'] is not None: - key = params['key'] - lines = key.splitlines() - if re.match('^-----BEGIN ((EC|RSA) )?PRIVATE KEY-----$', lines[0]) and re.match('^-----END ((EC|RSA) )?PRIVATE KEY-----$', lines[-1]): - params['key'] = base64.b64encode(key.encode()).decode() - elif not re.match('LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t', key): - self.module.fail_json(msg='Could not recognize key format: %s' % (key)) - - def _params_to_obj(self): - """ return a dict from module params """ - params = self.params - - obj = dict() - self.obj = obj - - # certificate description - obj['descr'] = params['name'] - if params['state'] == 'present': - - if params['ca'] is not None: - # found CA - ca = self._find_ca(params['ca']) - if ca is not None: - # get CA refid - obj['caref'] = ca.find('refid').text - else: - self.module.fail_json(msg='CA (%s) not found' % params['ca']) - - if 'certificate' in params and params['certificate'] is not None: - obj['crt'] = params['certificate'] - if 'key' in params and params['key'] is not None: - obj['prv'] = params['key'] - - return obj - - ############################## - # XML processing - # - def _find_target(self): - result = self.root_elt.findall("cert[descr='{0}']".format(self.obj['descr'])) - if len(result) == 1: - return result[0] - elif len(result) > 1: - self.module.fail_json(msg='Found multiple certificates for descr {0}.'.format(self.obj['descr'])) - else: - return None - - def _find_this_cert_index(self): - return self.certs.index(self.target_elt) - - def _find_last_cert_index(self): - return list(self.root_elt).index(self.certs[len(self.certs) - 1]) - - def _find_ca(self, caref): - result = self.root_elt.findall("ca[descr='{0}']".format(caref)) - if len(result) == 1: - return result[0] - elif len(result) > 1: - self.module.fail_json(msg='Found multiple CAs for caref {0}.'.format(caref)) - else: - result = self.root_elt.findall("ca[refid='{0}']".format(caref)) - if len(result) == 1: - return result[0] - elif len(result) > 1: - self.module.fail_json(msg='Found multiple CAs for caref {0}.'.format(caref)) - else: - return None - - def _create_target(self): - """ create the XML target_elt """ - return self.pfsense.new_element('cert') - - def _copy_and_add_target(self): - """ populate the XML target_elt """ - obj = self.obj - - obj['refid'] = self.pfsense.uniqid() - self.diff['after'] = obj - self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self.root_elt.insert(self._find_last_cert_index(), self.target_elt) - # Reset certs list - self.certs = self.root_elt.findall('cert') - - def _copy_and_update_target(self): - """ update the XML target_elt """ - - before = self.pfsense.element_to_dict(self.target_elt) - self.diff['before'] = before - - changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - - return (before, changed) - - ############################## - # Logging - # - def _get_obj_name(self): - """ return obj's name """ - return "'" + self.obj['descr'] + "'" - - def _log_fields(self, before=None): - """ generate pseudo-CLI command fields parameters to create an obj """ - values = '' - if before is None: - values += self.format_cli_field(self.params, 'descr') - else: - values += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(values)) - return values - - ############################## - # run - # - def _update(self): - if self.params['state'] == 'present': - if self.params['method'] == 'import': - # import certificate - return self.pfsense.phpshell(""" - require_once('certs.inc'); - init_config_arr(array('cert')); - $cert =& lookup_cert('{refid}'); - cert_import($cert, '{cert}', '{key}'); - $savemsg = sprintf(gettext("Imported certificate %s"), $cert['descr']); - write_config($savemsg);""".format(refid=self.target_elt.find('refid').text, - cert=base64.b64decode(self.target_elt.find('crt').text.encode()).decode(), - key=base64.b64decode(self.target_elt.find('prv').text.encode()).decode())) - else: - # generate internal certificate - return self.pfsense.phpshell(""" - require_once('certs.inc'); - init_config_arr(array('cert')); - $cert =& lookup_cert('{refid}'); - - $pconfig = array( 'dn_commonname' => '{dn_commonname}', - 'dn_country' => '{dn_country}', - 'dn_state' => '{dn_state}', - 'dn_city' => '{dn_city}', - 'dn_organization' => '{dn_organization}', - 'dn_organizationalunit' => '{dn_organizationalunit}', - 'dn_altnames' => '{altnames}' ); - - /* Create an internal certificate */ - $dn = array('commonName' => $pconfig['dn_commonname']); - if (!empty($pconfig['dn_country']) && ($pconfig['dn_country']!=='None')) {{ - $dn['countryName'] = $pconfig['dn_country']; - }} - if (!empty($pconfig['dn_state']) && ($pconfig['dn_state']!=='None')) {{ - $dn['stateOrProvinceName'] = $pconfig['dn_state']; - }} - if (!empty($pconfig['dn_city']) && ($pconfig['dn_city']!=='None')) {{ - $dn['localityName'] = $pconfig['dn_city']; - }} - if (!empty($pconfig['dn_organization']) && ($pconfig['dn_organization']!=='None')) {{ - $dn['organizationName'] = $pconfig['dn_organization']; - }} - if (!empty($pconfig['dn_organizationalunit']) && ($pconfig['dn_organizationalunit']!=='None')) {{ - $dn['organizationalUnitName'] = $pconfig['dn_organizationalunit']; - }} - $altnames_tmp = array(); - $cn_altname = cert_add_altname_type($pconfig['dn_commonname']); - if (!empty($cn_altname)) {{ - $altnames_tmp[] = $cn_altname; - }} - if (!empty($pconfig['dn_altnames']) && ($pconfig['dn_altnames']!=='None')) {{ - $altnames_tmp[] = $pconfig['dn_altnames']; - }} - if (!empty($altnames_tmp)) {{ - $dn['subjectAltName'] = implode(",", $altnames_tmp); - }} - - if (!cert_create($cert, '{caref}', '{keylen}', '{lifetime}', $dn, '{certtype}', '{digest_alg}', '{keytype}', '{ecname}')) {{ - $input_errors = array(); - while ($ssl_err = openssl_error_string()) {{ - if (strpos($ssl_err, 'NCONF_get_string:no value') === false) {{ - $input_errors[] = sprintf(gettext("OpenSSL Library Error: %s"), $ssl_err); - }} - }} - print_r($input_errors); - }} - $savemsg = sprintf(gettext("Created internal certificate %s"), $cert['descr']); - write_config($savemsg);""".format(refid=self.target_elt.find('refid').text, - dn_commonname=self.params['name'], - dn_country=self.params['dn_country'], - dn_state=self.params['dn_state'], - dn_city=self.params['dn_city'], - dn_organization=self.params['dn_organization'], - dn_organizationalunit=self.params['dn_organizationalunit'], - altnames=self.params['altnames'], - caref=self.target_elt.find('caref').text, - keylen=self.params['keylen'], - lifetime=self.params['lifetime'], - certtype=self.params['certtype'], - keytype=self.params['keytype'], - digest_alg=self.params['digestalg'], - ecname=self.params['ecname'])) - else: - return (None, '', '') - - def _pre_remove_target_elt(self): - self.diff['after'] = {} - if self.target_elt is not None: - self.diff['before'] = self.pfsense.element_to_dict(self.target_elt) - self.certs.remove(self.target_elt) - else: - self.diff['before'] = {} +from ansible_collections.pfsensible.core.plugins.module_utils.cert import PFSenseCertModule, CERT_ARGUMENT_SPEC def main(): @@ -453,6 +162,5 @@ def main(): pfmodule.run(module.params) pfmodule.commit_changes() - if __name__ == '__main__': main() From 9290f7238a439e8f4b4d61b64bbd996e238e66f6 Mon Sep 17 00:00:00 2001 From: Ruben Smits Date: Tue, 16 Jul 2024 12:20:40 +0000 Subject: [PATCH 3/4] Update user module to create user certificate if provided --- plugins/module_utils/cert.py | 1 - plugins/module_utils/user.py | 278 +++++++++++++++++++++ plugins/modules/pfsense_user.py | 423 ++++++++++++-------------------- 3 files changed, 438 insertions(+), 264 deletions(-) create mode 100644 plugins/module_utils/user.py diff --git a/plugins/module_utils/cert.py b/plugins/module_utils/cert.py index 96896e07..7a958640 100644 --- a/plugins/module_utils/cert.py +++ b/plugins/module_utils/cert.py @@ -11,7 +11,6 @@ import base64 import re -from ansible.module_utils.basic import AnsibleModule from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase CERT_ARGUMENT_SPEC = dict( diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py new file mode 100644 index 00000000..f3c613a2 --- /dev/null +++ b/plugins/module_utils/user.py @@ -0,0 +1,278 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019-2020, Orion Poplawski +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import base64 +import re + +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + +USER_ARGUMENT_SPEC = dict( + name=dict(required=True, type='str'), + state=dict(type='str', default='present', choices=['present', 'absent']), + descr=dict(type='str'), + scope=dict(type='str', default='user', choices=['user', 'system']), + uid=dict(type='str'), + password=dict(type='str', no_log=True), + groups=dict(type='list', elements='str'), + priv=dict(type='list', elements='str'), + authorizedkeys=dict(type='str'), +) + +USER_PHP_COMMAND_PREFIX = """ +require_once('auth.inc'); +init_config_arr(array('system', 'user')); +""" + +USER_PHP_COMMAND_SET = USER_PHP_COMMAND_PREFIX + """ +$a_user = &$config['system']['user']; +$userent = $a_user[{idx}]; +local_user_set($userent); +global $groupindex; +foreach ({mod_groups} as $groupname) {{ + $group = &$config['system']['group'][$groupindex[$groupname]]; + local_group_set($group); +}} +if (is_dir("/etc/inc/privhooks")) {{ + run_plugins("/etc/inc/privhooks"); +}} +""" + +# This runs after we remove the group from the config so we can't use $config +USER_PHP_COMMAND_DEL = USER_PHP_COMMAND_PREFIX + """ +$userent['name'] = '{name}'; +$userent['uid'] = {uid}; +global $groupindex; +foreach ({mod_groups} as $groupname) {{ + $group = &$config['system']['group'][$groupindex[$groupname]]; + local_group_set($group); +}} +local_user_del($userent); +""" + + +class PFSenseUserModule(PFSenseModuleBase): + """ module managing pfsense users """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return USER_ARGUMENT_SPEC + + def __init__(self, module, pfsense=None): + super(PFSenseUserModule, self).__init__(module, pfsense) + self.name = "pfsense_user" + self.root_elt = self.pfsense.get_element('system') + self.users = self.root_elt.findall('user') + self.groups = self.root_elt.findall('group') + self.mod_groups = [] + + ############################## + # params processing + # + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + if 'password' in params and params['password'] is not None: + password = params['password'] + if re.match(r'\$2[aby]\$', str(password)): + params['bcrypt-hash'] = password + else: + self.module.fail_json(msg='Password (%s) does not appear to be a bcrypt hash' % password) + del params['password'] + if 'name' in params and params['name'] is not None: + name = params['name'] + if not re.match(r'^[a-zA-Z0-9._-]{1,16}$', str(name)): + self.module.fail_json(msg='Name (%s) invalid, must be 16 characters or less and may only contain letters, numbers, and a period, hyphen, or underscore' % name) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + self.obj = obj + + obj['name'] = params['name'] + if params['state'] == 'present': + for option in ['authorizedkeys', 'descr', 'scope', 'uid', 'bcrypt-hash', 'groups', 'priv', 'cert']: + if option in params and params[option] is not None: + obj[option] = params[option] + + # Allow authorizedkeys to be clear or base64 encoded + if 'authorizedkeys' in obj and 'ssh-' in obj['authorizedkeys']: + obj['authorizedkeys'] = base64.b64encode(obj['authorizedkeys'].encode()).decode() + + return obj + + ############################## + # XML processing + # + def _find_target(self): + result = self.root_elt.findall("user[name='{0}']".format(self.obj['name'])) + if len(result) == 1: + return result[0] + elif len(result) > 1: + self.module.fail_json(msg='Found multiple users for name {0}.'.format(self.obj['name'])) + else: + return None + + def _find_group(self, name): + result = self.root_elt.findall("group[name='{0}']".format(name)) + if len(result) == 1: + return result[0] + elif len(result) > 1: + self.module.fail_json(msg='Found multiple groups for name {0}.'.format(name)) + else: + return None + + def _find_groups_for_uid(self, uid): + groups = [] + for group_elt in self.root_elt.findall("group[member='{0}']".format(uid)): + groups.append(group_elt.find('name').text) + return groups + + def _find_this_user_index(self): + return self.users.index(self.target_elt) + + def _find_last_user_index(self): + return list(self.root_elt).index(self.users[len(self.users) - 1]) + + def _nextuid(self): + nextuid_elt = self.root_elt.find('nextuid') + nextuid = nextuid_elt.text + nextuid_elt.text = str(int(nextuid) + 1) + return nextuid + + def _format_diff_priv(self, priv): + if isinstance(priv, str): + return [priv] + else: + return priv + + def _create_target(self): + """ create the XML target_elt """ + return self.pfsense.new_element('user') + + def _copy_and_add_target(self): + """ populate the XML target_elt """ + obj = self.obj + if 'bcrypt-hash' not in obj: + self.module.fail_json(msg='Password is required when adding a user') + if 'uid' not in obj: + obj['uid'] = self._nextuid() + + self.diff['after'] = obj + self.pfsense.copy_dict_to_element(self.obj, self.target_elt) + self._update_groups() + self.root_elt.insert(self._find_last_user_index(), self.target_elt) + # Reset users list + self.users = self.root_elt.findall('user') + + def _copy_and_update_target(self): + """ update the XML target_elt """ + before = self.pfsense.element_to_dict(self.target_elt) + self.diff['before'] = before + if 'priv' in before: + before['priv'] = self._format_diff_priv(before['priv']) + changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) + self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) + if 'priv' in self.diff['after']: + self.diff['after']['priv'] = self._format_diff_priv(self.diff['after']['priv']) + if self._update_groups(): + changed = True + + return (before, changed) + + def _update_groups(self): + user = self.obj + changed = False + + # Handle group member element - need uid set or retrieved above + if 'groups' in user: + uid = self.target_elt.find('uid').text + # Get current group membership + self.diff['before']['groups'] = self._find_groups_for_uid(uid) + + # Add user to groups if needed + for group in self.obj['groups']: + group_elt = self._find_group(group) + if group_elt is None: + self.module.fail_json(msg='Group (%s) does not exist' % group) + if len(group_elt.findall("[member='{0}']".format(uid))) == 0: + changed = True + self.mod_groups.append(group) + group_elt.append(self.pfsense.new_element('member', uid)) + + # Remove user from groups if needed + for group in self.diff['before']['groups']: + if group not in self.obj['groups']: + group_elt = self._find_group(group) + if group_elt is None: + self.module.fail_json(msg='Group (%s) does not exist' % group) + for member_elt in group_elt.findall('member'): + if member_elt.text == uid: + changed = True + self.mod_groups.append(group) + group_elt.remove(member_elt) + break + + # Groups are not stored in the user element + self.diff['after']['groups'] = user.pop('groups') + + # Decode keys for diff + for k in self.diff: + if 'authorizedkeys' in self.diff[k]: + self.diff[k]['authorizedkeys'] = base64.b64decode(self.diff[k]['authorizedkeys']) + + return changed + + ############################## + # Logging + # + def _get_obj_name(self): + """ return obj's name """ + return "'" + self.obj['name'] + "'" + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + if before is None: + values += self.format_cli_field(self.params, 'descr') + else: + values += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(values)) + return values + + ############################## + # run + # + def _update(self): + if self.params['state'] == 'present': + return self.pfsense.phpshell(USER_PHP_COMMAND_SET.format(idx=self._find_this_user_index(), mod_groups=self.mod_groups)) + else: + return self.pfsense.phpshell(USER_PHP_COMMAND_DEL.format(name=self.obj['name'], uid=self.obj['uid'], mod_groups=self.mod_groups)) + + def _pre_remove_target_elt(self): + self.diff['after'] = {} + if self.target_elt is not None: + self.diff['before'] = self.pfsense.element_to_dict(self.target_elt) + # Store uid for _update() + self.obj['uid'] = self.target_elt.find('uid').text + + # Get current group membership + self.diff['before']['groups'] = self._find_groups_for_uid(self.obj['uid']) + + # Remove user from groups if needed + for group in self.diff['before']['groups']: + group_elt = self._find_group(group) + if group_elt is None: + self.module.fail_json(msg='Group (%s) does not exist' % group) + for member_elt in group_elt.findall('member'): + if member_elt.text == self.obj['uid']: + self.mod_groups.append(group) + group_elt.remove(member_elt) + break diff --git a/plugins/modules/pfsense_user.py b/plugins/modules/pfsense_user.py index 3cefd815..a2946ee4 100644 --- a/plugins/modules/pfsense_user.py +++ b/plugins/modules/pfsense_user.py @@ -5,6 +5,7 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function + __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', @@ -60,6 +61,97 @@ authorizedkeys: description: Contents of ~/.ssh/authorized_keys. Can be base64 encoded. type: str + cert: + description: options for the users certificate. + type: dict + suboptions: + name: + description: The name of the certificate + required: true + type: str + ca: + description: The Certificate Authority + type: str + keytype: + description: The type of key to generate + default: 'RSA' + choices: [ 'RSA', 'ECDSA' ] + type: str + digestalg: + description: The digest method used when the certificate is signed + default: 'sha256' + choices: ['sha1', 'sha224', 'sha256', 'sha384', 'sha512'] + type: str + ecname: + description: The Elliptic Curve Name to use when generating a new ECDSA key + default: 'prime256v1' + choices: ['secp112r1', 'secp112r2', 'secp128r1', 'secp128r2', 'secp160k1', 'secp160r1', 'secp160r2', 'secp192k1', 'secp224k1', 'secp224r1', + 'secp256k1', 'secp384r1', 'secp521r1', 'prime192v1', 'prime192v2', 'prime192v3', 'prime239v1', 'prime239v2', 'prime239v3', 'prime256v1', + 'sect113r1', 'sect113r2', 'sect131r1', 'sect131r2', 'sect163k1', 'sect163r1', 'sect163r2', 'sect193r1', 'sect193r2', 'sect233k1', 'sect233r1', + 'sect239k1', 'sect283k1', 'sect283r1', 'sect409k1', 'sect409r1', 'sect571k1', 'sect571r1', 'c2pnb163v1', 'c2pnb163v2', 'c2pnb163v3', 'c2pnb176v1', + 'c2tnb191v1', 'c2tnb191v2', 'c2tnb191v3', 'c2pnb208w1', 'c2tnb239v1', 'c2tnb239v2', 'c2tnb239v3', 'c2pnb272w1', 'c2pnb304w1', 'c2tnb359v1', + 'c2pnb368w1', 'c2tnb431r1', 'wap-wsg-idm-ecid-wtls1', 'wap-wsg-idm-ecid-wtls3', 'wap-wsg-idm-ecid-wtls4', 'wap-wsg-idm-ecid-wtls5', + 'wap-wsg-idm-ecid-wtls6', 'wap-wsg-idm-ecid-wtls7', 'wap-wsg-idm-ecid-wtls8', 'wap-wsg-idm-ecid-wtls9', 'wap-wsg-idm-ecid-wtls10', + 'wap-wsg-idm-ecid-wtls11', 'wap-wsg-idm-ecid-wtls12', 'Oakley-EC2N-3', 'Oakley-EC2N-4', 'brainpoolP160r1', 'brainpoolP160t1', 'brainpoolP192r1', + 'brainpoolP192t1', 'brainpoolP224r1', 'brainpoolP224t1', 'brainpoolP256r1', 'brainpoolP256t1', 'brainpoolP320r1', 'brainpoolP320t1', + 'brainpoolP384r1', 'brainpoolP384t1', 'brainpoolP512r1', 'brainpoolP512t1', 'SM2'] + type: str + keylen: + description: The length to use when generating a new RSA key, in bits + default: '2048' + type: str + lifetime: + description: The length of time the signed certificate will be valid, in days + default: '3650' + type: str + dn_country: + description: The Country Code + type: str + dn_state: + description: The State or Province + type: str + dn_city: + description: The City + type: str + dn_organization: + description: The Organization + type: str + dn_organizationalunit: + description: The Organizational Unit + type: str + altnames: + description: + > + The Alternative Names. A list of aditional identifiers for the certificate. + A comma separed values with format: DNS:hostname,IP:X.X.X.X,email:user@mail,URI:url + type: str + certificate: + description: + > + The certificate to import. This can be in PEM form or Base64 + encoded PEM as a single string (which is how pfSense stores it). + type: str + key: + description: + > + The key to import. This can be in PEM form or Base64 + encoded PEM as a single string (which is how pfSense stores it). + type: str + state: + description: State in which to leave the certificate + default: 'present' + choices: [ 'present', 'absent' ] + type: str + method: + description: Method of the certificate created + default: 'internal' + choices: [ 'internal', 'import' ] + type: str + certtype: + description: Type of the certificate ('user' is a certificate for the user) + default: 'user' + choices: [ 'user', 'server' ] + type: str """ EXAMPLES = """ @@ -71,6 +163,23 @@ groups: [ 'Operators' ] priv: [ 'page-all', 'user-shell-access' ] +- name: Add operator user with certificate + pfsense_user: + name: operator + descr: Operator + scope: user + groups: [ 'Operators' ] + priv: [ 'page-all', 'user-shell-access' ] + cert: + method: "internal" + name: "operator.cert" + ca: "internal-ca" + keytype: "RSA" + keylen: 2048 + lifetime: 3650 + certtype: "user" + state: present + - name: Remove user pfsense_user: name: operator @@ -85,9 +194,52 @@ import re from ansible.module_utils.basic import AnsibleModule -from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModule +from ansible_collections.pfsensible.core.plugins.module_utils.cert import PFSenseCertModule, CERT_ARGUMENT_SPEC +from ansible_collections.pfsensible.core.plugins.module_utils.user import PFSenseUserModule, USER_ARGUMENT_SPEC -USER_ARGUMENT_SPEC = dict( +class PFSenseUserCertModule(object): + """ module managing pfsense users """ + + def __init__(self, module): + self.module = module + self.pfsense = PFSenseModule(module) + self.pfsense_cert = PFSenseCertModule(module, self.pfsense) + self.pfsense_user = PFSenseUserModule(module, self.pfsense) + + def run(self): + """process input params to create user""" + cert_params = self.module.params['cert'] + if cert_params: + self.pfsense_cert.run(cert_params) + certref = self.pfsense.get_certref(cert_params['name']) + print("Certref: %s", certref) + # Overwrite cert parameters with id of created certificate. + self.module.params['cert'] = certref + self.pfsense_user.run(self.module.params) + + def commit_changes(self): + """ apply changes and exit module """ + result = {} + result['stdout'] = '' + result['stderr'] = '' + result['changed'] = ( self.pfsense_cert.result['changed'] or self.pfsense_user.result['changed']) + if result['changed'] and not self.module.check_mode: + self.pfsense.write_config(descr='user change') + if self.pfsense_cert.result['changed']: + (dummy, stdout, stderr) = self.pfsense_cert._update() + result['stdout'] += stdout + result['stderr'] += stderr + if self.pfsense_user.result['changed']: + (dummy, stdout, stderr) = self.pfsense_user._update() + result['stdout'] += stdout + result['stderr'] += stderr + result['result_cert'] = { 'diff': self.pfsense_cert.result['diff'], 'commands': self.pfsense_cert.result['commands'] } + result['result_user'] = { 'diff': self.pfsense_cert.result['diff'], 'commands': self.pfsense_cert.result['commands'] } + self.module.exit_json(**result) + +def main(): + argument_spec = dict( name=dict(required=True, type='str'), state=dict(type='str', default='present', choices=['present', 'absent']), descr=dict(type='str'), @@ -97,269 +249,14 @@ groups=dict(type='list', elements='str'), priv=dict(type='list', elements='str'), authorizedkeys=dict(type='str'), -) - -USER_PHP_COMMAND_PREFIX = """ -require_once('auth.inc'); -init_config_arr(array('system', 'user')); -""" - -USER_PHP_COMMAND_SET = USER_PHP_COMMAND_PREFIX + """ -$a_user = &$config['system']['user']; -$userent = $a_user[{idx}]; -local_user_set($userent); -global $groupindex; -foreach ({mod_groups} as $groupname) {{ - $group = &$config['system']['group'][$groupindex[$groupname]]; - local_group_set($group); -}} -if (is_dir("/etc/inc/privhooks")) {{ - run_plugins("/etc/inc/privhooks"); -}} -""" - -# This runs after we remove the group from the config so we can't use $config -USER_PHP_COMMAND_DEL = USER_PHP_COMMAND_PREFIX + """ -$userent['name'] = '{name}'; -$userent['uid'] = {uid}; -global $groupindex; -foreach ({mod_groups} as $groupname) {{ - $group = &$config['system']['group'][$groupindex[$groupname]]; - local_group_set($group); -}} -local_user_del($userent); -""" - - -class PFSenseUserModule(PFSenseModuleBase): - """ module managing pfsense users """ - - @staticmethod - def get_argument_spec(): - """ return argument spec """ - return USER_ARGUMENT_SPEC - - def __init__(self, module, pfsense=None): - super(PFSenseUserModule, self).__init__(module, pfsense) - self.name = "pfsense_user" - self.root_elt = self.pfsense.get_element('system') - self.users = self.root_elt.findall('user') - self.groups = self.root_elt.findall('group') - self.mod_groups = [] - - ############################## - # params processing - # - def _validate_params(self): - """ do some extra checks on input parameters """ - params = self.params - if 'password' in params and params['password'] is not None: - password = params['password'] - if re.match(r'\$2[aby]\$', str(password)): - params['bcrypt-hash'] = password - else: - self.module.fail_json(msg='Password (%s) does not appear to be a bcrypt hash' % password) - del params['password'] - if 'name' in params and params['name'] is not None: - name = params['name'] - if not re.match(r'^[a-zA-Z0-9._-]{1,16}$', str(name)): - self.module.fail_json(msg='Name (%s) invalid, must be 16 characters or less and may only contain letters, numbers, and a period, hyphen, or underscore' % name) - - def _params_to_obj(self): - """ return a dict from module params """ - params = self.params - - obj = dict() - self.obj = obj - - obj['name'] = params['name'] - if params['state'] == 'present': - for option in ['authorizedkeys', 'descr', 'scope', 'uid', 'bcrypt-hash', 'groups', 'priv']: - if option in params and params[option] is not None: - obj[option] = params[option] - - # Allow authorizedkeys to be clear or base64 encoded - if 'authorizedkeys' in obj and 'ssh-' in obj['authorizedkeys']: - obj['authorizedkeys'] = base64.b64encode(obj['authorizedkeys'].encode()).decode() - - return obj - - ############################## - # XML processing - # - def _find_target(self): - result = self.root_elt.findall("user[name='{0}']".format(self.obj['name'])) - if len(result) == 1: - return result[0] - elif len(result) > 1: - self.module.fail_json(msg='Found multiple users for name {0}.'.format(self.obj['name'])) - else: - return None - - def _find_group(self, name): - result = self.root_elt.findall("group[name='{0}']".format(name)) - if len(result) == 1: - return result[0] - elif len(result) > 1: - self.module.fail_json(msg='Found multiple groups for name {0}.'.format(name)) - else: - return None - - def _find_groups_for_uid(self, uid): - groups = [] - for group_elt in self.root_elt.findall("group[member='{0}']".format(uid)): - groups.append(group_elt.find('name').text) - return groups - - def _find_this_user_index(self): - return self.users.index(self.target_elt) - - def _find_last_user_index(self): - return list(self.root_elt).index(self.users[len(self.users) - 1]) - - def _nextuid(self): - nextuid_elt = self.root_elt.find('nextuid') - nextuid = nextuid_elt.text - nextuid_elt.text = str(int(nextuid) + 1) - return nextuid - - def _format_diff_priv(self, priv): - if isinstance(priv, str): - return [priv] - else: - return priv - - def _create_target(self): - """ create the XML target_elt """ - return self.pfsense.new_element('user') - - def _copy_and_add_target(self): - """ populate the XML target_elt """ - obj = self.obj - if 'bcrypt-hash' not in obj: - self.module.fail_json(msg='Password is required when adding a user') - if 'uid' not in obj: - obj['uid'] = self._nextuid() - - self.diff['after'] = obj - self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self._update_groups() - self.root_elt.insert(self._find_last_user_index(), self.target_elt) - # Reset users list - self.users = self.root_elt.findall('user') - - def _copy_and_update_target(self): - """ update the XML target_elt """ - before = self.pfsense.element_to_dict(self.target_elt) - self.diff['before'] = before - if 'priv' in before: - before['priv'] = self._format_diff_priv(before['priv']) - changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - if 'priv' in self.diff['after']: - self.diff['after']['priv'] = self._format_diff_priv(self.diff['after']['priv']) - if self._update_groups(): - changed = True - - return (before, changed) - - def _update_groups(self): - user = self.obj - changed = False - - # Handle group member element - need uid set or retrieved above - if 'groups' in user: - uid = self.target_elt.find('uid').text - # Get current group membership - self.diff['before']['groups'] = self._find_groups_for_uid(uid) - - # Add user to groups if needed - for group in self.obj['groups']: - group_elt = self._find_group(group) - if group_elt is None: - self.module.fail_json(msg='Group (%s) does not exist' % group) - if len(group_elt.findall("[member='{0}']".format(uid))) == 0: - changed = True - self.mod_groups.append(group) - group_elt.append(self.pfsense.new_element('member', uid)) - - # Remove user from groups if needed - for group in self.diff['before']['groups']: - if group not in self.obj['groups']: - group_elt = self._find_group(group) - if group_elt is None: - self.module.fail_json(msg='Group (%s) does not exist' % group) - for member_elt in group_elt.findall('member'): - if member_elt.text == uid: - changed = True - self.mod_groups.append(group) - group_elt.remove(member_elt) - break - - # Groups are not stored in the user element - self.diff['after']['groups'] = user.pop('groups') - - # Decode keys for diff - for k in self.diff: - if 'authorizedkeys' in self.diff[k]: - self.diff[k]['authorizedkeys'] = base64.b64decode(self.diff[k]['authorizedkeys']) - - return changed - - ############################## - # Logging - # - def _get_obj_name(self): - """ return obj's name """ - return "'" + self.obj['name'] + "'" - - def _log_fields(self, before=None): - """ generate pseudo-CLI command fields parameters to create an obj """ - values = '' - if before is None: - values += self.format_cli_field(self.params, 'descr') - else: - values += self.format_updated_cli_field(self.obj, before, 'descr', add_comma=(values)) - return values - - ############################## - # run - # - def _update(self): - if self.params['state'] == 'present': - return self.pfsense.phpshell(USER_PHP_COMMAND_SET.format(idx=self._find_this_user_index(), mod_groups=self.mod_groups)) - else: - return self.pfsense.phpshell(USER_PHP_COMMAND_DEL.format(name=self.obj['name'], uid=self.obj['uid'], mod_groups=self.mod_groups)) - - def _pre_remove_target_elt(self): - self.diff['after'] = {} - if self.target_elt is not None: - self.diff['before'] = self.pfsense.element_to_dict(self.target_elt) - # Store uid for _update() - self.obj['uid'] = self.target_elt.find('uid').text - - # Get current group membership - self.diff['before']['groups'] = self._find_groups_for_uid(self.obj['uid']) - - # Remove user from groups if needed - for group in self.diff['before']['groups']: - group_elt = self._find_group(group) - if group_elt is None: - self.module.fail_json(msg='Group (%s) does not exist' % group) - for member_elt in group_elt.findall('member'): - if member_elt.text == self.obj['uid']: - self.mod_groups.append(group) - group_elt.remove(member_elt) - break - - -def main(): + cert=dict(type='dict', options=CERT_ARGUMENT_SPEC) + ) module = AnsibleModule( - argument_spec=USER_ARGUMENT_SPEC, + argument_spec=argument_spec, supports_check_mode=True) - - pfmodule = PFSenseUserModule(module) - pfmodule.run(module.params) + + pfmodule = PFSenseUserCertModule(module) + pfmodule.run() pfmodule.commit_changes() From 4bd0bb24532f4e34a8848a3f515b44c8c5b3b9be Mon Sep 17 00:00:00 2001 From: Ruben Smits Date: Tue, 16 Jul 2024 12:39:19 +0000 Subject: [PATCH 4/4] Fix result_user output --- plugins/modules/pfsense_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/pfsense_user.py b/plugins/modules/pfsense_user.py index a2946ee4..c4a4f040 100644 --- a/plugins/modules/pfsense_user.py +++ b/plugins/modules/pfsense_user.py @@ -235,7 +235,7 @@ def commit_changes(self): result['stdout'] += stdout result['stderr'] += stderr result['result_cert'] = { 'diff': self.pfsense_cert.result['diff'], 'commands': self.pfsense_cert.result['commands'] } - result['result_user'] = { 'diff': self.pfsense_cert.result['diff'], 'commands': self.pfsense_cert.result['commands'] } + result['result_user'] = { 'diff': self.pfsense_user.result['diff'], 'commands': self.pfsense_user.result['commands'] } self.module.exit_json(**result) def main():