From 10e1d077bbb67ce5b1e15288268a81ba58b1e891 Mon Sep 17 00:00:00 2001 From: Kevin Brooks Date: Thu, 15 Jan 2026 13:58:15 +0000 Subject: [PATCH 1/5] init --- README.md | 1 + plugins/modules/pfsense_saml.py | 248 ++++++++++++ .../modules/fixtures/pfsense_saml_config.xml | 360 ++++++++++++++++++ .../unit/plugins/modules/test_pfsense_saml.py | 86 +++++ 4 files changed, 695 insertions(+) create mode 100644 plugins/modules/pfsense_saml.py create mode 100644 tests/unit/plugins/modules/fixtures/pfsense_saml_config.xml create mode 100644 tests/unit/plugins/modules/test_pfsense_saml.py diff --git a/README.md b/README.md index 1b744677..c9858b79 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ These modules allow you to manage installed packages: * [pfsense_haproxy_backend](https://github.com/pfsensible/core/wiki/pfsense_haproxy_backend) for HAProxy backends * [pfsense_haproxy_backend_server](https://github.com/pfsensible/core/wiki/pfsense_haproxy_backend_server) for HAProxy backends servers +* [pfsense_saml](https://github.com/pfsensible/core/wiki/pfsense_saml) for SSO over SAML ## [Change Log](https://github.com/pfsensible/core/blob/master/CHANGELOG.rst) diff --git a/plugins/modules/pfsense_saml.py b/plugins/modules/pfsense_saml.py new file mode 100644 index 00000000..1a9f0ee6 --- /dev/null +++ b/plugins/modules/pfsense_saml.py @@ -0,0 +1,248 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Kevin Brooks +# 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 + +INSTALLATION = """ +- name: Set pfSense-pkg-saml2-auth url for pfSense version 2.8 + tags: + - setup + set_fact: + pfSense_saml2_pkg: https://github.com/pfrest/pfSense-pkg-saml2-auth/releases/latest/download/pfSense-2.8-pkg-saml2-auth.pkg + +- name: Add plugin pfSense-pkg-saml2-auth to repo + tags: + - setup + command: pkg add {{ pfSense_saml2_pkg }} + register: pkg_command + changed_when: not pkg_command.stdout is search("is already installed") +""" + +DOCUMENTATION = """ +--- +module: pfsense_saml +version_added: 0.8.0 +short_description: Manage pfSense SAML configuration +description: + - Manage pfSense-pkg-saml2-auth configuration +author: Kevin Brooks (@KevinB-rocks) +notes: +options: + enable: + description: State of authentication through SAML + default: true + type: bool + strip_username: + description: State of removal of @domain.example from emails in NameID + default: false + type: bool + debug_mode: + description: State of debug mode + default: false + type: bool + idp_metadata_url: + description: Metadata URL to IdP for automatic settings + default: "" + type: str + idp_entity_id: + description: Entity ID of the upstream IdP. + default: "" + type: str + idp_sign_on_url: + description: Sign-on ID of the upstream IdP. + default: "" + type: str + idp_groups_attribute: + description: Name of groups attribute returned in the SAML assertion for groups based privilege mapping. + default: "" + type: str + idp_x509_cert: + description: x509 cert provided by the IdP. + default: "" + type: str + sp_base_url: + description: Base URL of pfSense. + required: true + type: str + custom_conf: + description: JSON-config extending the explicitly defined fields. Must comply with OneLogin PHP-SAML format. Use at your own risk. + default: "" + type: str +""" + +EXAMPLES = """ +- name: Modify SAML config + pfsense_saml: + enable: true + idp_metadata_url: http://keycloak.local/realms/master/protocol/saml/descriptor + sp_base_url: https://pfSense.local +""" + +RETURN = """ + +""" + +import re +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase + + +SAML_ARGUMENT_SPEC = dict( + enable=dict(default=True, type='bool'), + strip_username=dict(default=False, type='bool'), + debug_mode=dict(default=False, type='bool'), + idp_metadata_url=dict(default="", type='str'), + idp_entity_id=dict(default="", type='str'), + idp_sign_on_url=dict(default="", type='str'), + idp_groups_attribute=dict(default="", type='str'), + idp_x509_cert=dict(default="", type='str'), + sp_base_url=dict(required=True, type='str'), + custom_conf=dict(default="", type='str'), +) + +# URL as defined in RFC 2396 +URL_REGEX = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?" +IDP_ENTITY_REGEX = "[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+" + +# Alternatively... +# URL_REGEX = "^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$" + +class PFSenseSAMLModule(PFSenseModuleBase): + """ module managing saml config """ + + def __init__(self, module, pfsense=None): + super(PFSenseSAMLModule, self).__init__(module, pfsense, key="sp_base_url") + + self.name = "saml2-auth" + self.root_elt = self._find_target() + self.obj = dict() + + + ############################## + # params processing + # + def _validate_set_if_idp_metadata_unset(self, params, key): + if params[key] == "": + if params["idp_metadata_url"] == "": + self.module.fail_json(msg="{0} is required when idp_metadata_url is unset.".format(key)) + + def _validate_url(self, params, key): + if not re.fullmatch(URL_REGEX, params[key]): + self.module.fail_json(msg="{0} is not a valid URL".format(key)) + + def _validate_params(self): + """ do some extra checks on input parameters """ + + params = self.params + + self._validate_url(params, 'sp_base_url') + + if params['idp_metadata_url'] != "": + self._validate_url(params, 'idp_metadata_url') + + self._validate_set_if_idp_metadata_unset(params, 'idp_entity_idp') + if params['idp_entity_idp'] != "": + if len(params['idp_entity_idp']) > 1024: + self.module.fail_json(msg="idp_entity_idp must be less than 1024 characters long.") + if not re.fullmatch(IDP_ENTITY_REGEX, params['idp_entity_idp']): + self.module.fail_json(msg="idp_entity_idp contains invalid characters.") + + self._validate_set_if_idp_metadata_unset(params, 'idp_sign_on_url') + if params['idp_sign_on_url'] != "": + self._validate_url(params, 'idp_sign_on_url') + + self._validate_set_if_idp_metadata_unset(params, 'idp_x509_cert') + if params['idp_x509_cert'] != "": + if not (params['idp_x509_cert'].startswith('-----BEGIN CERTIFICATE-----') and params['idp_x509_cert'].endswith('-----END CERTIFICATE-----')): + self.module.fail_json(msg="idp_x509_cert is missing BEGIN and/or END tags.") + + if params['custom_conf'] != "": + try: + json.loads(params['custom_conf']) + except json.decoder.JSONDecodeError: + self.module.fail_json(msg="custom_conf is not valid JSON") + + + ############################## + # XML processing + # + def _find_target(self): + installed_pkgs_elt = self.pfsense.get_element('installedpackages') + pkgs_elts = installed_pkgs_elt.findall('package') if installed_pkgs_elt is not None else None + + for elt in pkgs_elts: + pkg_name = elt.find('internal_name') + if pkg_name is not None and pkg_name.text == self.name: + conf_elt = elt.find('conf') + if conf_elt is not None: + return conf_elt + + return self.module.fail_json(msg='Unable to find XML configuration entry. Are you sure SAML2 package is installed?') + + 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) + if self._remove_deleted_params(): + changed = True + self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) + + return (before, changed) + + + ############################## + # run + # + + + ############################## + # logging + # + 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.obj, 'enable', fvalue=self.fvalue_bool) + values += self.format_cli_field(self.obj, 'strip_username', fvalue=self.fvalue_bool) + values += self.format_cli_field(self.obj, 'debug_mode', fvalue=self.fvalue_bool) + values += self.format_cli_field(self.obj, 'idp_metadata_url') + values += self.format_cli_field(self.obj, 'idp_entity_id') + values += self.format_cli_field(self.obj, 'idp_sign_on_url') + values += self.format_cli_field(self.obj, 'idp_groups_attribute') + values += self.format_cli_field(self.obj, 'idp_x509_cert') + values += self.format_cli_field(self.obj, 'sp_base_url') + values += self.format_cli_field(self.obj, 'custom_config') + else: + values += self.format_updated_cli_field(self.obj, before, 'enable', fvalue=self.fvalue_bool) + values += self.format_updated_cli_field(self.obj, before, 'strip_username', fvalue=self.fvalue_bool) + values += self.format_updated_cli_field(self.obj, before, 'debug_mode', fvalue=self.fvalue_bool) + values += self.format_updated_cli_field(self.obj, before, 'idp_metadata_url') + values += self.format_updated_cli_field(self.obj, before, 'idp_entity_id') + values += self.format_updated_cli_field(self.obj, before, 'idp_sign_on_url') + values += self.format_updated_cli_field(self.obj, before, 'idp_groups_attribute') + values += self.format_updated_cli_field(self.obj, before, 'idp_x509_cert') + values += self.format_updated_cli_field(self.obj, before, 'sp_base_url') + values += self.format_updated_cli_field(self.obj, before, 'custom_config') + return values + + +def main(): + module = AnsibleModule( + argument_spec=SAML_ARGUMENT_SPEC, + supports_check_mode=True) + + pfmodule = PFSenseSAMLModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() diff --git a/tests/unit/plugins/modules/fixtures/pfsense_saml_config.xml b/tests/unit/plugins/modules/fixtures/pfsense_saml_config.xml new file mode 100644 index 00000000..862e723b --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/pfsense_saml_config.xml @@ -0,0 +1,360 @@ + + + 19.1 + + + normal + pfSense + localdomain + + + + all + + system + 1998 + 0 + + + admins + + system + 1999 + 0 + page-all + + + admin + + system + admins + $2b$10$13u6qwCOwODv34GyCMgdWub6oQF3RX0rG7c3d3X4JvzuEmAXLYDd2 + 0 + user-shell-access + + + testdel + + all + 2000 + user + $2b$12$D2jkq4Iut3ODUBN0BCrDk.bV3J5N.MrY5YEnGvTXwxeNBkyxjbbtW + page-dashboard-all + + 2001 + 2000 + 0.pfsense.pool.ntp.org + + https + + 5cf09a99bf5fe + + yes + + + + 400000 + hadp + hadp + hadp + + monthly + + + + enabled + + + + + + re0 + + dhcp + dhcp6 + + + + + + + + + 0 + + + + re1 + 192.168.100.2 + 24 + + + + + wan + 0 + + + + + + + + + 192.168.1.100 + 192.168.1.199 + + + + + + + ::1000 + ::2000 + + assist + medium + + + + + + public + + + + + + + + 1 + + + + automatic + + + + + pass + inet + + lan + 0100000101 + + lan + + + + + + + pass + inet6 + + lan + 0100000102 + + lan + + + + + + + + + + + + + 1,31 + 0-5 + * + * + * + root + /usr/bin/nice -n20 adjkerntz -a + + + 1 + 3 + 1 + * + * + root + /usr/bin/nice -n20 /etc/rc.update_bogons.sh + + + 1 + 1 + * + * + * + root + /usr/bin/nice -n20 /etc/rc.dyndns.update + + + */60 + * + * + * + * + root + /usr/bin/nice -n20 /usr/local/sbin/expiretable -v -t 3600 virusprot + + + 30 + 12 + * + * + * + root + /usr/bin/nice -n20 /etc/rc.update_urltables + + + 1 + 0 + * + * + * + root + /usr/bin/nice -n20 /etc/rc.update_pkg_metadata + + + + + + + + + ICMP + icmp + + + + + TCP + tcp + + + + + HTTP + http + + + / + + 200 + + + + HTTPS + https + + + / + + 200 + + + + SMTP + send + + + + 220 * + + + + + system_information:col1:show,netgate_services_and_support:col2:show,interfaces:col2:show + 10 + + + + + + + + + + + + + + + + + (system) + + + 5cf09a99bf5fe + + server + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVlakNDQTJLZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREJhTVRnd05nWURWUVFLRXk5d1psTmwKYm5ObElIZGxZa052Ym1acFozVnlZWFJ2Y2lCVFpXeG1MVk5wWjI1bFpDQkRaWEowYVdacFkyRjBaVEVlTUJ3RwpBMVVFQXhNVmNHWlRaVzV6WlMwMVkyWXdPV0U1T1dKbU5XWmxNQjRYRFRFNU1EVXpNVEF6TURnd09Wb1hEVEkwCk1URXlNREF6TURnd09Wb3dXakU0TURZR0ExVUVDaE12Y0daVFpXNXpaU0IzWldKRGIyNW1hV2QxY21GMGIzSWcKVTJWc1ppMVRhV2R1WldRZ1EyVnlkR2xtYVdOaGRHVXhIakFjQmdOVkJBTVRGWEJtVTJWdWMyVXROV05tTURsaApPVGxpWmpWbVpUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUt6QW91N1I0TW5VCjlWUlhQYkZyK2NRVXhtdGNKdDNpbjAvbFppRGh6ZjNVV0hnTHVRdHBoL2NMMUZIdlFwdzZzazlFRzY5RjREdTUKL2IweElnS1B2YlVFcVpJeit0aEkvZ09LZ29GRTJ1dERxcW8xSnh6SzliVzdVdHVnZWlxUnEyT1hLaGh5ZUNKcQo5OEEwL3l4VitqNkJDbmpNeVVGS2krQjJpcVNnSG1JQVkySHVRcklsb3ZwMGZuTUpkeWRjOGNzVHh0aUJUODh4Cko3K0locDJrSGNDcUtIRGg0bjVYOEdUek54YmxreTJERXBkYkdsUWcyRE1JQm9hTmRnMXpVdUx1cnV2LzVoWkIKQ2JyWjBRNXpRaWVGdU13ZnMrNEdPVW9kS201ZFJJM3YwT0NRd05JUnZVZVpDTUFhTFJVNVdvT0hzMUUzb3RjaAorNlZ3RnVaNndrOENBd0VBQWFPQ0FVa3dnZ0ZGTUFrR0ExVWRFd1FDTUFBd0VRWUpZSVpJQVliNFFnRUJCQVFECkFnWkFNQXNHQTFVZER3UUVBd0lGb0RBekJnbGdoa2dCaHZoQ0FRMEVKaFlrVDNCbGJsTlRUQ0JIWlc1bGNtRjAKWldRZ1UyVnlkbVZ5SUVObGNuUnBabWxqWVhSbE1CMEdBMVVkRGdRV0JCUUd2bSs0NWxFUW5oSDVOaHhFRk9YWQpxWVZ1ZnpDQmdnWURWUjBqQkhzd2VZQVVCcjV2dU9aUkVKNFIrVFljUkJUbDJLbUZibitoWHFSY01Gb3hPREEyCkJnTlZCQW9UTDNCbVUyVnVjMlVnZDJWaVEyOXVabWxuZFhKaGRHOXlJRk5sYkdZdFUybG5ibVZrSUVObGNuUnAKWm1sallYUmxNUjR3SEFZRFZRUURFeFZ3WmxObGJuTmxMVFZqWmpBNVlUazVZbVkxWm1XQ0FRQXdIUVlEVlIwbApCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQ0FJQ01DQUdBMVVkRVFRWk1CZUNGWEJtVTJWdWMyVXROV05tCk1EbGhPVGxpWmpWbVpUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFiTjJPL2hwcjU5bC9XclJIQlhvTjJDSFYKQi9ra0VwUVVuRXhrSzVNUkJGYTV5ODVLSk5Eb09HL0R5UlVNRFR4VENkVnQrdlpQS0ZveUJXRFdDdGt3YTJScgo1Z1BDb21TUk5mUWhyV1ZtYkhSd0VNTyt6OXkxS1AzU3JhZ0tIeVdTM3lFY25wU1NRY1B1M3hhaERDNERtclJzCjMvNytBMTk2SEJxRjlBc25oUjFDZWpBcm4rSFR5dUdOQk5udGtqUktPN01NcDVxaHlZbkV3SVk5SVQ0TzFIeHYKOEtFTTdBUGRmRS93VUFvZGxET0F5RFRIRFpid2Z1eCtoOURRTGNBRE1sRi9IdzF0Y2UvcHJLek9LT3h6YmdlQQpWZ2pQVmxNbVJqQ2lJa2JNL3BPeWNObENDTzAxczQzbFJzcU5VRzFaLzBjakFzeEw0c0ltUlErQkI2dXdDUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2d0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktrd2dnU2xBZ0VBQW9JQkFRQ3N3S0x1MGVESjFQVlUKVnoyeGEvbkVGTVpyWENiZDRwOVA1V1lnNGMzOTFGaDRDN2tMYVlmM0M5UlI3MEtjT3JKUFJCdXZSZUE3dWYyOQpNU0lDajcyMUJLbVNNL3JZU1A0RGlvS0JSTnJyUTZxcU5TY2N5dlcxdTFMYm9Ib3FrYXRqbHlvWWNuZ2lhdmZBCk5QOHNWZm8rZ1FwNHpNbEJTb3ZnZG9xa29CNWlBR05oN2tLeUphTDZkSDV6Q1hjblhQSExFOGJZZ1UvUE1TZS8KaUlhZHBCM0FxaWh3NGVKK1YvQms4emNXNVpNdGd4S1hXeHBVSU5nekNBYUdqWFlOYzFMaTdxN3IvK1lXUVFtNgoyZEVPYzBJbmhiak1IN1B1QmpsS0hTcHVYVVNONzlEZ2tNRFNFYjFIbVFqQUdpMFZPVnFEaDdOUk42TFhJZnVsCmNCYm1lc0pQQWdNQkFBRUNnZ0VCQUtzVzFjY0VZVVpDL1AyY3NXTG45eU4xRjlYNEhCNGdkWHRoVERaQXJBdzUKbzZ5d240Rm44TnFCQXJScTYyTmkxbm1la0hTVUZiSFJVRFZ4VmFlSHlIQmd2N2dtZHNhQjgrQjU2eW92a1VqegphVERORjRGeW1NcDFUV2hxbE5OWUZZKzZoRnhWOGhqVUs2NVdUbW9RZEpnMm9MSm16dU0wK3pkQTc2cC9VZGZuCnZkSzZVbDFWSi9WcEhFaEhSS1duSk9uTE1nRU1KS1oxT0NjRTFXOExHU08zY3QvM0VkWi9IRWU2TkZqOXArZTgKTmplTWNpSVduaTE1aTM3ai9LL1FBcFJQcUh5cDBhdUUzR0ZQckhPWjdsUHhlYlZjTExwSEFINWNuc0VyWkJXTApnSWlQcGkxTytrL01wb0h0RklHK25aSXpVdjFzeldrVjg4TjhtcHMrd2FFQ2dZRUExLzZXYVJlVk1GbnFPd0YvCm9UbUJNVTl0TnQ5U1NacTF2RlpNcFVkZk5wMmY0c0ZZNFE2U1gzeFptUEFHQkMyMDBWQWoxbmVlNlBOUTBuclgKSVIvSFJjelNpQXA3R3ZqakRjcGdQaWc2SHdpZHVVM1lvelhKSFdzaHFIVVMrTlo4b0VmUFBld0tvK3hTMmwxdwo2NG5ZR09RcHc0ZTFibURqbEJYOC9CcTBlcGtDZ1lFQXpMKzVDN1EwQmVKZExzOWZOQmFvWFhVRUlHNXhjZmNTCmJHbDFjbzNDN3h2WCtWQXNZYkVWV0hiYk40TVdmVWpSMGhuYStRM2dyaVZvdmZ5K0FqNWovMkFMM2dIY0U1UmMKU2Q0VVA4aG5YdmhST3M5cTNMblZUYVFaZktKY09KTm82VGtMeXZacVJjdmNTWHJDK21tQXc5a0c4SUNFeWZ4VApBV2lmTHk0VzNTY0NnWUVBbWtGRGdnSkpsYUpoV1lxVWI3djF2QldSVmVMZmpabGp1UUdZODJDcGF3UGZMNzROCmo0MHNrK3ptd0FhTEJXanUvWjFTT3RSck5NcXdLZUY0eWpzN3dXbXA0V1k2ek9SNm8xcW9xVHRwWnNoc2UrNVEKalI3WVpwNGdCNEswN2VtZ1Q0ZDVSaXZRM1lqbEV2WXdzc1piQWt2UVY4Z1BscWl6WHdybEJkYThsZUVDZ1lBbgpSVmFlc2crUVdWeDZEL2c0cTJmYmxRZ1htRmRWL29lZ0Y1SVpTS3RzNVRCRmQyVXJ6NlZDZEhtVGFpYzBISFZ5CkVOZDVFWHBZcklBc2dIK0pPcUkvWnhLZm9FZXYwYkxwMEJpZUt6ZjRkVFJQVFYwM3ZNVDJ3VlRLSFBJSFArN04KWE0yd1BoY2dEL3ZPZENkVmxFcklSYVlaRnUxaE9HNUxSTi9UVXNtNzNRS0JnUUNVeFFmY2RyZnZwL0ZSanlkUQpPaUJWWm1JOGFsVXg5SklhZlBjaU5WWlZBZFJnamVEVlVFUTljTklxcDlnd0F4cVVVQ1U3OW9wNWtwbzJoMkZaClBIaFRZaHlpanRxQ09hNWxHdnZTYjg4aWE1Z2xleE1zL2M1bmc0d3RlWHVpSWtqd0lTUmhMWGxCOTU3a2hvSEkKczFnbEp2YTM4MEM0Yk0rcFJhbWdqVlY4blE9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg== + + + + + sudo + https://docs.netgate.com/pfsense/en/latest/packages/sudo.html + + http://www.sudo.ws/ + 0.3_10 + sudo.xml + /usr/local/pkg/sudo.inc + + + SAML2 + saml2-auth + + https://github.com/pfrest/pfSense-pkg-saml2-auth + System + %%PKGVERSION%% + saml2.xml + jaredhendrickson13@gmail.com + + yes + + + http://keycloak.local/realms/master/protocol/saml/descriptor + + + memberOf + + https://pfSense.local + + + + SAML2 + saml2 + saml2.log + + + + + + + user:root + ALL + + none + + + + sudo +
System
+ /pkg_edit.php?xml=sudo.xml +
+ + SAML2 + A SAML2 authentication extension for the pfSense UI +
System
+ /system_saml2_settings.php +
+
+
diff --git a/tests/unit/plugins/modules/test_pfsense_saml.py b/tests/unit/plugins/modules/test_pfsense_saml.py new file mode 100644 index 00000000..f52a8d3d --- /dev/null +++ b/tests/unit/plugins/modules/test_pfsense_saml.py @@ -0,0 +1,86 @@ +# 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 pytest +import sys + +if sys.version_info < (2, 7): + pytestmark = pytest.mark.skip("pfSense Ansible modules require Python >= 2.7") + +from ansible_collections.pfsensible.core.plugins.modules import pfsense_saml +from .pfsense_module import TestPFSenseModule + + +class TestPFSenseSAMLModule(TestPFSenseModule): + + module = pfsense_saml + + def __init__(self, *args, **kwargs): + super(TestPFSenseSAMLModule, self).__init__(*args, **kwargs) + self.config_file = 'pfsense_saml_config.xml' + self.pfmodule = pfsense_saml.PFSenseSAMLModule + + @staticmethod + def runTest(): + """ dummy function needed to instantiate this test module from another in python 2.7 """ + pass + + def get_target_elt(self, obj, absent=False, module_result=None): + """ return target elt from XML """ + installed_pkgs_elt = self.assert_find_xml_elt(self.xml_result, 'installedpackages') + pkgs_elts = installed_pkgs_elt.findall('package') if installed_pkgs_elt is not None else None + + for elt in pkgs_elts: + pkg_name = elt.find('internal_name') + if pkg_name is not None and pkg_name.text == "saml2-auth": + conf_elt = elt.find('conf') + if conf_elt is not None: + return conf_elt + + return None + + def check_target_elt(self, obj, target_elt): + """ check XML definition of target elt """ + + self.check_param_equal(obj, target_elt, 'enable') + self.check_param_equal(obj, target_elt, 'strip_username') + self.check_param_equal(obj, target_elt, 'debug_mode') + + self.check_param_equal(obj, target_elt, 'idp_metadata_url') + self.check_param_equal(obj, target_elt, 'idp_entity_id', default='') + self.check_param_equal(obj, target_elt, 'idp_sign_on_url', default='') + self.check_param_equal(obj, target_elt, 'idp_groups_attribute', default='') + self.check_param_equal(obj, target_elt, 'idp_x509_cert', default='') + + self.check_param_equal(obj, target_elt, 'sp_base_url') + + self.check_param_equal(obj, target_elt, 'custom_conf', default='') + + + ############## + # tests + # + + # def test_conf_not_found(self): + # """ TODO """ + + # def test_updated_settings(self): + # """ TODO """ + + def test_x509_cert_update_noop(self): + """ test not applying invalid x509 cert """ + obj = dict(sp_base_url="https://pfSense.local", idp_x509_cert="NOT_A_VALID_CERT") + self.do_module_test(obj, command="update saml", changed=False) + + # def test_invalid_url_update_noop(self): + # """ test not appling an invalid url """ + # obj = dict(sp_base_url="https://pfSense.local") + # self.do_module_test(obj, command="update saml", changed=False) + + # def test_invalid_json_update_noop(self): + # """ test not appling an invalid url """ + # obj = dict(sp_base_url="https://pfSense.local", custom_conf="invalid_json") + # self.do_module_test(obj, command="update saml", changed=False) + From 77e78453dafa7914c6abc588ab8614620c8dab85 Mon Sep 17 00:00:00 2001 From: Kevin Brooks Date: Thu, 15 Jan 2026 15:28:40 +0000 Subject: [PATCH 2/5] docs: add temp link to docs --- plugins/modules/pfsense_saml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/modules/pfsense_saml.py b/plugins/modules/pfsense_saml.py index 1a9f0ee6..33fbf57b 100644 --- a/plugins/modules/pfsense_saml.py +++ b/plugins/modules/pfsense_saml.py @@ -92,6 +92,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase +# Check this https://github.com/pfsensible/core/wiki/PFSenseModuleBase-Template SAML_ARGUMENT_SPEC = dict( enable=dict(default=True, type='bool'), @@ -113,6 +114,7 @@ # Alternatively... # URL_REGEX = "^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$" + class PFSenseSAMLModule(PFSenseModuleBase): """ module managing saml config """ From 861c46e196abdaa8ccb23cc2b1918f14937b5046 Mon Sep 17 00:00:00 2001 From: Kevin Brooks Date: Fri, 16 Jan 2026 16:42:44 +0000 Subject: [PATCH 3/5] fix: various changes, and added tests --- plugins/modules/pfsense_saml.py | 193 +++++++++--------- .../modules/fixtures/pfsense_saml_config.xml | 4 +- .../unit/plugins/modules/test_pfsense_saml.py | 91 ++++++--- 3 files changed, 170 insertions(+), 118 deletions(-) diff --git a/plugins/modules/pfsense_saml.py b/plugins/modules/pfsense_saml.py index 33fbf57b..563eb821 100644 --- a/plugins/modules/pfsense_saml.py +++ b/plugins/modules/pfsense_saml.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 INSTALLATION = """ @@ -78,7 +79,7 @@ - name: Modify SAML config pfsense_saml: enable: true - idp_metadata_url: http://keycloak.local/realms/master/protocol/saml/descriptor + idp_metadata_url: https://keycloak.local/realms/master/protocol/saml/descriptor sp_base_url: https://pfSense.local """ @@ -88,163 +89,173 @@ import re import json +from urllib.parse import urlparse from ansible.module_utils.basic import AnsibleModule from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase -# Check this https://github.com/pfsensible/core/wiki/PFSenseModuleBase-Template - SAML_ARGUMENT_SPEC = dict( - enable=dict(default=True, type='bool'), - strip_username=dict(default=False, type='bool'), - debug_mode=dict(default=False, type='bool'), - idp_metadata_url=dict(default="", type='str'), - idp_entity_id=dict(default="", type='str'), - idp_sign_on_url=dict(default="", type='str'), - idp_groups_attribute=dict(default="", type='str'), - idp_x509_cert=dict(default="", type='str'), - sp_base_url=dict(required=True, type='str'), - custom_conf=dict(default="", type='str'), + enable=dict(default=True, type="bool"), + strip_username=dict(default=False, type="bool"), + debug_mode=dict(default=False, type="bool"), + idp_metadata_url=dict(default="", type="str"), + idp_entity_id=dict(default="", type="str"), + idp_sign_on_url=dict(default="", type="str"), + idp_groups_attribute=dict(default="", type="str"), + idp_x509_cert=dict(default="", type="str"), + sp_base_url=dict(required=True, type="str"), + custom_conf=dict(default="", type="str"), ) -# URL as defined in RFC 2396 -URL_REGEX = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?" -IDP_ENTITY_REGEX = "[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+" - -# Alternatively... -# URL_REGEX = "^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$" - +IDP_ENTITY_REGEX = r"[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]+" class PFSenseSAMLModule(PFSenseModuleBase): """ module managing saml config """ + ############################## + # unit tests + # + # Must be class method for unit test usage + @staticmethod + def get_argument_spec(): + """return argument spec""" + return SAML_ARGUMENT_SPEC + def __init__(self, module, pfsense=None): super(PFSenseSAMLModule, self).__init__(module, pfsense, key="sp_base_url") - + self.name = "saml2-auth" self.root_elt = self._find_target() self.obj = dict() - ############################## # params processing # def _validate_set_if_idp_metadata_unset(self, params, key): if params[key] == "": - if params["idp_metadata_url"] == "": - self.module.fail_json(msg="{0} is required when idp_metadata_url is unset.".format(key)) + if params["idp_metadata_url"] == "": + self.module.fail_json(msg="{0} is required when idp_metadata_url is unset".format(key)) def _validate_url(self, params, key): - if not re.fullmatch(URL_REGEX, params[key]): - self.module.fail_json(msg="{0} is not a valid URL".format(key)) + try: + url = urlparse(params[key]) + if not all([url.scheme, url.netloc]): + raise Exception + except Exception: + self.module.fail_json(msg="{0} is not a valid URL".format(key)) def _validate_params(self): - """ do some extra checks on input parameters """ - - params = self.params + """do some extra checks on input parameters""" - self._validate_url(params, 'sp_base_url') + params = self.params - if params['idp_metadata_url'] != "": - self._validate_url(params, 'idp_metadata_url') + self._validate_url(params, "sp_base_url") - self._validate_set_if_idp_metadata_unset(params, 'idp_entity_idp') - if params['idp_entity_idp'] != "": - if len(params['idp_entity_idp']) > 1024: - self.module.fail_json(msg="idp_entity_idp must be less than 1024 characters long.") - if not re.fullmatch(IDP_ENTITY_REGEX, params['idp_entity_idp']): - self.module.fail_json(msg="idp_entity_idp contains invalid characters.") + if params["idp_metadata_url"] != "": + self._validate_url(params, "idp_metadata_url") - self._validate_set_if_idp_metadata_unset(params, 'idp_sign_on_url') - if params['idp_sign_on_url'] != "": - self._validate_url(params, 'idp_sign_on_url') + self._validate_set_if_idp_metadata_unset(params, "idp_entity_id") + if params["idp_entity_id"] != "": + if len(params["idp_entity_id"]) > 1024: + self.module.fail_json(msg="idp_entity_id must be less than 1025 characters long") + if not re.fullmatch(IDP_ENTITY_REGEX, params["idp_entity_id"]): + self.module.fail_json(msg="idp_entity_id contains invalid characters") - self._validate_set_if_idp_metadata_unset(params, 'idp_x509_cert') - if params['idp_x509_cert'] != "": - if not (params['idp_x509_cert'].startswith('-----BEGIN CERTIFICATE-----') and params['idp_x509_cert'].endswith('-----END CERTIFICATE-----')): - self.module.fail_json(msg="idp_x509_cert is missing BEGIN and/or END tags.") + self._validate_set_if_idp_metadata_unset(params, "idp_sign_on_url") + if params["idp_sign_on_url"] != "": + self._validate_url(params, "idp_sign_on_url") - if params['custom_conf'] != "": - try: - json.loads(params['custom_conf']) - except json.decoder.JSONDecodeError: - self.module.fail_json(msg="custom_conf is not valid JSON") + self._validate_set_if_idp_metadata_unset(params, "idp_x509_cert") + if params["idp_x509_cert"] != "": + if not (params["idp_x509_cert"].startswith("-----BEGIN CERTIFICATE-----") and params["idp_x509_cert"].endswith("-----END CERTIFICATE-----")): + self.module.fail_json(msg="idp_x509_cert is missing BEGIN and/or END tags") + if params["custom_conf"] != "": + try: + json.loads(params["custom_conf"]) + except json.decoder.JSONDecodeError: + self.module.fail_json(msg="custom_conf is not valid JSON") ############################## # XML processing # def _find_target(self): - installed_pkgs_elt = self.pfsense.get_element('installedpackages') - pkgs_elts = installed_pkgs_elt.findall('package') if installed_pkgs_elt is not None else None + installed_pkgs_elt = self.pfsense.get_element("installedpackages") + pkgs_elts = installed_pkgs_elt.findall("package") if installed_pkgs_elt is not None else None for elt in pkgs_elts: - pkg_name = elt.find('internal_name') + pkg_name = elt.find("internal_name") if pkg_name is not None and pkg_name.text == self.name: - conf_elt = elt.find('conf') + conf_elt = elt.find("conf") if conf_elt is not None: return conf_elt - - return self.module.fail_json(msg='Unable to find XML configuration entry. Are you sure SAML2 package is installed?') + + return self.module.fail_json(msg="Unable to find XML configuration entry. Are you sure SAML2 package is installed?") def _copy_and_update_target(self): """ update the XML target_elt """ - before = self.pfsense.element_to_dict(self.target_elt) - self.diff['before'] = before + self.diff["before"] = self.pfsense.element_to_dict(self.target_elt) + self.diff["after"] = self.pfsense.element_to_dict(self.target_elt) + changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) - if self._remove_deleted_params(): - changed = True - self.diff['after'] = self.pfsense.element_to_dict(self.target_elt) - - return (before, changed) - + + return (self.diff["before"], changed) + + # def _remove_deleted_params(self): + # """" todo """ + # return False + + # # TODO: Does not work for ignore + # def _get_params_to_remove(self): + # """ returns the list of params to remove if they are not set """ + # return [] ############################## # run # - ############################## # logging # 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.obj, 'enable', fvalue=self.fvalue_bool) - values += self.format_cli_field(self.obj, 'strip_username', fvalue=self.fvalue_bool) - values += self.format_cli_field(self.obj, 'debug_mode', fvalue=self.fvalue_bool) - values += self.format_cli_field(self.obj, 'idp_metadata_url') - values += self.format_cli_field(self.obj, 'idp_entity_id') - values += self.format_cli_field(self.obj, 'idp_sign_on_url') - values += self.format_cli_field(self.obj, 'idp_groups_attribute') - values += self.format_cli_field(self.obj, 'idp_x509_cert') - values += self.format_cli_field(self.obj, 'sp_base_url') - values += self.format_cli_field(self.obj, 'custom_config') + """ generate pseudo-CLI command fields parameters to create an obj """ + values = "" + + if before is None: + values += self.format_cli_field(self.obj, "enable", fvalue=self.fvalue_bool, none_value='') + values += self.format_cli_field(self.obj, "strip_username", fvalue=self.fvalue_bool, none_value='') + values += self.format_cli_field(self.obj, "debug_mode", fvalue=self.fvalue_bool, none_value='') + values += self.format_cli_field(self.obj, "idp_metadata_url") + values += self.format_cli_field(self.obj, "idp_entity_id") + values += self.format_cli_field(self.obj, "idp_sign_on_url") + values += self.format_cli_field(self.obj, "idp_groups_attribute") + values += self.format_cli_field(self.obj, "idp_x509_cert") + values += self.format_cli_field(self.obj, "sp_base_url") + values += self.format_cli_field(self.obj, "custom_config") else: - values += self.format_updated_cli_field(self.obj, before, 'enable', fvalue=self.fvalue_bool) - values += self.format_updated_cli_field(self.obj, before, 'strip_username', fvalue=self.fvalue_bool) - values += self.format_updated_cli_field(self.obj, before, 'debug_mode', fvalue=self.fvalue_bool) - values += self.format_updated_cli_field(self.obj, before, 'idp_metadata_url') - values += self.format_updated_cli_field(self.obj, before, 'idp_entity_id') - values += self.format_updated_cli_field(self.obj, before, 'idp_sign_on_url') - values += self.format_updated_cli_field(self.obj, before, 'idp_groups_attribute') - values += self.format_updated_cli_field(self.obj, before, 'idp_x509_cert') - values += self.format_updated_cli_field(self.obj, before, 'sp_base_url') - values += self.format_updated_cli_field(self.obj, before, 'custom_config') + values += self.format_updated_cli_field(self.obj, before, "enable", add_comma=(values), fvalue=self.fvalue_bool, none_value='') + values += self.format_updated_cli_field(self.obj, before, "strip_username", add_comma=(values), fvalue=self.fvalue_bool, none_value='') + values += self.format_updated_cli_field(self.obj, before, "debug_mode", add_comma=(values), fvalue=self.fvalue_bool, none_value='') + values += self.format_updated_cli_field(self.obj, before, "idp_metadata_url", add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, "idp_entity_id", add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, "idp_sign_on_url", add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, "idp_groups_attribute", add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, "idp_x509_cert", add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, "sp_base_url", add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, "custom_config", add_comma=(values)) return values - def main(): module = AnsibleModule( argument_spec=SAML_ARGUMENT_SPEC, - supports_check_mode=True) + supports_check_mode=True, + ) pfmodule = PFSenseSAMLModule(module) pfmodule.run(module.params) pfmodule.commit_changes() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tests/unit/plugins/modules/fixtures/pfsense_saml_config.xml b/tests/unit/plugins/modules/fixtures/pfsense_saml_config.xml index 862e723b..0beeedd7 100644 --- a/tests/unit/plugins/modules/fixtures/pfsense_saml_config.xml +++ b/tests/unit/plugins/modules/fixtures/pfsense_saml_config.xml @@ -321,10 +321,10 @@ yes - http://keycloak.local/realms/master/protocol/saml/descriptor + https://keycloak.local/realms/master/protocol/saml/descriptor - memberOf + https://pfSense.local diff --git a/tests/unit/plugins/modules/test_pfsense_saml.py b/tests/unit/plugins/modules/test_pfsense_saml.py index f52a8d3d..82c1c4ba 100644 --- a/tests/unit/plugins/modules/test_pfsense_saml.py +++ b/tests/unit/plugins/modules/test_pfsense_saml.py @@ -1,3 +1,4 @@ +# Copyright: (c) 2026, Kevin Brooks # 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) @@ -12,6 +13,11 @@ from ansible_collections.pfsensible.core.plugins.modules import pfsense_saml from .pfsense_module import TestPFSenseModule +CURRENT_CONFIG = dict( + enable=True, + sp_base_url="https://pfSense.local", + idp_metadata_url="https://keycloak.local/realms/master/protocol/saml/descriptor", +) class TestPFSenseSAMLModule(TestPFSenseModule): @@ -44,43 +50,78 @@ def get_target_elt(self, obj, absent=False, module_result=None): def check_target_elt(self, obj, target_elt): """ check XML definition of target elt """ - self.check_param_equal(obj, target_elt, 'enable') - self.check_param_equal(obj, target_elt, 'strip_username') - self.check_param_equal(obj, target_elt, 'debug_mode') + self.check_param_bool(obj, target_elt, 'enable', default=True, value_true="yes") + self.check_param_bool(obj, target_elt, 'strip_username', value_true="yes") + self.check_param_bool(obj, target_elt, 'debug_mode', value_true="yes") self.check_param_equal(obj, target_elt, 'idp_metadata_url') - self.check_param_equal(obj, target_elt, 'idp_entity_id', default='') - self.check_param_equal(obj, target_elt, 'idp_sign_on_url', default='') - self.check_param_equal(obj, target_elt, 'idp_groups_attribute', default='') - self.check_param_equal(obj, target_elt, 'idp_x509_cert', default='') + self.check_param_equal(obj, target_elt, 'idp_entity_id') + self.check_param_equal(obj, target_elt, 'idp_sign_on_url') + self.check_param_equal(obj, target_elt, 'idp_groups_attribute') + self.check_param_equal(obj, target_elt, 'idp_x509_cert') self.check_param_equal(obj, target_elt, 'sp_base_url') - self.check_param_equal(obj, target_elt, 'custom_conf', default='') + self.check_param_equal(obj, target_elt, 'custom_conf') ############## - # tests + # test validation # + def test_entity_id_required_if_metadata_unset(self): + """ test not applying if not composite requirement fulfilled """ + obj = dict(enable=True, sp_base_url="https://pfSense.local") + self.do_module_test(obj, state=None, failed=True, msg="idp_entity_id is required when idp_metadata_url is unset") + + def test_sign_on_url_required_if_metadata_unset(self): + """ test not applying if not composite requirement fulfilled """ + obj = dict(enable=True, sp_base_url="https://pfSense.local", idp_entity_id="https://keycloak.local/realms/master") + self.do_module_test(obj, state=None, failed=True, msg="idp_sign_on_url is required when idp_metadata_url is unset") + + def test_x509_cert_required_if_metadata_unset(self): + """ test not applying if not composite requirement fulfilled """ + obj = dict(enable=True, sp_base_url="https://pfSense.local", idp_entity_id="https://keycloak.local/realms/master", idp_sign_on_url="https://keycloak.local/realms/master/protocol/saml") + self.do_module_test(obj, state=None, failed=True, msg="idp_x509_cert is required when idp_metadata_url is unset") + + def test_composite_requirement_fulfilled_when_metadata_unset(self): + """ test not applying if not composite requirement fulfilled """ + obj = dict(enable=True, sp_base_url="https://pfSense.local", idp_entity_id="https://keycloak.local/realms/master", idp_sign_on_url="https://keycloak.local/realms/master/protocol/saml", idp_x509_cert="-----BEGIN CERTIFICATE-----\nSOME_CERT\n-----END CERTIFICATE-----") + self.do_module_test(obj, state=None, changed=True, command="update saml2-auth 'https://pfSense.local' set idp_metadata_url='', idp_entity_id='https://keycloak.local/realms/master', idp_sign_on_url='https://keycloak.local/realms/master/protocol/saml', idp_x509_cert='-----BEGIN CERTIFICATE-----\nSOME_CERT\n-----END CERTIFICATE-----'") + + def test_entity_id_update_noop(self): + """ test not applying entity id with invalid characters """ + obj = CURRENT_CONFIG | dict(idp_entity_id="£://£") + self.do_module_test(obj, state=None, failed=True, msg="idp_entity_id contains invalid characters") + + def test_entity_id_update_noop(self): + """ test not applying too long entity id """ + obj = CURRENT_CONFIG | dict(idp_entity_id="x" * 1025) + self.do_module_test(obj, state=None, failed=True, msg="idp_entity_id must be less than 1025 characters long") - # def test_conf_not_found(self): - # """ TODO """ - - # def test_updated_settings(self): - # """ TODO """ - def test_x509_cert_update_noop(self): """ test not applying invalid x509 cert """ - obj = dict(sp_base_url="https://pfSense.local", idp_x509_cert="NOT_A_VALID_CERT") - self.do_module_test(obj, command="update saml", changed=False) + obj = CURRENT_CONFIG | dict(idp_x509_cert="NOT_A_VALID_CERT") + self.do_module_test(obj, state=None, failed=True, msg="idp_x509_cert is missing BEGIN and/or END tags") - # def test_invalid_url_update_noop(self): - # """ test not appling an invalid url """ - # obj = dict(sp_base_url="https://pfSense.local") - # self.do_module_test(obj, command="update saml", changed=False) + def test_invalid_url_update_noop(self): + """ test not appling an invalid url """ + obj = CURRENT_CONFIG | dict(sp_base_url="test.com") + self.do_module_test(obj, state=None, failed=True, msg="sp_base_url is not a valid URL") - # def test_invalid_json_update_noop(self): - # """ test not appling an invalid url """ - # obj = dict(sp_base_url="https://pfSense.local", custom_conf="invalid_json") - # self.do_module_test(obj, command="update saml", changed=False) + def test_invalid_json_update_noop(self): + """ test not appling an invalid url """ + obj = CURRENT_CONFIG | dict(custom_conf="invalid_json") + self.do_module_test(obj, state=None, failed=True, msg="custom_conf is not valid JSON") + ############## + # test apply + # + def test_updated_settings(self): + """ test updating config """ + obj = CURRENT_CONFIG | dict(idp_groups_attribute="memberOf") + self.do_module_test(obj, state=None, changed=True, command="update saml2-auth 'https://pfSense.local' set idp_groups_attribute='memberOf'") + + def test_nochange(self): + """ test applying same config as set """ + obj = CURRENT_CONFIG + self.do_module_test(obj, state=None, changed=False) From 03c4315bb6425655096fcdac713ec7a5534ccc34 Mon Sep 17 00:00:00 2001 From: Kevin Brooks Date: Fri, 16 Jan 2026 16:53:44 +0000 Subject: [PATCH 4/5] fix: cleanup --- plugins/modules/pfsense_saml.py | 13 ------------- tests/unit/plugins/modules/test_pfsense_saml.py | 1 - 2 files changed, 14 deletions(-) diff --git a/plugins/modules/pfsense_saml.py b/plugins/modules/pfsense_saml.py index 563eb821..2d5b3d8b 100644 --- a/plugins/modules/pfsense_saml.py +++ b/plugins/modules/pfsense_saml.py @@ -201,19 +201,6 @@ def _copy_and_update_target(self): changed = self.pfsense.copy_dict_to_element(self.obj, self.target_elt) return (self.diff["before"], changed) - - # def _remove_deleted_params(self): - # """" todo """ - # return False - - # # TODO: Does not work for ignore - # def _get_params_to_remove(self): - # """ returns the list of params to remove if they are not set """ - # return [] - - ############################## - # run - # ############################## # logging diff --git a/tests/unit/plugins/modules/test_pfsense_saml.py b/tests/unit/plugins/modules/test_pfsense_saml.py index 82c1c4ba..9daa362b 100644 --- a/tests/unit/plugins/modules/test_pfsense_saml.py +++ b/tests/unit/plugins/modules/test_pfsense_saml.py @@ -64,7 +64,6 @@ def check_target_elt(self, obj, target_elt): self.check_param_equal(obj, target_elt, 'custom_conf') - ############## # test validation # From c5baa44851e54c5d18dc63460fc8ffa5771e7a97 Mon Sep 17 00:00:00 2001 From: Kevin Brooks Date: Fri, 16 Jan 2026 16:56:50 +0000 Subject: [PATCH 5/5] fix: cleanup --- tests/unit/plugins/modules/test_pfsense_saml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/test_pfsense_saml.py b/tests/unit/plugins/modules/test_pfsense_saml.py index 9daa362b..e3866bd3 100644 --- a/tests/unit/plugins/modules/test_pfsense_saml.py +++ b/tests/unit/plugins/modules/test_pfsense_saml.py @@ -89,7 +89,7 @@ def test_composite_requirement_fulfilled_when_metadata_unset(self): def test_entity_id_update_noop(self): """ test not applying entity id with invalid characters """ - obj = CURRENT_CONFIG | dict(idp_entity_id="£://£") + obj = CURRENT_CONFIG | dict(idp_entity_id="£") self.do_module_test(obj, state=None, failed=True, msg="idp_entity_id contains invalid characters") def test_entity_id_update_noop(self):