From 54673341172279d5c0e17f641da90e8168208d0c Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:53:03 +0100 Subject: [PATCH] [PR #9275/adb4b3c8 backport][stable-10] Add module ldap inc (#9480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add module ldap inc (#9275) * Add module ldap_inc This module adds the ‘modify-increment’ capability corresponding to the extension implemented by OpenLdap described in RFC-4525. It can be used to increment an integer attribute and read it atomically. It is an help for posix userId definition while relying only on the directory server. Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Felix Fontein Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein Fix the check mode support Check mode documentation fix * Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein * Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein * Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein * Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein * Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein * Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein * Update plugins/modules/ldap_inc.py Co-authored-by: Felix Fontein --------- Co-authored-by: Felix Fontein (cherry picked from commit adb4b3c8a56aced01494b86711cc2a864ab35189) Co-authored-by: Philippe Duveau --- .github/BOTMETA.yml | 2 + plugins/modules/ldap_inc.py | 251 ++++++++++++++++++ tests/integration/targets/ldap_inc/aliases | 11 + .../targets/ldap_inc/meta/main.yml | 7 + .../targets/ldap_inc/tasks/main.yml | 16 ++ .../targets/ldap_inc/tasks/tests/basic.yml | 99 +++++++ .../files/inc_schema_cnconfig.ldif | 5 + .../files/inc_schema_cnconfig.ldif.license | 3 + .../setup_openldap/files/ldap_inc_config.ldif | 10 + .../files/ldap_inc_config.ldif.license | 3 + .../targets/setup_openldap/tasks/main.yml | 9 +- 11 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 plugins/modules/ldap_inc.py create mode 100644 tests/integration/targets/ldap_inc/aliases create mode 100644 tests/integration/targets/ldap_inc/meta/main.yml create mode 100644 tests/integration/targets/ldap_inc/tasks/main.yml create mode 100644 tests/integration/targets/ldap_inc/tasks/tests/basic.yml create mode 100644 tests/integration/targets/setup_openldap/files/inc_schema_cnconfig.ldif create mode 100644 tests/integration/targets/setup_openldap/files/inc_schema_cnconfig.ldif.license create mode 100644 tests/integration/targets/setup_openldap/files/ldap_inc_config.ldif create mode 100644 tests/integration/targets/setup_openldap/files/ldap_inc_config.ldif.license diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 6c7e48ec09..5390d81b27 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -856,6 +856,8 @@ files: maintainers: drybjed jtyr noles $modules/ldap_entry.py: maintainers: jtyr + $modules/ldap_inc.py: + maintainers: pduveau $modules/ldap_passwd.py: maintainers: KellerFuchs jtyr $modules/ldap_search.py: diff --git a/plugins/modules/ldap_inc.py b/plugins/modules/ldap_inc.py new file mode 100644 index 0000000000..d916331827 --- /dev/null +++ b/plugins/modules/ldap_inc.py @@ -0,0 +1,251 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Philippe Duveau +# Copyright (c) 2019, Maciej Delmanowski (ldap_attrs.py) +# Copyright (c) 2017, Alexander Korinek (ldap_attrs.py) +# Copyright (c) 2016, Peter Sagerson (ldap_attrs.py) +# Copyright (c) 2016, Jiri Tyr (ldap_attrs.py) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# The code of this module is derived from that of ldap_attrs.py + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: ldap_inc +short_description: Use the Modify-Increment LDAP V3 feature to increment an attribute value +version_added: 10.2.0 +description: + - Atomically increments the value of an attribute and return its new value. +notes: + - When implemented by the directory server, the module uses the ModifyIncrement extension + defined in L(RFC4525, https://www.rfc-editor.org/rfc/rfc4525.html) and the control PostRead. This extension and the control are + implemented in OpenLdap but not all directory servers implement them. In this case, the + module automatically uses a more classic method based on two phases, first the current + value is read then the modify operation remove the old value and add the new one in a + single request. If the value has changed by a concurrent call then the remove action will + fail. Then the sequence is retried 3 times before raising an error to the playbook. In an + heavy modification environment, the module does not guarante to be systematically successful. + - This only deals with integer attribute of an existing entry. To modify attributes + of an entry, see M(community.general.ldap_attrs) or to add or remove whole entries, + see M(community.general.ldap_entry). + - The default authentication settings will attempt to use a SASL EXTERNAL + bind over a UNIX domain socket. If you need to use a simple bind to access + your server, pass the credentials in O(bind_dn) and O(bind_pw). +author: + - Philippe Duveau (@pduveau) +requirements: + - python-ldap +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + dn: + required: true + type: str + description: + - The DN entry containing the attribute to increment. + attribute: + required: true + type: str + description: + - The attribute to increment. + increment: + required: false + type: int + default: 1 + description: + - The value of the increment to apply. + method: + required: false + type: str + default: auto + choices: [auto, rfc4525, legacy] + description: + - If V(auto), the module determines automatically the method to use. + - If V(rfc4525) or V(legacy) force to use the corresponding method. +extends_documentation_fragment: + - community.general.ldap.documentation + - community.general.attributes + +''' + + +EXAMPLES = r''' +- name: Increments uidNumber 1 Number for example.com + community.general.ldap_inc: + dn: "cn=uidNext,ou=unix-management,dc=example,dc=com" + attribute: "uidNumber" + increment: "1" + register: ldap_uidNumber_sequence + +- name: Modifies the user to define its identification number (uidNumber) when incrementation is successful + community.general.ldap_attrs: + dn: "cn=john,ou=posix-users,dc=example,dc=com" + state: present + attributes: + - uidNumber: "{{ ldap_uidNumber_sequence.value }}" + when: ldap_uidNumber_sequence.incremented +''' + + +RETURN = r''' +incremented: + description: + - It is set to V(true) if the attribute value has changed. + returned: success + type: bool + sample: true + +attribute: + description: + - The name of the attribute that was incremented. + returned: success + type: str + sample: uidNumber + +value: + description: + - The new value after incrementing. + returned: success + type: str + sample: "2" + +rfc4525: + description: + - Is V(true) if the method used to increment is based on RFC4525, V(false) if legacy. + returned: success + type: bool + sample: true +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_native, to_bytes +from ansible_collections.community.general.plugins.module_utils import deps +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together + +with deps.declare("ldap", reason=missing_required_lib('python-ldap')): + import ldap + import ldap.controls.readentry + + +class LdapInc(LdapGeneric): + def __init__(self, module): + LdapGeneric.__init__(self, module) + # Shortcuts + self.attr = self.module.params['attribute'] + self.increment = self.module.params['increment'] + self.method = self.module.params['method'] + + def inc_rfc4525(self): + return [(ldap.MOD_INCREMENT, self.attr, [to_bytes(str(self.increment))])] + + def inc_legacy(self, curr_val, new_val): + return [(ldap.MOD_DELETE, self.attr, [to_bytes(curr_val)]), + (ldap.MOD_ADD, self.attr, [to_bytes(new_val)])] + + def serverControls(self): + return [ldap.controls.readentry.PostReadControl(attrList=[self.attr])] + + LDAP_MOD_INCREMENT = to_bytes("1.3.6.1.1.14") + + +def main(): + module = AnsibleModule( + argument_spec=gen_specs( + attribute=dict(type='str', required=True), + increment=dict(type='int', default=1, required=False), + method=dict(type='str', default='auto', choices=['auto', 'rfc4525', 'legacy']), + ), + supports_check_mode=True, + required_together=ldap_required_together(), + ) + + deps.validate(module) + + # Instantiate the LdapAttr object + mod = LdapInc(module) + + changed = False + ret = "" + rfc4525 = False + + try: + if mod.increment != 0 and not module.check_mode: + changed = True + + if mod.method != "auto": + rfc4525 = mod.method == "rfc425" + else: + rootDSE = mod.connection.search_ext_s( + base="", + scope=ldap.SCOPE_BASE, + attrlist=["*", "+"]) + if len(rootDSE) == 1: + if to_bytes(ldap.CONTROL_POST_READ) in rootDSE[0][1]["supportedControl"] and ( + mod.LDAP_MOD_INCREMENT in rootDSE[0][1]["supportedFeatures"] or + mod.LDAP_MOD_INCREMENT in rootDSE[0][1]["supportedExtension"] + ): + rfc4525 = True + + if rfc4525: + dummy, dummy, dummy, resp_ctrls = mod.connection.modify_ext_s( + dn=mod.dn, + modlist=mod.inc_rfc4525(), + serverctrls=mod.serverControls(), + clientctrls=None) + if len(resp_ctrls) == 1: + ret = resp_ctrls[0].entry[mod.attr][0] + + else: + tries = 0 + max_tries = 3 + while tries < max_tries: + tries = tries + 1 + result = mod.connection.search_ext_s( + base=mod.dn, + scope=ldap.SCOPE_BASE, + filterstr="(%s=*)" % mod.attr, + attrlist=[mod.attr]) + if len(result) != 1: + module.fail_json(msg="The entry does not exist or does not contain the specified attribute.") + return + try: + ret = str(int(result[0][1][mod.attr][0]) + mod.increment) + # if the current value first arg in inc_legacy has changed then the modify will fail + mod.connection.modify_s( + dn=mod.dn, + modlist=mod.inc_legacy(result[0][1][mod.attr][0], ret)) + break + except ldap.NO_SUCH_ATTRIBUTE: + if tries == max_tries: + module.fail_json(msg="The increment could not be applied after " + str(max_tries) + " tries.") + return + + else: + result = mod.connection.search_ext_s( + base=mod.dn, + scope=ldap.SCOPE_BASE, + filterstr="(%s=*)" % mod.attr, + attrlist=[mod.attr]) + if len(result) == 1: + ret = str(int(result[0][1][mod.attr][0]) + mod.increment) + changed = mod.increment != 0 + else: + module.fail_json(msg="The entry does not exist or does not contain the specified attribute.") + + except Exception as e: + module.fail_json(msg="Attribute action failed.", details=to_native(e)) + + module.exit_json(changed=changed, incremented=changed, attribute=mod.attr, value=ret, rfc4525=rfc4525) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/ldap_inc/aliases b/tests/integration/targets/ldap_inc/aliases new file mode 100644 index 0000000000..7958445488 --- /dev/null +++ b/tests/integration/targets/ldap_inc/aliases @@ -0,0 +1,11 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/1 +skip/aix +skip/freebsd +skip/osx +skip/macos +skip/rhel +needs/root diff --git a/tests/integration/targets/ldap_inc/meta/main.yml b/tests/integration/targets/ldap_inc/meta/main.yml new file mode 100644 index 0000000000..d282aa0dc8 --- /dev/null +++ b/tests/integration/targets/ldap_inc/meta/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openldap diff --git a/tests/integration/targets/ldap_inc/tasks/main.yml b/tests/integration/targets/ldap_inc/tasks/main.yml new file mode 100644 index 0000000000..521075b5e1 --- /dev/null +++ b/tests/integration/targets/ldap_inc/tasks/main.yml @@ -0,0 +1,16 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Run LDAP search module tests + block: + - include_tasks: "{{ item }}" + with_fileglob: + - 'tests/*.yml' + when: ansible_os_family in ['Ubuntu', 'Debian'] diff --git a/tests/integration/targets/ldap_inc/tasks/tests/basic.yml b/tests/integration/targets/ldap_inc/tasks/tests/basic.yml new file mode 100644 index 0000000000..4165ece743 --- /dev/null +++ b/tests/integration/targets/ldap_inc/tasks/tests/basic.yml @@ -0,0 +1,99 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- debug: + msg: Running tests/basic.yml + +#################################################################### +## Increment ####################################################### +#################################################################### +- name: Test increment by default + ldap_inc: + bind_dn: "cn=admin,dc=example,dc=com" + bind_pw: "Test1234!" + dn: "cn=ldapinctest,ou=sequence,dc=example,dc=com" + attribute: "uidNumber" + ignore_errors: true + register: output + +- name: assert that test increment by default + assert: + that: + - output is not failed + - output.incremented + - output.value == "1001" + - output.rfc4525 + +- name: Test defined increment + ldap_inc: + bind_dn: "cn=admin,dc=example,dc=com" + bind_pw: "Test1234!" + dn: "cn=ldapinctest,ou=sequence,dc=example,dc=com" + attribute: "uidNumber" + increment: 2 + ignore_errors: true + register: output + +- name: assert that test increment by default + assert: + that: + - output is not failed + - output.incremented + - output.value == "1003" + - output.rfc4525 + +- name: Test defined increment by 0 + ldap_inc: + bind_dn: "cn=admin,dc=example,dc=com" + bind_pw: "Test1234!" + dn: "cn=ldapinctest,ou=sequence,dc=example,dc=com" + attribute: "uidNumber" + increment: 0 + ignore_errors: true + register: output + +- name: assert that test defined increment by 0 + assert: + that: + - output is not failed + - output.incremented == false + - output.value == "1003" + +- name: Test defined negative increment + ldap_inc: + bind_dn: "cn=admin,dc=example,dc=com" + bind_pw: "Test1234!" + dn: "cn=ldapinctest,ou=sequence,dc=example,dc=com" + attribute: "uidNumber" + increment: -1 + ignore_errors: true + register: output + +- name: assert that test defined negative increment + assert: + that: + - output is not failed + - output.incremented + - output.value == "1002" + - output.rfc4525 + +- name: Test forcing classic method instead of automatic detection + ldap_inc: + bind_dn: "cn=admin,dc=example,dc=com" + bind_pw: "Test1234!" + dn: "cn=ldapinctest,ou=sequence,dc=example,dc=com" + attribute: "uidNumber" + increment: -1 + method: "legacy" + ignore_errors: true + register: output + +- name: assert that test defined negative increment + assert: + that: + - output is not failed + - output.incremented + - output.value == "1001" + - output.rfc4525 == False diff --git a/tests/integration/targets/setup_openldap/files/inc_schema_cnconfig.ldif b/tests/integration/targets/setup_openldap/files/inc_schema_cnconfig.ldif new file mode 100644 index 0000000000..2ff39d8aee --- /dev/null +++ b/tests/integration/targets/setup_openldap/files/inc_schema_cnconfig.ldif @@ -0,0 +1,5 @@ +dn: cn=inc-schema,cn=schema,cn=config +changetype: add +objectClass: olcSchemaConfig +cn: inc-schema +olcObjectClasses: ( 1.3.6.1.4.1.4203.666.599 NAME 'uidNext' SUP top STRUCTURAL MUST ( cn $ uidNumber ) ) diff --git a/tests/integration/targets/setup_openldap/files/inc_schema_cnconfig.ldif.license b/tests/integration/targets/setup_openldap/files/inc_schema_cnconfig.ldif.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/setup_openldap/files/inc_schema_cnconfig.ldif.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/setup_openldap/files/ldap_inc_config.ldif b/tests/integration/targets/setup_openldap/files/ldap_inc_config.ldif new file mode 100644 index 0000000000..3c01248176 --- /dev/null +++ b/tests/integration/targets/setup_openldap/files/ldap_inc_config.ldif @@ -0,0 +1,10 @@ +dn: ou=sequence,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: sequence + +dn: cn=ldapinctest,ou=sequence,dc=example,dc=com +uidNumber: 1000 +objectClass: top +objectClass: uidNext +cn: ldapinctest diff --git a/tests/integration/targets/setup_openldap/files/ldap_inc_config.ldif.license b/tests/integration/targets/setup_openldap/files/ldap_inc_config.ldif.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/setup_openldap/files/ldap_inc_config.ldif.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/setup_openldap/tasks/main.yml b/tests/integration/targets/setup_openldap/tasks/main.yml index 00f8f6a108..a4cdae6de2 100644 --- a/tests/integration/targets/setup_openldap/tasks/main.yml +++ b/tests/integration/targets/setup_openldap/tasks/main.yml @@ -79,14 +79,21 @@ - rootpw_cnconfig.ldif - cert_cnconfig.ldif - initial_config.ldif + - inc_schema_cnconfig.ldif + - ldap_inc_config.ldif - name: Configure admin password for cn=config shell: "ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/{{ item }}" loop: - rootpw_cnconfig.ldif - cert_cnconfig.ldif + - inc_schema_cnconfig.ldif - name: Add initial config become: true - shell: 'ldapadd -H ldapi:/// -x -D "cn=admin,dc=example,dc=com" -w Test1234! -f /tmp/initial_config.ldif' + shell: 'ldapadd -H ldapi:/// -x -D "cn=admin,dc=example,dc=com" -w Test1234! -f /tmp/{{ item }}' + loop: + - initial_config.ldif + - ldap_inc_config.ldif + when: ansible_os_family in ['Ubuntu', 'Debian']