From f9f258f58a355ec826bfed64081b3ef177a3a69b Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:15:44 -0800 Subject: [PATCH 01/13] added a more comprehensive .gitignore --- .gitignore | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 135 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 1e8d101..0c417a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,136 @@ -.coverage -cover -dist -build -fakeldap.egg-info -*.pyc -.tox -*.swp +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg *.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +reports +results.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Development artifacts +.python-version +.DS_Store +/*.sql +config.codekit3 +sql/docker/mysql-data + +# Vim +*.sw* +*.bak +tags + +# Terraform +.terraform +tags +supervisord.pid +requirements.txt.new + +# Ignore the config.codekit3 file -- it changes constantly +config.codekit3 + +# Ignore Visual Studio Code and PyCharm workspace and config +*.code-workspace +.idea +.vscode + +# Local temp files +local_storage + From 869ba0eed87b1eb49fe059eeefe3c49530891075 Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:17:18 -0800 Subject: [PATCH 02/13] Added search_ext and result3 support This allows us to mock paged searches. --- fakeldap.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/fakeldap.py b/fakeldap.py index d274833..c67a7ea 100644 --- a/fakeldap.py +++ b/fakeldap.py @@ -31,6 +31,7 @@ import types from collections import defaultdict import ldap +from ldap.controls import SimplePagedResultsControl logger = logging.getLogger(__name__) @@ -107,6 +108,9 @@ def __init__(self, directory=None): else: self.directory = defaultdict(lambda: {}) + self.cookie = 0 + self._async_results = {} + self.reset() def reset(self): @@ -183,11 +187,62 @@ def simple_bind_s(self, who='', cred=''): return value - def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): - # Hack, cause attributes as a list can't be hashed for storing it - if isinstance(attrlist, list): - attrlist = ', '.join(attrlist) + def search_ext( + self, + base, + scope, + filterstr='(objectClass=*)', + attrlist=None, + attrsonly=0, + serverctrls=None, + clientctrls=None, + timeout=-1, + sizelimit=0 + ): + self._record_call('search_ext', { + 'base': base, + 'scope': scope, + 'filterstr': filterstr, + 'attrlist': attrlist, + 'attrsonly': attrsonly, + 'serverctrls': serverctrls, + 'clientctrls': clientctrls, + 'timeout': timeout, + 'sizelimit': sizelimit + }) + msgid = self.cookie + serverctrls[0].cookie = b'%d' % msgid + self._async_results[self.cookie] = {} + self._async_results[self.cookie]['ctrls'] = serverctrls + value = self._get_return_value('search_ext', (base, scope, filterstr, attrlist, attrsonly)) + if value is None: + value = self._search_s(base, scope, filterstr, attrlist, attrsonly) + self._async_results[self.cookie]['data'] = value + self.cookie += 1 + return msgid + + def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None): + self._record_call('result3', { + 'msgid': msgid, + 'all': all, + 'timeout': timeout, + }) + + if self._async_results: + if msgid == ldap.RES_ANY: + msgid = self._async_results.keys()[0] + if msgid in self._async_results: + data = self._async_results[msgid]['data'] + controls = self._async_results[msgid]['ctrls'] + del self._async_results[msgid] + else: + data = [] + controls[0].cookie = None + + return ldap.RES_SEARCH_RESULT, data, msgid, controls + + def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): self._record_call('search_s', { 'base': base, 'scope': scope, From 3d854fb28a0b74e708439057ff64e4331a00c5ea Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:24:18 -0800 Subject: [PATCH 03/13] PEP8 --- fakeldap.py | 57 +++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/fakeldap.py b/fakeldap.py index c67a7ea..6ed50d9 100644 --- a/fakeldap.py +++ b/fakeldap.py @@ -3,17 +3,17 @@ # Copyright (c) 2009, Peter Sagerson # Copyright (c) 2011, Christo Buschek # 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 @@ -28,7 +28,6 @@ import re import sys import logging -import types from collections import defaultdict import ldap from ldap.controls import SimplePagedResultsControl @@ -64,7 +63,8 @@ class MockLDAP(object): been made, with or without arguments. """ - class PresetReturnRequiredError(Exception): pass + class PresetReturnRequiredError(Exception): + pass SCOPE_BASE = 0 SCOPE_ONELEVEL = 1 @@ -87,7 +87,6 @@ def escape_filter_chars(s): return s escape_filter_chars = staticmethod(escape_filter_chars) - def __init__(self, directory=None): """ directory is a complex structure with the entire contents of the @@ -168,8 +167,10 @@ def initialize(self, uri, trace_level=0, trace_file=sys.stdout, trace_stack_limi 'trace_stack_limit': trace_stack_limit }) - value = self._get_return_value('initialize', - (uri, trace_level, trace_file, trace_stack_limit)) + value = self._get_return_value( + 'initialize', + (uri, trace_level, trace_file, trace_stack_limit) + ) if value is None: value = self @@ -246,12 +247,14 @@ def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attr self._record_call('search_s', { 'base': base, 'scope': scope, - 'filterstr':filterstr, - 'attrlist':attrlist, - 'attrsonly':attrsonly + 'filterstr': filterstr, + 'attrlist': attrlist, + 'attrsonly': attrsonly }) - value = self._get_return_value('search_s', - (base, scope, filterstr, attrlist, attrsonly)) + value = self._get_return_value( + 'search_s', + (base, scope, filterstr, attrlist, attrsonly) + ) if value is None: value = self._search_s(base, scope, filterstr, attrlist, attrsonly) @@ -336,7 +339,7 @@ def _simple_bind_s(self, who='', cred=''): success = True if success: - return (97, []) # python-ldap returns this; I don't know what it means + return (97, []) # python-ldap returns this; I don't know what it means else: raise ldap.INVALID_CREDENTIALS('%s:%s' % (who, cred)) @@ -356,12 +359,12 @@ def _modify_s(self, dn, mod_attrs): for item in mod_attrs: op, key, value = item - if op is 0: + if op == 0: # FIXME: Can't handle multiple entries with the same name # its broken right now # do a MOD_ADD, assume it to be a list of values key.append(value) - elif op is 1: + elif op == 1: # do a MOD_DELETE row = entry[key] if isinstance(row, list): @@ -371,7 +374,7 @@ def _modify_s(self, dn, mod_attrs): else: del entry[key] self.directory[dn] = entry - elif op is 2: + elif op == 2: # do a MOD_REPLACE entry[key] = value @@ -413,8 +416,9 @@ def _search_s(self, base, scope, filterstr, attrlist, attrsonly): if scope == self.SCOPE_BASE: if filterstr != '(objectClass=*)': - raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' % - (base, scope, filterstr, attrlist, attrsonly)) + raise self.PresetReturnRequiredError( + 'search_s("%s", %d, "%s", "%s", %d)' % (base, scope, filterstr, attrlist, attrsonly) + ) attrs = self.directory.get(base) logger.debug("attrs: %s".format(attrs)) if attrs is None: @@ -425,13 +429,15 @@ def _search_s(self, base, scope, filterstr, attrlist, attrsonly): simple_query_regex = r"\(\w+=.+\)$" # matches things like (some_attribute=value) r = re.compile(simple_query_regex) if r.match(filterstr) is None: # only this very simple search is supported - raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' % - (base, scope, filterstr, attrlist, attrsonly)) + raise self.PresetReturnRequiredError( + 'search_s("%s", %d, "%s", "%s", %d)' % (base, scope, filterstr, attrlist, attrsonly) + ) return self._simple_onelevel_search(base, filterstr) else: - raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' % - (base, scope, filterstr, attrlist, attrsonly)) + results = self.directory.get(f'search:{filterstr}', []) + logger.debug("results: %s".format(results)) + return results def _add_s(self, dn, record): # change the record into the proper format for the internal directory @@ -444,7 +450,7 @@ def _add_s(self, dn, record): raise ldap.ALREADY_EXISTS except KeyError: self.directory[dn] = entry - return (105,[], len(self.calls), []) + return (105, [], len(self.calls), []) def _simple_onelevel_search(self, base, filterstr): search_attr_name, search_attr_value = filterstr[1:-1].split('=') @@ -498,4 +504,3 @@ def _get_return_value(self, api_name, arguments): raise value return value - From a39ac468484f5b46f5e7c130edffe3f9f64de833 Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:25:58 -0800 Subject: [PATCH 04/13] When using the argument list as a dict key, serialize the argument list with a bytes-aware JSON encoder This avoids the tuple -> list hacks. --- fakeldap.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/fakeldap.py b/fakeldap.py index 6ed50d9..d52505d 100644 --- a/fakeldap.py +++ b/fakeldap.py @@ -31,11 +31,19 @@ from collections import defaultdict import ldap from ldap.controls import SimplePagedResultsControl +import json logger = logging.getLogger(__name__) +class BytesDump(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, bytes): # deal with bytes + return obj.decode() + return json.JSONEncoder.default(self, obj) # everything else + + class MockLDAP(object): """ This is a stand-in for the python-ldap module; it serves as both the ldap @@ -127,11 +135,9 @@ def set_return_value(self, api_name, arguments, value): Stores a preset return value for a given API with a given set of arguments. """ - # hack, cause lists are not hashable - if isinstance(arguments[1], list): - arguments[1] = tuple(arguments[1]) logger.info("Set value. api_name: %s, arguments: %s, value: %s" % (api_name, arguments, value)) - self.return_value_maps[api_name][arguments] = value + args_str = json.dumps(arguments, cls=BytesDump) + self.return_value_maps[api_name][args_str] = value def ldap_methods_called_with_arguments(self): """ @@ -494,9 +500,10 @@ def _record_call(self, api_name, arguments): self.calls.append((api_name, arguments)) def _get_return_value(self, api_name, arguments): + args_str = json.dumps(arguments, cls=BytesDump) try: - logger.info("api: %s, arguments: %s" % (api_name, arguments)) - value = self.return_value_maps[api_name][arguments] + logger.info("RETURN: api: %s, arguments: %s" % (api_name, arguments)) + value = self.return_value_maps[api_name][args_str] except KeyError: value = None From 44b319697066fb70c2798314cec30f8a69607918 Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:26:36 -0800 Subject: [PATCH 05/13] added unbind_s support --- fakeldap.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fakeldap.py b/fakeldap.py index d52505d..db0b510 100644 --- a/fakeldap.py +++ b/fakeldap.py @@ -332,6 +332,9 @@ def rename_s(self, dn, newdn): return result + def unbind_s(self): + self._record_call('unbind_s', {}) + # # Internal implementations # From 861271ce5e440007a10bb27fbd1e6a3c712b3c39 Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:27:18 -0800 Subject: [PATCH 06/13] rename_s now works how python-ldap's version does --- fakeldap.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/fakeldap.py b/fakeldap.py index db0b510..b45e0e1 100644 --- a/fakeldap.py +++ b/fakeldap.py @@ -320,15 +320,16 @@ def add_s(self, dn, record): return result - def rename_s(self, dn, newdn): + def rename_s(self, dn, newrdn, superior=None): self._record_call('rename_s', { 'dn': dn, - 'newdn': newdn, + 'newrdn': newrdn, + 'superior': superior, }) - result = self._get_return_value('rename_s', (dn, newdn)) + result = self._get_return_value('rename_s', (dn, newrdn, superior)) if result is None: - result = self._rename_s(dn, newdn) + result = self._rename_s(dn, newrdn, superior) return result @@ -391,18 +392,21 @@ def _modify_s(self, dn, mod_attrs): return (103, []) - def _rename_s(self, dn, newdn): + def _rename_s(self, dn, newrdn, superior=None): try: entry = self.directory[dn] except KeyError: raise ldap.NO_SUCH_OBJECT - changes = newdn.split('=') - newfulldn = '%s=%s,%s' % (changes[0], changes[1], - ','.join(dn.split(',')[1:])) + if not superior: + basedn = ','.join(dn.split(',')[1:]) + else: + basedn = superior + newdn = newrdn + ',' + basedn + attr, value = newrdn.split('=') - entry[changes[0]] = changes[1] - self.directory[newfulldn] = entry + entry[attr] = value + self.directory[newdn] = entry del self.directory[dn] return (109, []) From 72e810947ddcd75205f0a89b67adec5c36cf3c52 Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:28:51 -0800 Subject: [PATCH 07/13] added a bit of extra logging to log calls This allows you to debug exactly what your call arguments are so you can set up your mock data more accurately --- fakeldap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fakeldap.py b/fakeldap.py index b45e0e1..4987a6c 100644 --- a/fakeldap.py +++ b/fakeldap.py @@ -504,6 +504,7 @@ def _mangle_record(self, record): return new_record def _record_call(self, api_name, arguments): + logger.info("CALL: api: %s, arguments: %s" % (api_name, arguments)) self.calls.append((api_name, arguments)) def _get_return_value(self, api_name, arguments): From 9c421c441af45e6559570903aa6cfa446378c65f Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:30:54 -0800 Subject: [PATCH 08/13] more pep8 --- setup.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 1f4027a..f36d62e 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,44 @@ from setuptools import setup, find_packages import os -import sys from version import __version__ + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() + extra = {} -requirements = ['python-ldap'], -tests_require = ['nose', 'Mock', 'coverage', 'unittest2', 'python-ldap'] +requirements = [ + 'python-ldap' +], +tests_require = [ + 'nose', + 'Mock', + 'coverage', + 'unittest2', + 'python-ldap' +] setup( - name = "fakeldap", - version = __version__, - #packages = find_packages('fakeldap'), + name="fakeldap", + version=__version__, + #packages=find_packages('fakeldap'), #include_package_data=True, - py_modules = ['fakeldap'], - install_requires = requirements, + py_modules=['fakeldap'], + install_requires=requirements, tests_require=tests_require, setup_requires='nose', - test_suite = "nose.collector", + test_suite="nose.collector", extras_require={'test': tests_require}, - author = "Christo Buschek", - author_email = "crito@30loops.net", - url = "https://github.com/zulip/fakeldap", - description = "An implementation of a LDAPObject to fake a ldap server in unittests.", - long_description = read('README.rst'), - classifiers = [ + author="Christo Buschek", + author_email="crito@30loops.net", + url="https://github.com/zulip/fakeldap", + description="An implementation of a LDAPObject to fake a ldap server in unittests.", + long_description=read('README.rst'), + classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', From 800bdf052c273c31f5abad75feac1416f85c8843 Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:31:16 -0800 Subject: [PATCH 09/13] added some development dependencies Mostly linters and nose. --- requirements.dev.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 requirements.dev.txt diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..23ce926 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,10 @@ +# Development +# ------------------------------------------------------------------------------ +autopep8==1.5 # https://github.com/hhatto/autopep8 +flake8==3.7.9 # https://github.com/PyCQA/flake8 +pycodestyle==2.5.0 # https://github.com/PyCQA/pycodestyle +mypy==0.701 # https://github.com/python/mypy + +# Testing +# ------------------------------------------------------------------------------ +nose==1.3.7 # https://github.com/nose-devs/nose From 26875573bb58d170cb41571969e4840f35b89292 Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:34:07 -0800 Subject: [PATCH 10/13] setup.cfg: added flake8 config and mypy config --- setup.cfg | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/setup.cfg b/setup.cfg index aa41da2..dee770d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,17 @@ +[flake8] +max-line-length: 120 +filename: *.py +exclude: *.cfg, *.js, *.json, *.bak, *.md, *.sql, *.sh, *.txt, *.yml, simple_test_db, Makefile, Dockerfile, MANIFEST.in +# E221: multiple spaces before operator +# E241: multiple spaces after : +# E265: block comment should start with '# ' +# E266: too many leading '#' for block comment +# E401: multiple imports on one line +ignore = E221,E241,E265,E266,E401 + +[mypy] +python_executable: ~/.pyenv/shims/python + [nosetests] with-coverage = true cover-package = fakeldap From 21a7f74050ff58bc76e7fba03f1635fb1970664d Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Fri, 15 Jan 2021 09:34:17 -0800 Subject: [PATCH 11/13] more pep8 --- tests.py | 89 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/tests.py b/tests.py index ab99281..f026cd8 100644 --- a/tests.py +++ b/tests.py @@ -7,8 +7,8 @@ directory = { "cn=admin,dc=30loops,dc=net": { - "userPassword": "ldaptest" - } + "userPassword": "ldaptest" + } } @@ -23,7 +23,7 @@ def test_simple_bind_s_operation(self): """Try to bind a user.""" # Make a valid bind eq_( - (97,[]), + (97, []), self.mock_ldap.simple_bind_s("cn=admin,dc=30loops,dc=net", "ldaptest") ) @@ -37,27 +37,27 @@ def test_simple_bind_s_operation(self): def test_add_s_operation(self): """Test the addition of records to the mock ldap object.""" record = [ - ('uid', 'crito'), - ('userPassword', 'secret'), - ] - eq_((105,[],1,[]), self.mock_ldap.add_s( - "uid=crito,ou=people,dc=30loops,dc=net", record - )) + ('uid', 'crito'), + ('userPassword', 'secret'), + ] + eq_((105, [], 1, []), self.mock_ldap.add_s( + "uid=crito,ou=people,dc=30loops,dc=net", record + )) directory = { - "cn=admin,dc=30loops,dc=net": {"userPassword": "ldaptest"}, - "uid=crito,ou=people,dc=30loops,dc=net": { - "uid": "crito", "userPassword": "secret"} - } + "cn=admin,dc=30loops,dc=net": {"userPassword": "ldaptest"}, + "uid=crito,ou=people,dc=30loops,dc=net": { + "uid": "crito", "userPassword": "secret"} + } eq_(directory, self.mock_ldap.directory) record = [ - ('uid', 'bas'), - ('userPassword', 'secret'), - ] - eq_((105,[],2,[]), self.mock_ldap.add_s( - "uid=bas,ou=people,dc=30loops,dc=net", record - )) + ('uid', 'bas'), + ('userPassword', 'secret'), + ] + eq_((105, [], 2, []), self.mock_ldap.add_s( + "uid=bas,ou=people,dc=30loops,dc=net", record + )) def test_search_s_base(self): result = self.mock_ldap.search_s("cn=admin,dc=30loops,dc=net", ldap.SCOPE_BASE) @@ -65,41 +65,50 @@ def test_search_s_base(self): def test_search_s_onelevel(self): directory = { - "ou=users,dc=30loops,dc=net": { "ou": "users" }, + "ou=users,dc=30loops,dc=net": {"ou": "users"}, "cn=admin,ou=users,dc=30loops,dc=net": { - "userPassword": "ldaptest" - }, + "userPassword": "ldaptest" + }, "cn=john,ou=users,dc=30loops,dc=net": { - "userPassword": "ldaptest", - "mail": "john@example.com" - }, + "userPassword": "ldaptest", + "mail": "john@example.com" + }, "cn=jack,ou=users,dc=30loops,dc=net": { - # test [value, ] format here - "userPassword": ["ldaptest", ], - "mail": ["jack@example.com", ] - }, + # test [value, ] format here + "userPassword": ["ldaptest", ], + "mail": ["jack@example.com", ] + }, "cn=john2,ou=users,dc=30loops,dc=net": { - "userPassword": "ldaptest", - "mail": "john@example.com" # same mail as john - } + "userPassword": "ldaptest", + "mail": "john@example.com" # same mail as john + } } self.mock_ldap = MockLDAP(directory) - result = self.mock_ldap.search_s("dc=30loops,dc=net", ldap.SCOPE_ONELEVEL, - "(mail=jack@example.com)") + result = self.mock_ldap.search_s( + "dc=30loops,dc=net", + ldap.SCOPE_ONELEVEL, + "(mail=jack@example.com)" + ) # The search is one-level, so the above should return no results: self.assertEqual(result, []) - result = self.mock_ldap.search_s("ou=users,dc=30loops,dc=net", ldap.SCOPE_ONELEVEL, - "(mail=jack@example.com)") + result = self.mock_ldap.search_s( + "ou=users,dc=30loops,dc=net", + ldap.SCOPE_ONELEVEL, + "(mail=jack@example.com)" + ) self.assertEqual( result, [('cn=jack,ou=users,dc=30loops,dc=net', - {'userPassword': ['ldaptest'], 'mail': ['jack@example.com']})] + {'userPassword': ['ldaptest'], 'mail': ['jack@example.com']})] ) - result = self.mock_ldap.search_s("ou=users,dc=30loops,dc=net", ldap.SCOPE_ONELEVEL, - "(mail=john@example.com)") + result = self.mock_ldap.search_s( + "ou=users,dc=30loops,dc=net", + ldap.SCOPE_ONELEVEL, + "(mail=john@example.com)" + ) self.assertEqual(len(result), 2) self.assertIn( ('cn=john,ou=users,dc=30loops,dc=net', {'userPassword': 'ldaptest', 'mail': 'john@example.com'}), @@ -111,5 +120,5 @@ def test_search_s_onelevel(self): ) result = self.mock_ldap.search_s("dc=30loops,dc=net", ldap.SCOPE_ONELEVEL, - "(mail=nonexistant@example.com)") + "(mail=nonexistant@example.com)") self.assertEqual(result, []) From 300407e4b3a2f21f5c649a685ee09009f9b85c23 Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Tue, 2 Feb 2021 16:04:37 -0800 Subject: [PATCH 12/13] BUGFIX: rename_s should no longer delete the renamed object --- README.rst | 27 +++++++++++++++++---------- fakeldap.py | 6 ++++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index bff041a..b350b3a 100644 --- a/README.rst +++ b/README.rst @@ -17,9 +17,13 @@ taken from Peter Sagerson's excellent django-auth-ldap_ module. Installation ============ -Get and install the code:: +To install from PyPI:: - $ git clone git://github.com/30loops/fakeldap.git + $ pip install fakeldap + +To install via ``setup.py``:: + + $ git clone git://github.com/zulip/fakeldap.git $ cd fakeldap $ python setup.py install @@ -34,14 +38,14 @@ Usage This code is still experimental and not very tested as of yet. So is the documentation - + The ``MockLDAP`` class replaces the ``LDAPObject`` of the python-ldap module. The easiest way to use it, is to overwrite ``ldap.initialize`` to return -``MockLDAP`` instead of ``LDAPObject``. The example below uses Michael Foord's +``MockLDAP`` instead of ``LDAPObject``. The example below uses Python 3's Mock_ library to achieve that:: import unittest - from mock import patch + from unittest.mock import patch from fakeldap import MockLDAP @@ -60,13 +64,16 @@ Mock_ library to achieve that:: The mock ldap object implements the following ldap operations: -- simple_bind_s -- search_s +- add_s - compare_s -- modify_s - delete_s -- add_s +- modify_s - rename_s +- result3 +- search_ext +- search_s +- simple_bind_s +- unbind_s This is an example how to use ``MockLDAP`` with fixed return values:: @@ -121,7 +128,7 @@ ldap server with a directory of entries:: "userPassword": "ldaptest" } } - mock_ldap = MockLDAP(tree) + mock_ldap = MockLDAP(tree) record = [ ('uid', 'crito'), diff --git a/fakeldap.py b/fakeldap.py index 4987a6c..66af827 100644 --- a/fakeldap.py +++ b/fakeldap.py @@ -25,13 +25,15 @@ # 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. +from copy import deepcopy import re import sys import logging from collections import defaultdict +import json import ldap from ldap.controls import SimplePagedResultsControl -import json +import pprint logger = logging.getLogger(__name__) @@ -394,7 +396,7 @@ def _modify_s(self, dn, mod_attrs): def _rename_s(self, dn, newrdn, superior=None): try: - entry = self.directory[dn] + entry = deepcopy(self.directory[dn]) except KeyError: raise ldap.NO_SUCH_OBJECT From 9de9e92a6138b59a0589e148b452bd1c4c1cc69a Mon Sep 17 00:00:00 2001 From: Chris Malek Date: Thu, 11 Feb 2021 10:22:59 -0800 Subject: [PATCH 13/13] UPDATE: log _search_s results with more clarity --- fakeldap.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fakeldap.py b/fakeldap.py index 66af827..c0ad5ec 100644 --- a/fakeldap.py +++ b/fakeldap.py @@ -450,8 +450,9 @@ def _search_s(self, base, scope, filterstr, attrlist, attrsonly): return self._simple_onelevel_search(base, filterstr) else: - results = self.directory.get(f'search:{filterstr}', []) - logger.debug("results: %s".format(results)) + key = f'search:{filterstr}' + results = self.directory.get(key, []) + logger.debug(f"_search_s.results('{key}'): {results}") return results def _add_s(self, dn, record):