From f2b4f076b3be09ea3eca54f595beffaf0cbaf354 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 12:18:27 +0400 Subject: [PATCH 01/12] Refactor LDAP test to remove dependency on mockldap Signed-off-by: tdruez --- dje/tests/test_ldap.py | 273 +++++++++++++++++++++++++---------------- 1 file changed, 164 insertions(+), 109 deletions(-) diff --git a/dje/tests/test_ldap.py b/dje/tests/test_ldap.py index f7a17716..a0736b37 100644 --- a/dje/tests/test_ldap.py +++ b/dje/tests/test_ldap.py @@ -11,6 +11,7 @@ from django.apps import apps from django.conf import settings +from django.contrib.auth import authenticate from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.contrib.auth.models import Permission @@ -21,11 +22,12 @@ from django.urls import reverse import ldap +import slapdtest from django_auth_ldap.backend import _LDAPUserGroups from django_auth_ldap.config import GroupOfNamesType from django_auth_ldap.config import LDAPSearch from guardian.shortcuts import assign_perm -from mockldap import MockLdap +from ldap.ldapobject import SimpleLDAPObject from dje.ldap_backend import DejaCodeLDAPBackend from dje.models import Dataspace @@ -38,103 +40,88 @@ Product = apps.get_model("product_portfolio", "Product") -AUTH_LDAP_USER_ATTR_MAP = { - "first_name": "givenName", - "last_name": "sn", - "email": "mail", -} - - -LDAP_GROUP_SETTINGS = { - "AUTH_LDAP_GROUP_SEARCH": LDAPSearch( - "ou=groups,dc=nexb,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" - ), - "AUTH_LDAP_GROUP_TYPE": GroupOfNamesType(), -} +LDIF = """ +dn: o=test +objectClass: organization +o: test + +dn: ou=people,o=test +objectClass: organizationalUnit +ou: people + +dn: ou=groups,o=test +objectClass: organizationalUnit +ou: groups + +dn: uid=bob,ou=people,o=test +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: posixAccount +cn: bob +uid: bob +userPassword: secret +uidNumber: 1001 +gidNumber: 50 +givenName: Robert +sn: Smith +homeDirectory: /home/bob +mail: bob@test.com + +dn: cn=active,ou=groups,o=test +cn: active +objectClass: groupOfNames +member: uid=bob,ou=people,o=test +""" @override_settings( - AUTH_LDAP_SERVER_URI="ldap://localhost/", AUTHENTICATION_BACKENDS=("dje.ldap_backend.DejaCodeLDAPBackend",), AUTH_LDAP_DATASPACE="nexB", - AUTH_LDAP_USER_SEARCH=LDAPSearch( - "ou=people,dc=nexb,dc=com", ldap.SCOPE_SUBTREE, "(samaccountname=%(user)s)" + AUTH_LDAP_START_TLS=False, + AUTH_LDAP_USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test", + AUTH_LDAP_USER_SEARCH=LDAPSearch("ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"), + AUTH_LDAP_UBIND_AS_AUTHENTICATING_USER=True, + AUTH_LDAP_USER_ATTR_MAP={ + "first_name": "givenName", + "last_name": "sn", + "email": "mail", + }, + AUTH_LDAP_GROUP_SEARCH=LDAPSearch( + "ou=groups,o=test", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" ), - **LDAP_GROUP_SETTINGS, + AUTH_LDAP_GROUP_TYPE=GroupOfNamesType(), ) class DejaCodeLDAPBackendTestCase(TestCase): - top = ("dc=com", {"dc": "com"}) - nexb = ("dc=nexb,dc=com", {"dc": "nexb"}) - people = ("ou=people,dc=nexb,dc=com", {"ou": "people"}) - groups = ("ou=groups,dc=nexb,dc=com", {"ou": "groups"}) - - bob = ( - "cn=bob,ou=people,dc=nexb,dc=com", - { - "cn": "bob", - "samaccountname": "bob", - "uid": ["bob"], - "userPassword": ["secret"], - "mail": ["bob@test.com"], - "givenName": ["Robert"], - "sn": ["Smith"], - }, - ) - - group_active = ( - "cn=active,ou=groups,dc=nexb,dc=com", - { - "cn": ["active"], - "objectClass": ["groupOfNames"], - "member": ["cn=bob,ou=people,dc=nexb,dc=com"], - }, - ) - - group_not_in_database = ( - "cn=not_in_database,ou=groups,dc=nexb,dc=com", - { - "cn": ["not_in_database"], - "objectClass": ["groupOfNames"], - "member": ["cn=bob,ou=people,dc=nexb,dc=com"], - }, - ) - - group_superuser = ( - "cn=superuser,ou=groups,dc=nexb,dc=com", - { - "cn": ["superuser"], - "objectClass": ["groupOfNames"], - "member": ["cn=bob,ou=people,dc=nexb,dc=com"], - }, - ) - - # This is the content of our mock LDAP directory. It takes the form - # {dn: {attr: [value, ...], ...}, ...}. - directory = dict( - [ - top, - nexb, - people, - groups, - bob, - group_active, - group_not_in_database, - group_superuser, - ] - ) + server_class = slapdtest.SlapdObject + ldap_object_class = SimpleLDAPObject @classmethod def setUpClass(cls): super().setUpClass() cls.configure_logger() - # We only need to create the MockLdap instance once. The content we - # pass in will be used for all LDAP connections. - cls.mockldap = MockLdap(cls.directory) + + cls.server = cls.server_class() + cls.server.suffix = "o=test" + cls.server.openldap_schema_files = [ + "core.ldif", + "cosine.ldif", + "inetorgperson.ldif", + "nis.ldif", + ] + cls.server.start() + cls.server.ldapadd(LDIF) + + # Override the AUTH_LDAP_SERVER_URI with the dynamic URI + cls._settings_override = override_settings(AUTH_LDAP_SERVER_URI=cls.server.ldap_uri) + cls._settings_override.enable() @classmethod def tearDownClass(cls): super().tearDownClass() - del cls.mockldap + cls.server.stop() + # Disable the settings override + cls._settings_override.disable() @classmethod def configure_logger(cls): @@ -145,24 +132,46 @@ def configure_logger(cls): def setUp(self): cache.clear() - # Patch ldap.initialize - self.mockldap.start() - self.ldapobj = self.mockldap["ldap://localhost/"] - - # DejaCode objects self.nexb_dataspace = Dataspace.objects.create(name="nexB") self.dejacode_group_active = Group.objects.create(name="active") self.dejacode_group1 = Group.objects.create(name="group1") - change_license_perm = Permission.objects.get_by_natural_key( "change_license", "license_library", "license" ) self.dejacode_group_active.permissions.add(change_license_perm) - def tearDown(self): - # Stop patching ldap.initialize and reset state. - self.mockldap.stop() - del self.ldapobj + def test_ldap_authentication_populate_user(self): + user = authenticate(username="bob", password="secret") + self.assertEqual(user.username, "bob") + self.assertEqual(user.first_name, "Robert") + self.assertEqual(user.last_name, "Smith") + self.assertEqual(user.email, "bob@test.com") + + def test_bind_and_search(self): + # Connect to the temporary slapd server + conn = self.ldap_object_class(self.server.ldap_uri) + conn.simple_bind_s(self.server.root_dn, self.server.root_pw) + + # Search for the top entry + result = conn.search_s(self.server.suffix, ldap.SCOPE_BASE) + self.assertEqual(len(result), 1) + dn, entry = result[0] + self.assertEqual(dn, self.server.suffix) + + def test_ldap_group_active_properly_setup_and_searchable(self): + conn = self.ldap_object_class(self.server.ldap_uri) + results = conn.search_s("ou=groups,o=test", ldap.SCOPE_ONELEVEL, "(cn=active)") + expected = [ + ( + "cn=active,ou=groups,o=test", + { + "cn": [b"active"], + "objectClass": [b"groupOfNames"], + "member": [b"uid=bob,ou=people,o=test"], + }, + ) + ] + self.assertEqual(expected, results) @override_settings(AUTH_LDAP_AUTOCREATE_USER=False) def test_ldap_authentication_no_autocreate_user(self): @@ -208,7 +217,7 @@ def test_ldap_authentication_autocreate_user_proper_dataspace(self): # Next login, the DB user is re-used self.assertTrue(self.client.login(username="bob", password="secret")) - @override_settings(AUTH_LDAP_USER_ATTR_MAP=AUTH_LDAP_USER_ATTR_MAP) + # @override_settings(AUTH_LDAP_USER_ATTR_MAP=AUTH_LDAP_USER_ATTR_MAP) def test_ldap_authentication_autocreate_user_with_attr_map(self): self.assertFalse(DejacodeUser.objects.filter(username="bob").exists()) @@ -221,7 +230,7 @@ def test_ldap_authentication_autocreate_user_with_attr_map(self): self.assertEqual(self.nexb_dataspace, created_user.dataspace) @override_settings( - AUTH_LDAP_USER_ATTR_MAP=AUTH_LDAP_USER_ATTR_MAP, + # AUTH_LDAP_USER_ATTR_MAP=AUTH_LDAP_USER_ATTR_MAP, AUTH_LDAP_ALWAYS_UPDATE_USER=True, ) def test_ldap_authentication_update_user_with_attr_map(self): @@ -243,22 +252,6 @@ def test_ldap_authentication_update_user_with_attr_map(self): self.assertEqual("bob@test.com", user.email) self.assertEqual(self.nexb_dataspace, user.dataspace) - def test_ldap_group_active_properly_setup_and_searchable(self): - conn = ldap.initialize("ldap://localhost/") - results = conn.search_s("ou=groups,dc=nexb,dc=com", ldap.SCOPE_ONELEVEL, "(cn=active)") - - expected = [ - ( - "cn=active,ou=groups,dc=nexb,dc=com", - { - "cn": ["active"], - "objectClass": ["groupOfNames"], - "member": ["cn=bob,ou=people,dc=nexb,dc=com"], - }, - ) - ] - self.assertEqual(expected, results) - @override_settings(AUTH_LDAP_FIND_GROUP_PERMS=True) def test_ldap_authentication_group_permissions(self): bob = create_user("bob", self.nexb_dataspace, email="bob@test.com", is_staff=True) @@ -390,3 +383,65 @@ def test_ldap_object_secured_access(self): # The `ObjectPermissionBackend` is not needed since `ProductSecuredManager.get_queryset()` # calls directly `guardian.shortcuts.get_objects_for_user` self.assertEqual(200, self.client.get(url).status_code) + + +# class DejaCodeLDAPBackendTestCase(TestCase): +# top = ("dc=com", {"dc": "com"}) +# nexb = ("dc=nexb,dc=com", {"dc": "nexb"}) +# people = ("ou=people,dc=nexb,dc=com", {"ou": "people"}) +# groups = ("ou=groups,dc=nexb,dc=com", {"ou": "groups"}) +# +# bob = ( +# "cn=bob,ou=people,dc=nexb,dc=com", +# { +# "cn": "bob", +# "samaccountname": "bob", +# "uid": ["bob"], +# "userPassword": ["secret"], +# "mail": ["bob@test.com"], +# "givenName": ["Robert"], +# "sn": ["Smith"], +# }, +# ) +# +# group_active = ( +# "cn=active,ou=groups,dc=nexb,dc=com", +# { +# "cn": ["active"], +# "objectClass": ["groupOfNames"], +# "member": ["cn=bob,ou=people,dc=nexb,dc=com"], +# }, +# ) +# +# group_not_in_database = ( +# "cn=not_in_database,ou=groups,dc=nexb,dc=com", +# { +# "cn": ["not_in_database"], +# "objectClass": ["groupOfNames"], +# "member": ["cn=bob,ou=people,dc=nexb,dc=com"], +# }, +# ) +# +# group_superuser = ( +# "cn=superuser,ou=groups,dc=nexb,dc=com", +# { +# "cn": ["superuser"], +# "objectClass": ["groupOfNames"], +# "member": ["cn=bob,ou=people,dc=nexb,dc=com"], +# }, +# ) +# +# # This is the content of our mock LDAP directory. It takes the form +# # {dn: {attr: [value, ...], ...}, ...}. +# directory = dict( +# [ +# top, +# nexb, +# people, +# groups, +# bob, +# group_active, +# group_not_in_database, +# group_superuser, +# ] +# ) From a780ec5abef7da50b0b0a014bbc1f46467271eea Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 12:24:16 +0400 Subject: [PATCH 02/12] Remove mockldap and funcparserlib dependencies Signed-off-by: tdruez --- pyproject.toml | 3 --- thirdparty/dist/funcparserlib-0.3.6.tar.gz | Bin 33846 -> 0 bytes .../dist/funcparserlib-0.3.6.tar.gz.ABOUT | 20 --------------- .../dist/funcparserlib-0.3.6.tar.gz.NOTICE | 18 -------------- .../mockldap-0.3.0.post1-py2.py3-none-any.whl | Bin 13583 -> 0 bytes ...dap-0.3.0.post1-py2.py3-none-any.whl.ABOUT | 22 ----------------- ...ap-0.3.0.post1-py2.py3-none-any.whl.NOTICE | 23 ------------------ 7 files changed, 86 deletions(-) delete mode 100644 thirdparty/dist/funcparserlib-0.3.6.tar.gz delete mode 100644 thirdparty/dist/funcparserlib-0.3.6.tar.gz.ABOUT delete mode 100644 thirdparty/dist/funcparserlib-0.3.6.tar.gz.NOTICE delete mode 100644 thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl delete mode 100644 thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.ABOUT delete mode 100644 thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE diff --git a/pyproject.toml b/pyproject.toml index 3a803c45..dfedb65b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,9 +119,6 @@ dependencies = [ "pyasn1==0.6.1", "pyasn1-modules==0.4.2", "django-auth-ldap==5.2.0", - # LDAP Testing - "mockldap==0.3.0.post1", - "funcparserlib==0.3.6", # license expressions "boolean.py==5.0", "license-expression==30.4.4", diff --git a/thirdparty/dist/funcparserlib-0.3.6.tar.gz b/thirdparty/dist/funcparserlib-0.3.6.tar.gz deleted file mode 100644 index fd54200a7825049de4e4c1cedec94d26794adeeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33846 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�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 Lm-!rx0O$e$DB%W= diff --git a/thirdparty/dist/funcparserlib-0.3.6.tar.gz.ABOUT b/thirdparty/dist/funcparserlib-0.3.6.tar.gz.ABOUT deleted file mode 100644 index a37e6be9..00000000 --- a/thirdparty/dist/funcparserlib-0.3.6.tar.gz.ABOUT +++ /dev/null @@ -1,20 +0,0 @@ -about_resource: funcparserlib-0.3.6.tar.gz -name: funcparserlib -version: 0.3.6 -download_url: https://pypi.python.org/packages/source/f/funcparserlib/funcparserlib-0.3.6.tar.gz#md5=3aba546bdad5d0826596910551ce37c0 -description: Recursive descent parsing library for Python based on functional combinators. (Description - from project page) -homepage_url: http://code.google.com/p/funcparserlib/ -package_url: pkg:pypi/funcparserlib@0.3.6 -license_expression: mit -copyright: Copyright 2008/2011 Andrey Vlasovskikh -notice_file: funcparserlib-0.3.6.tar.gz.NOTICE -notice_url: https://github.com/vlasovskikh/funcparserlib/blob/master/LICENSE -attribute: yes -owner: Andrey Vlasovskikh -checksum_md5: 3aba546bdad5d0826596910551ce37c0 -checksum_sha1: 6db0e9e78dcddd993cdd488945b040934a361f03 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/funcparserlib-0.3.6.tar.gz.NOTICE b/thirdparty/dist/funcparserlib-0.3.6.tar.gz.NOTICE deleted file mode 100644 index 012128de..00000000 --- a/thirdparty/dist/funcparserlib-0.3.6.tar.gz.NOTICE +++ /dev/null @@ -1,18 +0,0 @@ -Copyright © 2009/2013 Andrey Vlasovskikh - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl b/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl deleted file mode 100644 index de730ce7f0872448fd42f841c3598cd55c5967d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13583 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 diff --git a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index e675f3dc..00000000 --- a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,22 +0,0 @@ -about_resource: mockldap-0.3.0.post1-py2.py3-none-any.whl -name: mockldap -version: 0.3.0.post1 -download_url: https://files.pythonhosted.org/packages/e3/6e/1536bc788db4cbccf3f2ffb37737af5e90f163ce69858f5aa1275981ed8a/mockldap-0.3.0.post1-py2.py3-none-any.whl#sha256=cdde6a0266be9f95d83629d530888559f4458017b884a2763ea95e9ae33b38d9 -description: This project provides a mock replacement for python-ldap (pyldap on Python 3). - It's useful for any project that would like to write unit tests against LDAP code without - relying on a running LDAP server. -homepage_url: https://bitbucket.org/psagers/mockldap -package_url: pkg:pypi/mockldap@0.3.0.post1 -license_expression: bsd-new -copyright: Copyright (c) 2013, Peter Sagerson -notice_file: mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE -notice_url: https://bitbucket.org/psagers/mockldap/src/8a83569d4b2f250fd4384e3aca5530c8baa21965/LICENSE?at=default&fileviewer=file-view-default -attribute: yes -owner: Peter Sagerson -owner_url: https://www.linkedin.com/in/psagers -checksum_md5: 8f76e4496ac575e744de01c879af9784 -checksum_sha1: d291d06cc8126cb2f8a7669f23a78f057d5b6b58 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE b/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE deleted file mode 100644 index ebbc625e..00000000 --- a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2013, Peter Sagerson -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -- Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file From a5167b5d4296c2ae8e80f15c922412794721c804 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 12:32:29 +0400 Subject: [PATCH 03/12] Fix ldap unit tests Signed-off-by: tdruez --- CHANGELOG.rst | 6 ++ dje/tests/test_ldap.py | 128 ++++++++--------------------------------- 2 files changed, 30 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 40d1059f..ccf017bf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Release notes ============= +### Version 5.4.2 + +- Migrate the LDAP testing from using mockldap to slapdtest. + The mockldap and funcparserlib dependencies has been removed. + https://github.com/aboutcode-org/dejacode/issues/394 + ### Version 5.4.1 - Upgrade Django to latest security release 5.2.7 diff --git a/dje/tests/test_ldap.py b/dje/tests/test_ldap.py index a0736b37..99c5f79e 100644 --- a/dje/tests/test_ldap.py +++ b/dje/tests/test_ldap.py @@ -7,7 +7,6 @@ # import logging -from unittest import mock from django.apps import apps from django.conf import settings @@ -23,7 +22,6 @@ import ldap import slapdtest -from django_auth_ldap.backend import _LDAPUserGroups from django_auth_ldap.config import GroupOfNamesType from django_auth_ldap.config import LDAPSearch from guardian.shortcuts import assign_perm @@ -39,7 +37,6 @@ Component = apps.get_model("component_catalog", "Component") Product = apps.get_model("product_portfolio", "Product") - LDIF = """ dn: o=test objectClass: organization @@ -72,6 +69,16 @@ cn: active objectClass: groupOfNames member: uid=bob,ou=people,o=test + +dn: cn=not_in_database,ou=groups,o=test +cn: not_in_database +objectClass: groupOfNames +member: uid=bob,ou=people,o=test + +dn: cn=superuser,ou=groups,o=test +cn: superuser +objectClass: groupOfNames +member: uid=bob,ou=people,o=test """ @@ -93,6 +100,7 @@ AUTH_LDAP_GROUP_TYPE=GroupOfNamesType(), ) class DejaCodeLDAPBackendTestCase(TestCase): + # https://www.python-ldap.org/en/latest/reference/slapdtest.html server_class = slapdtest.SlapdObject ldap_object_class = SimpleLDAPObject @@ -147,17 +155,6 @@ def test_ldap_authentication_populate_user(self): self.assertEqual(user.last_name, "Smith") self.assertEqual(user.email, "bob@test.com") - def test_bind_and_search(self): - # Connect to the temporary slapd server - conn = self.ldap_object_class(self.server.ldap_uri) - conn.simple_bind_s(self.server.root_dn, self.server.root_pw) - - # Search for the top entry - result = conn.search_s(self.server.suffix, ldap.SCOPE_BASE) - self.assertEqual(len(result), 1) - dn, entry = result[0] - self.assertEqual(dn, self.server.suffix) - def test_ldap_group_active_properly_setup_and_searchable(self): conn = self.ldap_object_class(self.server.ldap_uri) results = conn.search_s("ou=groups,o=test", ldap.SCOPE_ONELEVEL, "(cn=active)") @@ -201,9 +198,9 @@ def test_ldap_authentication_autocreate_user_proper_dataspace(self): # User was created on first login created_user = DejacodeUser.objects.get(username="bob") - self.assertEqual("", created_user.first_name) - self.assertEqual("", created_user.last_name) - self.assertEqual("", created_user.email) + self.assertEqual("Robert", created_user.first_name) + self.assertEqual("Smith", created_user.last_name) + self.assertEqual("bob@test.com", created_user.email) self.assertEqual(self.nexb_dataspace, created_user.dataspace) self.assertTrue(created_user.is_active) @@ -217,7 +214,6 @@ def test_ldap_authentication_autocreate_user_proper_dataspace(self): # Next login, the DB user is re-used self.assertTrue(self.client.login(username="bob", password="secret")) - # @override_settings(AUTH_LDAP_USER_ATTR_MAP=AUTH_LDAP_USER_ATTR_MAP) def test_ldap_authentication_autocreate_user_with_attr_map(self): self.assertFalse(DejacodeUser.objects.filter(username="bob").exists()) @@ -229,10 +225,7 @@ def test_ldap_authentication_autocreate_user_with_attr_map(self): self.assertEqual("bob@test.com", created_user.email) self.assertEqual(self.nexb_dataspace, created_user.dataspace) - @override_settings( - # AUTH_LDAP_USER_ATTR_MAP=AUTH_LDAP_USER_ATTR_MAP, - AUTH_LDAP_ALWAYS_UPDATE_USER=True, - ) + @override_settings(AUTH_LDAP_ALWAYS_UPDATE_USER=True) def test_ldap_authentication_update_user_with_attr_map(self): # Manually create the user first, then see if the values are updated create_user("bob", self.nexb_dataspace, email="other@mail.com") @@ -268,9 +261,9 @@ def test_ldap_authentication_group_permissions(self): self.assertFalse(Group.objects.filter(name="not_in_database").exists()) self.assertEqual({"active", "not_in_database", "superuser"}, bob.ldap_user.group_names) expected_group_dns = { - "cn=active,ou=groups,dc=nexb,dc=com", - "cn=not_in_database,ou=groups,dc=nexb,dc=com", - "cn=superuser,ou=groups,dc=nexb,dc=com", + "cn=active,ou=groups,o=test", + "cn=not_in_database,ou=groups,o=test", + "cn=superuser,ou=groups,o=test", } self.assertEqual(expected_group_dns, bob.ldap_user.group_dns) @@ -307,25 +300,14 @@ def test_ldap_user_flags_assigned_through_groups(self): self.assertEqual({"active", "not_in_database", "superuser"}, bob.ldap_user.group_names) user_flags_by_group = { - "is_superuser": "cn=superuser,ou=groups,dc=nexb,dc=com", + "is_superuser": "cn=superuser,ou=groups,o=test", } - # WARNING: This is a workaround for a bug in mockldap. - # There's a comparison issue in `mockldap.ldapobject.LDAPObject._compare_s` - # where the `value` is bytes b'' and `values` is a list of strings. - # For example: - # value = b'cn=bob,ou=people,dc=nexb,dc=com' - # values = ['cn=bob,ou=people,dc=nexb,dc=com'] - # Note that mockldap has been replaced by `slapdtest` in recent `python-ldap` versions. - # The migration to `slapdtest` requires a slaptd daemon runnning plus the rewrite of this - # whole TestCase. - # https://www.python-ldap.org/en/latest/reference/slapdtest.html - with mock.patch.object(_LDAPUserGroups, "is_member_of", return_value=True): - with override_settings(AUTH_LDAP_USER_FLAGS_BY_GROUP=user_flags_by_group): - bob = DejaCodeLDAPBackend().authenticate( - request=None, username="bob", password="secret" - ) - self.assertTrue(bob.is_superuser) + with override_settings(AUTH_LDAP_USER_FLAGS_BY_GROUP=user_flags_by_group): + bob = DejaCodeLDAPBackend().authenticate( + request=None, username="bob", password="secret" + ) + self.assertTrue(bob.is_superuser) def test_ldap_tab_set_mixin_get_tabsets(self): from component_catalog.views import ComponentDetailsView @@ -383,65 +365,3 @@ def test_ldap_object_secured_access(self): # The `ObjectPermissionBackend` is not needed since `ProductSecuredManager.get_queryset()` # calls directly `guardian.shortcuts.get_objects_for_user` self.assertEqual(200, self.client.get(url).status_code) - - -# class DejaCodeLDAPBackendTestCase(TestCase): -# top = ("dc=com", {"dc": "com"}) -# nexb = ("dc=nexb,dc=com", {"dc": "nexb"}) -# people = ("ou=people,dc=nexb,dc=com", {"ou": "people"}) -# groups = ("ou=groups,dc=nexb,dc=com", {"ou": "groups"}) -# -# bob = ( -# "cn=bob,ou=people,dc=nexb,dc=com", -# { -# "cn": "bob", -# "samaccountname": "bob", -# "uid": ["bob"], -# "userPassword": ["secret"], -# "mail": ["bob@test.com"], -# "givenName": ["Robert"], -# "sn": ["Smith"], -# }, -# ) -# -# group_active = ( -# "cn=active,ou=groups,dc=nexb,dc=com", -# { -# "cn": ["active"], -# "objectClass": ["groupOfNames"], -# "member": ["cn=bob,ou=people,dc=nexb,dc=com"], -# }, -# ) -# -# group_not_in_database = ( -# "cn=not_in_database,ou=groups,dc=nexb,dc=com", -# { -# "cn": ["not_in_database"], -# "objectClass": ["groupOfNames"], -# "member": ["cn=bob,ou=people,dc=nexb,dc=com"], -# }, -# ) -# -# group_superuser = ( -# "cn=superuser,ou=groups,dc=nexb,dc=com", -# { -# "cn": ["superuser"], -# "objectClass": ["groupOfNames"], -# "member": ["cn=bob,ou=people,dc=nexb,dc=com"], -# }, -# ) -# -# # This is the content of our mock LDAP directory. It takes the form -# # {dn: {attr: [value, ...], ...}, ...}. -# directory = dict( -# [ -# top, -# nexb, -# people, -# groups, -# bob, -# group_active, -# group_not_in_database, -# group_superuser, -# ] -# ) From d42761e956b41fc84595d5d373bba46b1c8bf001 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:02:34 +0400 Subject: [PATCH 04/12] Add slapd as a Debian dependency Signed-off-by: tdruez --- .github/workflows/run-unit-tests.yml | 2 +- Dockerfile | 1 + dje/tests/test_ldap.py | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 6d3430ca..87dd4269 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -45,7 +45,7 @@ jobs: - name: Install python-ldap OS dependencies run: | sudo apt-get update - sudo apt-get install -y libldap2-dev libsasl2-dev + sudo apt-get install -y libldap2-dev libsasl2-dev slapd - name: Install dependencies run: make dev envfile diff --git a/Dockerfile b/Dockerfile index 28593553..46035be0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,7 @@ RUN apt-get update \ build-essential \ libldap2-dev \ libsasl2-dev \ + slapd \ libpq5 \ git \ wait-for-it \ diff --git a/dje/tests/test_ldap.py b/dje/tests/test_ldap.py index 99c5f79e..38833828 100644 --- a/dje/tests/test_ldap.py +++ b/dje/tests/test_ldap.py @@ -6,6 +6,20 @@ # See https://aboutcode.org for more information about AboutCode FOSS projects. # +""" +This module is based on slapdtest, it requires OpenLDAP to be installed on the host. +https://www.python-ldap.org/en/latest/reference/slapdtest.html + +On Debian: +$ apt-get install slapd + +On macOS: +$ brew install openldap +$ export PATH="/opt/homebrew/opt/openldap/bin:$PATH" +$ export PATH="/opt/homebrew/opt/openldap/sbin:$PATH" +$ export PATH="/opt/homebrew/opt/openldap/libexec:$PATH" +""" + import logging from django.apps import apps @@ -100,7 +114,6 @@ AUTH_LDAP_GROUP_TYPE=GroupOfNamesType(), ) class DejaCodeLDAPBackendTestCase(TestCase): - # https://www.python-ldap.org/en/latest/reference/slapdtest.html server_class = slapdtest.SlapdObject ldap_object_class = SimpleLDAPObject From e0cf1b9f5750bf7ce9f4880ff84f7b986ac33074 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:06:08 +0400 Subject: [PATCH 05/12] Fix format validity Signed-off-by: tdruez --- dje/tests/test_ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dje/tests/test_ldap.py b/dje/tests/test_ldap.py index 38833828..2f0eab58 100644 --- a/dje/tests/test_ldap.py +++ b/dje/tests/test_ldap.py @@ -7,7 +7,7 @@ # """ -This module is based on slapdtest, it requires OpenLDAP to be installed on the host. +Test module is based on slapdtest, it requires OpenLDAP to be installed on the host. https://www.python-ldap.org/en/latest/reference/slapdtest.html On Debian: From a75e2c9d68daaebe13a8a5b1aa6ec3bff4657163 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:11:41 +0400 Subject: [PATCH 06/12] Add ldap-utils as dependency Signed-off-by: tdruez --- .github/workflows/run-unit-tests.yml | 2 +- Dockerfile | 1 + docs/installation.rst | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 87dd4269..b2666607 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -45,7 +45,7 @@ jobs: - name: Install python-ldap OS dependencies run: | sudo apt-get update - sudo apt-get install -y libldap2-dev libsasl2-dev slapd + sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils - name: Install dependencies run: make dev envfile diff --git a/Dockerfile b/Dockerfile index 46035be0..6c649bbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,7 @@ RUN apt-get update \ libldap2-dev \ libsasl2-dev \ slapd \ + ldap-utils \ libpq5 \ git \ wait-for-it \ diff --git a/docs/installation.rst b/docs/installation.rst index 805509dc..f8038dd0 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -186,7 +186,7 @@ Before you install DejaCode, make sure you have the following prerequisites: #. **ldap** development libraries to build python-ldap on Linux. For instance on Debian:: - apt-get install -y libldap2-dev libsasl2-dev + apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils .. _system_dependencies: From 03955eac1cea7de0250abc293b92829c5f485709 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:27:01 +0400 Subject: [PATCH 07/12] Disable AppArmor for slapd to allow slapdtest to create temp instances Signed-off-by: tdruez --- .github/workflows/run-unit-tests.yml | 5 ++++- dje/tests/test_ldap.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index b2666607..eb8b4517 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -45,7 +45,10 @@ jobs: - name: Install python-ldap OS dependencies run: | sudo apt-get update - sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils + # https://github.com/python-ldap/python-ldap/blob/main/.github/workflows/ci.yml + sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils apparmor-utils + # Disable AppArmor for slapd to allow slapdtest to create temp instances + sudo aa-disable /usr/sbin/slapd - name: Install dependencies run: make dev envfile diff --git a/dje/tests/test_ldap.py b/dje/tests/test_ldap.py index 2f0eab58..1862b3c4 100644 --- a/dje/tests/test_ldap.py +++ b/dje/tests/test_ldap.py @@ -11,7 +11,7 @@ https://www.python-ldap.org/en/latest/reference/slapdtest.html On Debian: -$ apt-get install slapd +$ apt-get install slapd ldap-utils On macOS: $ brew install openldap From 71392a7abf9925e6a231040be942427a9fe504ef Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:32:14 +0400 Subject: [PATCH 08/12] Disable AppArmor for slapd to allow slapdtest to create temp instances Signed-off-by: tdruez --- .github/workflows/run-unit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index eb8b4517..a36281ad 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -46,7 +46,7 @@ jobs: run: | sudo apt-get update # https://github.com/python-ldap/python-ldap/blob/main/.github/workflows/ci.yml - sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils apparmor-utils + sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils enchant-2 apparmor-utils # Disable AppArmor for slapd to allow slapdtest to create temp instances sudo aa-disable /usr/sbin/slapd From 324f51745b53818792933aba9203b587eeb876c9 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:35:45 +0400 Subject: [PATCH 09/12] Disable AppArmor for slapd to allow slapdtest to create temp instances Signed-off-by: tdruez --- .github/workflows/run-unit-tests.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index a36281ad..9ec34dcd 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -42,13 +42,21 @@ jobs: with: python-version: "3.13" - - name: Install python-ldap OS dependencies +# - name: Install python-ldap OS dependencies +# run: | +# sudo apt-get update +# # https://github.com/python-ldap/python-ldap/blob/main/.github/workflows/ci.yml +# sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils enchant-2 apparmor-utils +# # Disable AppArmor for slapd to allow slapdtest to create temp instances +# sudo aa-disable /usr/sbin/slapd + + - name: Install apt dependencies run: | - sudo apt-get update - # https://github.com/python-ldap/python-ldap/blob/main/.github/workflows/ci.yml - sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils enchant-2 apparmor-utils - # Disable AppArmor for slapd to allow slapdtest to create temp instances - sudo aa-disable /usr/sbin/slapd + set -ex + sudo apt update + sudo apt install -y ldap-utils slapd enchant-2 libldap2-dev libsasl2-dev apparmor-utils + - name: Disable AppArmor + run: sudo aa-disable /usr/sbin/slapd - name: Install dependencies run: make dev envfile From e80b673d5f64da180b7fc8ae32b254009e89460a Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:39:19 +0400 Subject: [PATCH 10/12] Stop system slapd - slapdtest will create its own test instances Signed-off-by: tdruez --- .github/workflows/run-unit-tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 9ec34dcd..a3836e5c 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -50,13 +50,13 @@ jobs: # # Disable AppArmor for slapd to allow slapdtest to create temp instances # sudo aa-disable /usr/sbin/slapd - - name: Install apt dependencies + - name: Install python-ldap OS dependencies run: | - set -ex - sudo apt update - sudo apt install -y ldap-utils slapd enchant-2 libldap2-dev libsasl2-dev apparmor-utils - - name: Disable AppArmor - run: sudo aa-disable /usr/sbin/slapd + sudo apt-get update + sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils + # Stop system slapd - slapdtest will create its own test instances + sudo systemctl stop slapd + sudo systemctl disable slapd - name: Install dependencies run: make dev envfile From 69bbc00e8971c54d8b0192e7baa9328414833269 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:45:58 +0400 Subject: [PATCH 11/12] Disable AppArmor for slapd Signed-off-by: tdruez --- .github/workflows/run-unit-tests.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index a3836e5c..2687432a 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -54,9 +54,11 @@ jobs: run: | sudo apt-get update sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils - # Stop system slapd - slapdtest will create its own test instances - sudo systemctl stop slapd - sudo systemctl disable slapd + + - name: Disable AppArmor for slapd + run: | + sudo ln -s /etc/apparmor.d/usr.sbin.slapd /etc/apparmor.d/disable/ + sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.slapd - name: Install dependencies run: make dev envfile From 184a814567934f7476512a89f34769296dc655ef Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 29 Oct 2025 13:52:10 +0400 Subject: [PATCH 12/12] Skip test if slapd is not installed Signed-off-by: tdruez --- .github/workflows/run-unit-tests.yml | 8 -------- dje/tests/test_ldap.py | 8 ++++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 2687432a..be0a6662 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -42,14 +42,6 @@ jobs: with: python-version: "3.13" -# - name: Install python-ldap OS dependencies -# run: | -# sudo apt-get update -# # https://github.com/python-ldap/python-ldap/blob/main/.github/workflows/ci.yml -# sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils enchant-2 apparmor-utils -# # Disable AppArmor for slapd to allow slapdtest to create temp instances -# sudo aa-disable /usr/sbin/slapd - - name: Install python-ldap OS dependencies run: | sudo apt-get update diff --git a/dje/tests/test_ldap.py b/dje/tests/test_ldap.py index 1862b3c4..f0234ec4 100644 --- a/dje/tests/test_ldap.py +++ b/dje/tests/test_ldap.py @@ -21,6 +21,8 @@ """ import logging +import shutil +import unittest from django.apps import apps from django.conf import settings @@ -96,6 +98,12 @@ """ +def slapd_available(): + """Check if slapd is available in the system""" + return shutil.which("slapd") is not None + + +@unittest.skipUnless(slapd_available(), "slapd not installed") @override_settings( AUTHENTICATION_BACKENDS=("dje.ldap_backend.DejaCodeLDAPBackend",), AUTH_LDAP_DATASPACE="nexB",