Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add module ldap inc #9275

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
250 changes: 250 additions & 0 deletions plugins/modules/ldap_inc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2024, Philippe Duveau <[email protected]>
# Copyright (c) 2019, Maciej Delmanowski <[email protected]> (ldap_attrs.py)
# Copyright (c) 2017, Alexander Korinek <[email protected]> (ldap_attrs.py)
# Copyright (c) 2016, Peter Sagerson <[email protected]> (ldap_attrs.py)
# Copyright (c) 2016, Jiri Tyr <[email protected]> (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
pduveau marked this conversation as resolved.
Show resolved Hide resolved
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).
pduveau marked this conversation as resolved.
Show resolved Hide resolved
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]
russoz marked this conversation as resolved.
Show resolved Hide resolved

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)

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)
pduveau marked this conversation as resolved.
Show resolved Hide resolved


if __name__ == '__main__':
main()
11 changes: 11 additions & 0 deletions tests/integration/targets/ldap_inc/aliases
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions tests/integration/targets/ldap_inc/meta/main.yml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions tests/integration/targets/ldap_inc/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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']
99 changes: 99 additions & 0 deletions tests/integration/targets/ldap_inc/tasks/tests/basic.yml
Original file line number Diff line number Diff line change
@@ -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
Loading