python-ldap-faker
Current version is 1.1.0.
This package provides a fake python-ldap
interface that can be used for
automated testing of code that uses python-ldap
. With python-ldap-faker
you will be able to test your LDAP code without having to stand up an actual
LDAP server, and also without having to use complicated
unittest.mock.patch
and unittest.mock.Mock
setups.
When writing tests for code that talks to an LDAP server with python-ldap
, we
want to be able to control python-ldap
interactions in our tests to ensure
that our own code works properly. This may include populating the LDAP server
with fixture data, monitoring if, when and how python-ldap
calls are made by
our code, and ensuring our code handles python-ldap
exceptions properly.
Managing an actual LDAP server during our tests is usually out of the question,
so typically we revert to patching the python-ldap
code to use mock objects
instead, but this is very verbose and can lead to test code errors in practice.
This package provides replacement ldap.initialize
,
ldap.set_option
and ldap.get_option
functions, as well as
a test-instrumented ldap.ldap.ldapobject.LDAPObject
replacement.
Installation
To install from PyPI:
pip install python-ldap-faker
If you want, you can run the tests:
python -m unittest discover
Features:
These
python-ldap
global functions are faked:ldap.initialize
ldap.set_option
ldap.get_option
These
ldap.ldapobject.LDAPObject
methods are faked:set_option
get_option
start_tls_s
simple_bind_s
unbind_s
search_s
search_ext
result3
compare_s
add_s
modify_s
rename_s
delete_s
For
search_ext
andsearch_s
, your filter string will be validated as a valid LDAP filter, and your filter will be applied directly to your objects in our fake “server” to generate the result list. No canned searches!Inspect your call history for all calls (name, arguments), and test the order in which they were made
Simulate multiple fake LDAP “servers” with different sets of objects that correspond to different LDAP URIs.
Ease your test setup with
LDAPFakerMixin
, a mixin forunittest.TestCase
Automatically manages patching
python-ldap
for the code under testPopulate objects into one or more LDAP “servers” with fixture files
Provides the following test instrumentation for inspecting state after the test:
Access to the full object store for each LDAP uri accessed
All connections made
All
python-ldap
API calls madeAll
python-ldap
LDAP options set
Provides test isolation: object store changes, connections, call history, option changes are all reset between tests
Use handy LDAP specific asserts to ease your testing
Define your own hooks to change the behavior of your fake “servers”
Support behavior for specific LDAP implementations:
Redhat Directory Server/389 implementation support: have your test believe it’s talking to an RHDS/389 server.
Quickstart
The easiest way to use python-ldap-faker
in your unittest
based
tests is to use the LDAPFakerMixin
mixin for
unittest.TestCase
.
This will patch ldap.initialize
, ldap.set_option
and
ldap.get_option
to use our FakeLDAP
interface, and load
fixtures in from JSON files to use as test data.
Let’s say we have a class App
in our myapp
module that does LDAP work
that we want to test.
First, prepare a file named data.json
with the objects you want loaded into
your fake LDAP server. Let’s say you want your data to consist of some
posixAccount
objects. If we make data.json
look like this:
[
[
"uid=foo,ou=bar,o=baz,c=country",
{
"uid": ["foo"],
"cn": ["Foo Bar"],
"uidNumber": ["123"],
"gidNumber": ["123"],
"homeDirectory": ["/home/foo"],
"userPassword": ["the password"],
"objectclass": [
"posixAccount",
"top"
]
}
],
[
"uid=fred,ou=bar,o=baz,c=country",
{
"uid": ["fred"],
"cn": ["Fred Flintstone"],
"uidNumber": ["124"],
"gidNumber": ["124"],
"homeDirectory": ["/home/fred"],
"userPassword": ["the fredpassword"],
"objectclass": [
"posixAccount",
"top"
]
}
],
[
"uid=barney,ou=bar,o=baz,c=country",
{
"uid": ["barney"],
"cn": ["Barney Rubble"],
"uidNumber": ["125"],
"gidNumber": ["125"],
"homeDirectory": ["/home/barney"],
"userPassword": ["the barneypassword"],
"objectclass": [
"posixAccount",
"top"
]
}
]
]
We can write our TestCase
like so:
import unittest
import ldap
from ldap_faker import LDAPFakerMixin
from myapp import App
class YourTestCase(LDAPFakerMixin, unittest.TestCase):
ldap_modules = ['myapp']
ldap_fixtures = 'data.json'
def test_auth_works(self):
app = App()
# A method that does a `simple_bind_s`
app.auth('fred', 'the fredpassword')
conn = self.get_connections()[0]
self.assertLDAPConnectionMethodCalled(
conn, 'simple_bind_s',
{'who': 'uid=fred,ou=bar,o=baz,c=country', 'cred': 'the fredpassword'}
)
def test_correct_connection_options_were_set(self):
app = App()
app.auth('fred', 'the fredpassword')
conn = self.get_connections()[0]
self.assertLDAPConnectionOptionSet(conn, ldap.OPT_X_TLX_NEWCTX, 0)
def test_tls_was_used_before_auth(self):
app = App()
app.auth('fred', 'the fredpassword')
conn = self.get_connections()[0]
self.assertLDAPConnectiontMethodCalled(conn, 'start_tls_s')
self.assertLDAPConnectionMethodCalledAfter(conn, 'simple_bind_s', 'start_tls_s')
Faking LDAP servers
python-ldap-faker
stores all LDAP objects in a fake LDAP “server”
class: ObjectStore
, and all our fake python-ldap
methods
operate on the LDAP objects in that object store via the exposed methods
on ObjectStore
.
You won’t typically use ObjectStore
directly, but instead you’ll use
LDAPServerFactory
to register ObjectStore
objects to
correspond to specific LDAP URIs (e.g. ldap://server.example.com
). Our
main fake python-ldap
interface class FakeLDAP
uses the
LDAPServerFactory
to assign the correct ObjectStore
when
FakeLDAP.initialize
is called by our code under test.
Structure of LDAP records
python-ldap-faker
tries to pretend it is python-ldap
as much as
possible. Important to this is to mimic how python-ldap
and LDAP servers
represent LDAP objects.
LDAP objects have these characteristics:
The primary key for an LDAP object is the
dn
. Thedn
is case-insensitive in allpython-ldap
methods. For example, these two statements should operate on the same object:ldap_obj.simple_bind_s("uid=foo,ou=bar,o=baz,c=country", "the password") ldap_obj.simple_bind_s("UID=FOO,OU=BAR,O=BAZ,C=COUNTRY", "the password")
Simliarly,
basedn
, wherever required, is case-insensitive.When doing searches (
search_s
,search_ext
), LDAP object attributes and values are compared case-insensitively. These searches should all return the same set of objects:ldap_obj.search_s("ou=bar,o=baz,c=country", ldap.SCOPE_SUBTREE, '(uid=bar)') ldap_obj.search_s("ou=bar,o=baz,c=country", ldap.SCOPE_SUBTREE, '(UID=bar)') ldap_obj.search_s("ou=bar,o=baz,c=country", ldap.SCOPE_SUBTREE, '(uid=bAr)')
LDAP objects returned by
ldap.search_s
have this type:Tuple[str, Dict[str, List[bytes]]
. and this structure:('the dn', {'attribute1': [b'value1', b'value2'], ...})
LDAPServerFactory
LDAPServerFactory
objects allow you to register
ObjectStore
bound to particular LDAP URIs so that when someone uses
our FakeLDAP.initialize
method, it gets properly instrumented with a
copy of the ObjectStore
from the LDAPServerFactory
.
FakeLDAP
takes a fully loaded LDAPServerFactory
object
as a constructor object.
Note
Note that we said a copy of the ObjectStore
. Since the primary use of
python-ldap-faker
is in testing, and we want to ensure good test
isolation, we should start each test with a fresh copy of original
ObjectStore
data for our LDAP URI so that we can ensure that any
modifications to that data came only from our code under test.
ObjectStore
The core of python-ldap-faker
is the ObjectStore
class. This
behaves as the LDAP “server” with which our fake python-ldap
interface
interacts. In order to do meaningful work with it, it needs to be loaded with
LDAP objects. There are three methods on ObjectStore
that
you can use to load your objects:
ObjectStore.register_object
: load a single object into the object storeObjectStore.register_objects
: load a list of objects into the object storeObjectStore.load_objects
: load a list of objects from a JSON file into the object store
Once loaded into ObjectStore
, we make a fully case-insensitive
internal-only copy of the object (stored in ObjectStore.objects
for
use in executing searches, but the data returned will be the case-sensitive
versions of those objects (the case-sensitive versions are stored in
ObjectStore.raw_objects
).
Data Types for ObjectStore.register_object(s)
Each object loaded into ObjectStore.register_object
or
ObjectStore.register_objects
must be of this type:
- ldap_faker.types.LDAPRecord
The central part of internal API.
This represents a generic version of type ‘origin’ with type arguments ‘params’. There are two kind of these aliases: user defined and special. The special ones are wrappers around builtin collections and ABCs in collections.abc. These must have ‘name’ always set. If ‘inst’ is False, then the alias can’t be instantiated, this is used by e.g. typing.List and typing.Dict.
Example:
(
'uid=user,ou=mydept,o=myorg,c=country',
{
'cn': [b'Firstname User1'],
'uid': [b'user'],
'uidNumber': [b'123'],
'gidNumber': [b'456'],
'homeDirectory': [b'/home/user'],
'loginShell': [b'/bin/bash'],
'userPassword': [b'the password'],
'objectclass': [b'posixAccount', b'top']
}
)
Thus:
dn
is astr
Attribute names are
str
Attribute values are
List[bytes]
File format for ObjectStore.load_objects
Unfortunately, JSON has neither a Tuple
type nor a bytes
type, so we
need to use lists and strings instead, and convert them to the appropriate types
after reading the JSON file. Thus in our JSON files, we must provide our data
as List[List[str, Dict[str, List[str]]]]
instead. Example:
[
[
'uid=foo,ou=bar,o=baz,c=country',
{
"uid": ["foo"],
"cn": ["Foo Bar"],
"uidNumer": ["123"],
"gidNumer": ["123"],
"homeDirectory": ["/home/foo"],
"userPassword": ["the password"],
"ojectclass": [
"posixAccount",
"top"
]
}
]
]
If you structure your file of LDAP objects like that, and pass in the filename
to ObjectStore
, we’ll load the data from the file and convert that
struct to List[Tuple[str, List[bytes]]]
before using the result with
ObjectStore.register_objects
.`
Specific LDAP implementations supported
Out of the box, our “server” class ObjectStore
supports searching,
adding, updating and deleting objects like a regular LDAP server.
Real LDAP implementations (Redhat Directory Server, 389, openldap, Active Directory) can have special behavior and side-effects that you may need to support in order to run your tests properly.
Currently, we support some special behavior for one implementation: Redhat Directory Server/389.
Redhat Directory Server/389
To get these behaviors, add the 389
tag to your ObjectStore
:
>>> store = ObjectStore(tags=['389'])
In LDAPFakerMixin
, apply the tags with like this for a single, default server:
import unittest
from ldap_faker import LDAPFakerMixin
class TestDefaultTaggedServer(LDAPFakerMixin, unittest.TestCase):
ldap_modules = ['myapp']
ldap_fixtures = ('data.json', ['389'])
Or like this for a named server:
import unittest
from ldap_faker import LDAPFakerMixin
class TestDefaultTaggedServer(LDAPFakerMixin, unittest.TestCase):
ldap_modules = ['myapp']
ldap_fixtures = [
('server1.json', 'ldap://server1', ['389']),
]
Features supported
Operational attributes
entryid
nsUniqueId
entrydn
createTimestamp
modifyTimestamp
creatorName
modifierName
These work like they should in RHDS/389. They are not returned unless specifically asked for during searches, and they are read-only. The timestamps and names will be updated automatically.
nsrole
and nsroledn
User objects support the
nsroledn
(writeable) andnsrole
(read-only) attributes. Adding a DN tonsroledn
makes it appear automatically innsrole
, and any objects with`objectClass
ofldapsubentry
will affectnsrole
as it does in RHDS/389.
nsrole
andnsroledn
are operational attributes; they must be specifically requested during searches.Important
In RHDS/389, users do not seem to be identified by objectclass. We’re simulating this by assuming that any object with a
userPassword
attribute on it is a user.
ldapsubentries
The three
ldapsubentry
objectclasses are supported and behave as they do in RHDS/389:
nsManagedRoleDefinition
: does nothing when added or removed
nsNestedRoleDefinition
: user objects will gain the proper DN if they match one of this object’snsroledn
entries.
nsFilteredRoleDefinition
: user objects will gain the proper DN if they match this object’snsRoleFilter
.
Using ldap_faker with unittest
Most of the purpose of python-ldap-faker
is to make automated testing
of code that uses python-ldap
easier.
To this end, python-ldap-faker
provides LDAPFakerMixin
, a mixin class
for unittest.TestCase
which handles all the hard work of patching
and instrumenting the appropriate python-ldap
functions, objects and
methods.
LDAPFakerMixin
will do the following things for you:
Read data from JSON fixture files to populate one or more
ObjectStore
objects (our fake LDAP server class)Associate those
ObjectStore
objects with particular LDAP URIsPatch
ldap.initialize
to returnFakeLDAPObject
objects configured with the appropriateObjectStore
for the LDAP URI passed intoFakeLDAP.initialize
Configuring your LDAPFakerMixin TestCase
We need to set two class attributes on LDAPFakerMixin
in order for
it to properly set up your tests:
LDAPFakerMixin.ldap_modules
: The list of your code’s modules in which to patchldap.initialize
,ldap.set_option
andldap.get_option`
LDAPFakerMixin.ldap_fixtures
: A list of JSON fixture files with which to create theObjectStore
objects
LDAPFakerMixin.ldap_modules
LDAPFakerMixin
uses unittest.mock.patch
to patch your
code so that it uses our fake versions of ldap.initialize
,
ldap.set_option
and ldap.get_option
instead of the real
one. The way patch
works is that it must apply the patch within the context
of your module that does import ldap
, not within the ldap
module itself.
Thus, to make LDAPFakerMixin
work for you, you must list all the
modules for code under test in which you do import ldap
.
To list all the modules in which the code under test does import ldap
, use
the LDAPFakerMixin.ldap_modules
class attribute.
For example, if you have a class MyLDAPUsingClass
in the module
myapp.myldapstuff
, and you do import ldap
in myapp.myldapstuff
, for
instance:
import ldap
class MyLDAPUsingClass:
def connect(self, uid: str, password: str):
self.conn = ldap.initialize('ldap://server')
self.conn.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
self.conn.start_tls_s()
self.conn.simple_bind_s(
f'uid={uid},ou=bar,o=baz,c=country',
'the password'
)
To test this code, you would use this for ldap_modules
:
import unittest
from ldap_faker import LDAPFakerMixin
from myapp.myldapstuff import MyLDAPUsingClass
class TestMyLDAPUsingCLass(LDAPFakerMixin, unittest.TestCase):
ldap_modules = ['myapp.myldapstuff']
LDAPFakerMixin.ldap_fixtures
In order to effectively test your python-ldap
using code, you’ll need to
populate an LDAPServerFactory
one or more ObjectStore
objects bound to LDAP URIs. We use LDAPFakerMixin.ldap_fixtures
to
declare file paths to fixture files to use to populate those
ObjectClass
objects.
Fixture files are JSON files in the format described in File format for ObjectStore.load_objects.
File paths are either absolute paths or are treated as relative to the folder in which your
TestCase
resides.Fixtures are loaded into the
LDAPServerFactory
once perunittest.TestCase
via theunittest.TestCase.setUpClass
classmethod.
You can configure your LDAPFakerMixin
to use fixtures one of two ways:
Use a single default fixture that will be used no matter which LDAP URI is passed to
FakeLDAP.initialize
Bind each fixture to specific a LDAP URI. This allows you simulate talking to several different LDAP servers.
Note
When binding fixtures to particular LDAP URIs, if your tries to use
FakeLDAP.initialize
with an LDAP URI that was not explicitly configured,
python-ldap-faker
will raise ldap.SERVER_DOWN
This form sets up one default fixture:
import unittest
from ldap_faker import LDAPFakerMixin
from myapp.myldapstuff import MyLDAPUsingClass
class TestMyLDAPUsingCLass(LDAPFakerMixin, unittest.TestCase):
ldap_fixtures = 'objects.json'
This form binds fixtures to LDAP URIs:
import unittest
from ldap_faker import LDAPFakerMixin
from myapp.myldapstuff import MyLDAPUsingClass
class TestMyLDAPUsingCLass(LDAPFakerMixin, unittest.TestCase):
ldap_fixtures = [
('server1.json', 'ldap://server1.example.com'),
('server2.json', 'ldap://server2.example.com')
]
Test isolation
Each test method on your unittest.TestCase
will get a fresh, unaltered
copy of the fixture data, and connections, call histories, options set from previous
test methods will be cleared.
Test support offered by LDAPFakerMixin
For each test you run, your test will have access to the FakeLDAP
instance used for that test through the LDAPFakerMixin.fake_ldap
instance attribute. Each test gets a fresh FakeLDAP
instance.
Note
For detailed information on any of the below, see the Developer Interface.
Some things to know about your FakeLDAP
instance:
FakeLDAP.connections
lists all theFakeLDAPObject
connections created during your test method, in the order they were made. One such object is created each timeFakeLDAP.initialize
is called by your code.FakeLDAP.options
is aOptionStore
object that records all the global LDAP options set during your testFakeLDAP.calls
is aCallHistory
object that records calls (with arguments) toFakeLDAP.initialize
,FakeLDAP.set_option
,FakeLDAP.get_option
Some things to know about the FakeLDAPObject
objects in
FakeLDAP.connections
:
FakeLDAPObject.uri
is the LDAP URI requestedFakeLDAPObject.store
is ourObjectStore
copyFakeLDAP.options
is aOptionStore
object that records all the LDAP options set on this connection during your test methodFakeLDAPObject.calls
is aCallHistory
that records allpython-ldap
api calls (with arguments) that your code made to thisFakeLDAPObject
FakeLDAPObject.bound_dn
is thedn
of the user bound viasimple_bind_s
, if any. If this isNone
, we did anonymous binding.FakeLDAPObject.tls_enabled
will be set toTrue
ifstart_tls_s
was used on this connection
Hooks: modifying ObjectStore behavior
python-ldap-faker
provides a hook system to allow you to arbitrarily modify
behavior of ObjectStore
. Primarily this is provided so that you can
emulate the behavior of the various LDAP implementations (Redhat Directory
Server, Active Directory, openldap, etc.).
You can also use hooks in your test code to produce behavior that may not be
available out of the box from python-ldap-faker
.
Rules about hooks:
Hooks are run in the order they are registered
Each hook needs a callable with a particular signature
Hooks are global – they apply to all
ObjectStore
instances and instances instantiated (unless they are tagged hooks)
Registering hooks
Hooks have a name and a callable signature. Here is an example of registering a
hook to the pre_set
hook, which will be run in ObjectStore.set
before the object is saved to the internal storage, and requires the callable
signature Callable[[ObjectStore, LDAPRecord, Optional[str]], None]
:
from ldap_faker import hooks, ObjectStore, LDAPRecord
def pre_set_do_something_special(store: ObjectStore, record: LDAPRecord, bind_dn: str = None) -> None:
...
hooks.register('pre_set', pre_set_do_something_special)
Thereafter, whenever any code calls ObjectStore.set
, this function
will be called with the store as the first argument, the record to be written as
the second argument and the bind_dn
of the binding user as the third
argument.
Tagged hooks
Using tags, you can register a hook that will only apply to
ObjectStore
instances which are themselves tagged with one of those
tags:
from ldap_faker import hooks, ObjectStore, LDAPRecord
def pre_set_do_something_special(store: ObjectStore, record: LDAPRecord, bind_dn: str = None) -> None:
print(f'{bind_dn} ran pre_set_do_something_sepcial')
hooks.register('pre_set', pre_set_do_something_special, tags=['special'])
This hook will only be executed for ObjectStore
instances whose tags
include special
:
>>> store = ObjectStore(tags=['special'])
>>> obj = ('mydn', {'objectclass': [b'top']))
>>> store.set(obj, bind_dn='auser')
auser ran pre_set_do_something_special
It will not be executed for ObjectStore
instances whose tags do not
include special:
>>> store = ObjectStore(tags=['other'])
>>> obj = ('mydn', {'objectclass': [b'top']))
>>> store.set(obj, bind_dn='auser')
Tagging ObjectClass instances in LDAPFakerMixin
When using LDAPFakerMixin
, you can tag ldap_fixtures
with particular tags.
To tag the default “server”, specify the fixture as a 2-tuple, where the first element is the filename of the fixture file, and the second element is a list of tags:
import unittest
from ldap_faker import LDAPFakerMixin
class TestDefaultTaggedServer(LDAPFakerMixin, unittest.TestCase):
ldap_modules = ['myapp']
ldap_fixtures = ('data.json', ['special'])
To tag named “servers”, you can tag individual servers by providing a 3-tuple instad of a 2-tuple, where the third element is the list of tags:
import unittest
from ldap_faker import LDAPFakerMixin
class TestDefaultTaggedServer(LDAPFakerMixin, unittest.TestCase):
ldap_modules = ['myapp']
ldap_fixtures = [
('server1.json', 'ldap://server1', ['special']),
('server2.json', 'ldap://server2')
]
Above, ldap://server1
will use all hooks tagged with special
in addition
to any untagged hooks, while ldap://server2
will use only the untagged
hooks.
Available hooks
pre_objectstore_init
Signature:
Callable[[store: ObjectStore], None]
Where
store
is theObjectStore
object.This will be at the end of
ObjectStore.__init__
.You can use this to set up any state you might need for later hooks by adding keys to
ObjectStore.controls
, or to add attributes toObjectStore.operational_attributes
.pre_set
Signature:
Callable[[store: ObjectStore, record: LDAPRecord, bind_dn: Optional[str] = None], None]
Where
store
is theObjectStore
object,record
is the record to beset
andbind_dn
is the dn of the user doing theset
(possiblyNone
)This will be executed on
ObjectStore.set
before the object actually gets saved.ObjectStore.set
is called for every write operation:post_set
Signature:
Callable[[store: ObjectStore, record: LDAPRecord, bind_dn: Optional[str] = None], None]
Where
store
is theObjectStore
object,record
is the record to beset
andbind_dn
is the dn of the user doing theset
(possiblyNone
).This will be executed on
ObjectStore.set
after the object gets saved.pre_copy
Signature:
Callable[[store: ObjectStore, dn: str], None]
Where
store
is theObjectStore
object, anddn
is the DN of the object to copy.This will be executed on
ObjectStore.copy
before the object actually gets retrieved from the store to be copied.post_copy
Signature:
Callable[[store: ObjectStore, data: LDAPData], LDAPData]
Where
store
is theObjectStore
object, anddn
is the DN of the object to copy. It should return the modifiedLDAPData
dict.This will be executed on
ObjectStore.copy
after the object is retrieved from the store and :py:func:copy.deepcopy
has run, but before returning the data to the caller.pre_create
Signature:
Callable[[store: ObjectStore, dn: str, modlist: AddModlist, bind_dn: str = None], None]
Where
store
is theObjectStore
object,dn
is the record to be created,modlist
is modlist to be used for creating the record, andbind_dn
is the dn of the user doing thecreate
(possiblyNone
).This will be executed on
ObjectStore.create
before the modlist gets processed.ObjectStore.create
is what actually does the work whenFakeLDAPObject.add_s
is called.post_create
Signature:
Callable[[store: ObjectStore, record: LDAPRecord, bind_dn: Optional[str] = None], None]
Where
store
is theObjectStore
object,record
is the record to be created, andbind_dn
is the dn of the user doing thecreate
(possiblyNone
).This will be executed on
ObjectStore.create
after the modlist has processed to build the object, but before it has been writen to the data store.pre_update
Signature:
Callable[[store: ObjectStore, dn: str, modlist: Modlist, bind_dn: str = None], None]
Where
store
is theObjectStore
object,dn
is the record to be modified`,modlist
is modlist to be applied to the record, andbind_dn
is the dn of the user doing theupdate
(possiblyNone
).This will be executed on
ObjectStore.update
before the object actually gets saved.ObjectStore.update
is what actually does the work whenFakeLDAPObject.modify_s
is called.post_update
Signature:
Callable[[store: ObjectStore, record: LDAPRecord, bind_dn: Optional[str] = None], None]
Where
store
is theObjectStore
object,record
is the updated record andbind_dn
is the dn of the user doing theupdate
(possiblyNone
)This will be executed on
ObjectStore.update
after the modlist has been applied to the object, but before it has been writen to the data store.pre_delete
Signature:
Callable[[store: ObjectStore, record: LDAPRecord, bind_dn: Optional[str] = None], None]
Where
store
is theObjectStore
object,record
is the record to deleted, andbind_dn
is the dn of the user doing theset
(possiblyNone
).This will be executed on
ObjectStore.delete
before the object actually gets deleted from the data store.ObjectStore.delete
is what actually does the work whenFakeLDAPObject.delete_s
is called, and is also called duringFakeLDAPObject.rename_s
to delete the old object.post_delete
Signature:
Callable[[store: ObjectStore, record: LDAPRecord, bind_dn: Optional[str] = None], None]
Where
store
is theObjectStore
object,record
is the record deleted, andbind_dn
is the dn of the user doing theset
(possiblyNone
).This will be executed on
ObjectStore.delete
after the object actually gets deleted from the data store.pre_register_object
Signature:
Callable[[store: ObjectStore, record: LDAPRecord], None]
Where
store
is theObjectStore
object andrecord
is the record to be registered.This will be executed on
ObjectStore.register_object
before the object actually gets saved.post_register_object
Signature:
Callable[[store: ObjectStore, record: LDAPRecord], None]
Where
store
is theObjectStore
object andrecord
is the record that was registered.This will be executed on
ObjectStore.register_object
after the object gets saved.pre_register_objects
Signature:
Callable[[store: ObjectStore, records: List[LDAPRecord]], None]
Where
store
is theObjectStore
object andrecords
is the list of records to be registered.This will be executed on
ObjectStore.register_objects
before the objects actually get saved.post_register_objects
Signature:
Callable[[store: ObjectStore, records: List[LDAPRecord]], None]
Where
store
is theObjectStore
object andrecords
are the records that were registered.This will be executed on
ObjectStore.register_objects
after the objects get saved.pre_load_objects
Signature:
Callable[[store: ObjectStore, filename: str], None]
Where
store
is theObjectStore
object andfilename
is the name of the data file to load.This will be executed on
ObjectStore.load_objects
before the file gets loaded.post_load_objects
Signature:
Callable[[store: ObjectStore, records: List[LDAPRecord]], None]
Where
store
is theObjectStore
object andrecords
are the records that were loaded from the file.This will be executed on
ObjectStore.load_objects
after the objects loaded from the file get saved.
Developer Interface
This part of the documentation covers all the classes and functions that make up python-ldap-faker
.
Unittest Support
- class ldap_faker.LDAPFakerMixin(*args, **kwargs)[source]
This is a mixin for use with
unittest.TestCase
. Properly configured, it will patchldap.initialize
to use ourFakeLDAP.initialize
fake function instead, which will returnFakeLDAPObject
objects instead ofldap.ldapobject.LDAPObject
objects.ldap_modules
is a list of python module paths in which we should patchldap.initialize
with ourFakeLDAP.initialize
method. For example:class TestMyStuff(LDAPFakerMixin, unittest.TestCase): ldap_modules = ['myapp.module']
will cause
LDAPFakerMixin
to patchmyapp.module.ldap.initialize
.ldap_fixtures
names one or more JSON files containing LDAP records to load into aObjectStore
viaObjectStore.load_objects
.ldap_fixtures
can be either a single string, aTuple[str, List[str]]
, or a list ofTuple[str, str, List[str]]
.If we define our test class like so:
class TestMyStuff(LDAPFakerMixin, unittest.TestCase): ldap_fixtures = 'myfixture.json'
We will build our
LDAPServerFactory
with a single defaultObjectStore
with the contents ofmyfixture.json
loaded in.If we define our test class like so:
class TestMyStuff(LDAPFakerMixin, unittest.TestCase): ldap_fixtures = ('myfixture.json', ['389'])
We will build our
LDAPServerFactory
with a single defaultObjectStore
with the contents ofmyfixture.json
loaded in, with the tag 389 applied to it.If we define our test class like this instead:
class TestMyStuff(LDAPFakerMixin, unittest.TestCase): ldap_fixtures = [ ('server1.json', 'ldap://server1', []), ('server2.json', 'ldap://read-server2', ['389']), ]
we will build our
LDAPServerFactory
with twoObjectStore
objects. The first will have the data fromserver1.json
and will be used with urildap://server1
. The second will be used with urildap://server2
and have the data from with the contents ofserver2.json
loaded in, and will have the tag389
applied to it.Note
The tags are used when configuring behavior for our
ObjectStore`
. The389
tag tells theObjectStore
to emulate a 389 type LDAP server (Redhat Directory Server).- ldap_fixtures: Optional[ldap_faker.types.LDAPFixtureList] = None
The filenames of fixtures to load into our fake LDAP servers
- server_factory: LDAPServerFactory
The
LDAPServerFactory
configured by oursetUpClass
- classmethod resolve_file(filename: str) str [source]
Given
filename
, if that filename is a non-absolute path, resolve that filename to an absolute path under the folder in which our subclass’ file resides. Iffilename
is an absoute path, don’t change it.- Parameters
filename – the non-absolute file path to a fixture file
- Raises
FileNotFoundError – the fixture file did not exist
- Returns
The absolute path to the fixture file.
- classmethod load_servers(server_factory: LDAPServerFactory) None [source]
Configure
server_factory
with one or moreObjectStore
objects by looking atldap_fixtures
, a dict where the key is a uri and the value is the name of a JSON file to use as the objects for the associatedObjectStore
Note
If you want to populate your
LDAPServerFactory
in a different way than loading directly from the JSON files listed inldap_fixtures
, this is the classmethod you want to override.- Parameters
server_factory – the
LDAPServerFactory
object to populate
- classmethod setUpClass()[source]
Build the
LDAPServerFactory
we’ll use and save it as a class attribute.We do this as a classmethod because constructing our
ObjectStore
objects is time consuming and we don’t want to have to do it for each of our tests.
- classmethod tearDownClass()[source]
Delete our
server_factory
so we con’t corrupt future tests or leak memory.
- setUp()[source]
Create a
FakeLDAP
instance, make it use theserver_factory
that oursetUpClass
created, andpatch
ldap.initialize
in each of the modules named inldap_modules
. Save theFakeLDAP
instance to ourfake_ldap
attribute for later use in our test code.
- last_connection() Optional[FakeLDAPObject] [source]
Return the
FakeLDAPObject
for the last connection made during ourtest. Hopefully a useful shortcut for when we only make one connection.- Returns
The last connection made
- get_connections(uri: Optional[str] = None) List[FakeLDAPObject] [source]
Return a the list of
FakeLDAPObject
objects generated during our test, optionally filtered by LDAP URI.- Keyword Arguments
uri – the LDAP URI by which to filter our connections
- assertGlobalOptionSet(option: int, value: ldap_faker.types.LDAPOptionValue) None [source]
Assert that a global LDAP option was set.
- Parameters
option – an LDAP option (e.g. ldap.OPT_DEBUG_LEVEL)
value – the value we expect the option to be set to
- assertGlobalFunctionCalled(api_name: str) None [source]
Assert that a global LDAP function was called.
- Parameters
api_name – the name of the function to look for (e.g.
initialize
)
- assertLDAPConnectionOptionSet(conn: FakeLDAPObject, option: str, value: ldap_faker.types.LDAPOptionValue) None [source]
Assert that a specific
FakeLDAPObject
option was set with a specific value.- Parameters
conn – the connection object to examine
option – the code for the option (e.g.
ldap.OPT_X_TLS_NEWCTX
)value – the value we expect the option to be set to
- assertLDAPConnectionMethodCalled(conn: FakeLDAPObject, api_name: str, arguments: Optional[Dict[str, Any]] = None) None [source]
Assert that a specific
FakeLDAPObject
method was called, possibly specifying the specific arguments it should have been called with.- Parameters
conn – the connection object to examine
api_name – the name of the function to look for (e.g.
simple_bind_s
)
- Keyword Arguments
arguments – if given, assert that the call exists AND was called this set of arguments. See
LDAPCallRecord
for how thearguments
dict should be constructed.
- assertLDAPConnectionMethodCalledAfter(conn: FakeLDAPObject, api_name: str, target_api_name: str) None [source]
Assert that a specific
FakeLDAPObject
method was called after another specificFakeLDAPObject
method.- Parameters
conn – the connection object to examine
api_name – the name of the function to look for (e.g.
simple_bind_s
)target_api_name – the name of the function which should appear before
api_name
in the call history
- class ldap_faker.LDAPCallRecord(api_name: str, args: Dict[str, Any])[source]
This is a single LDAP call record, used by
CallHistory
to store information about calls to LDAP api functions.api_name
is the name of the LDAP api call made (e.g.simple_bind_s
,search_s
).args
is the argument list of the call, including defaults for keyword arguments not passed. This is a dict where the key is the name of the positional or keyword argument, and the value is the passed in (or default) value for that argument.Example
If we make this call to a patched
FakeLDAPObject
:ldap_obj.search_s('ou=bar,o=baz,c=country', ldap.SCOPE_SUBTREE, '(uid=foo)')
This will be recorded as:
LDAPCallRecord( api_name='search_s', args={ 'base': 'ou=bar,o=baz,c=country', 'scope': 2, 'filterstr': '(uid=foo)', 'attrlist': None, 'attrsonly': 0 } )
- class ldap_faker.CallHistory(calls: Optional[List[LDAPCallRecord]] = None)[source]
This class records the
python-ldap
call history for a particularFakeLDAPObject
asLDAPCallRecord
objects. It works in conjunction with the@record_call
decorator. AnCallHistory
object will be configured on eachFakeLDAPObject
and on eachFakeLDAP
object capture their call history.We use this in our tests with appropriate asserts to ensure that our code called the
python-ldap
methods we expected, in the order we expected, with the arguments we expected.- filter_calls(api_name: str) List[LDAPCallRecord] [source]
Filter our call history by function name.
- Parameters
api_name – look through our history for calls to this function
- Returns
A list of (
api_name
,arguments
) tuples in the order in which the calls were made. Arguments is aDict[str, Any]
.
- property calls: List[LDAPCallRecord]
This property returns the list of all calls made against the parent object.
Example
To test that your code did a
ldap.simple_bind_s
call with the usernam and password you expected, you could do:from unittest import TestCase import ldap from ldap_faker import LDAPFakerMixin from my_code import App class MyTest(LDAPFakerMixin, TestCase): ldap_modules = ['my_code'] ldap_fixtures = 'myfixture.json' def test_option_was_set(self): app = MyApp() app.do_the_thing() conn = self.ldap_faker.connections[0] self.assertEqual( conn.calls, [('simple_bind_s', {'who': 'uid=foo,ou=dept,o=org,c=country', 'cred': 'pass'})] )
- Returns
Returns a list of 2-tuples, one for each method call made since the last reset. Each tuple contains the name of the API and a dictionary of arguments. Argument defaults are included.
- property names: List[str]
Returns the list names of
python-ldap
functions or methods called, in the order they were called. You can use this to test whether an particularyExample
To test that your code did at least one
ldap.add_s
call, you could do:from unittest import TestCase import ldap from ldap_faker import LDAPFakerMixin from my_code import App class MyTest(LDAPFakerMixin, TestCase): ldap_modules = ['my_code'] ldap_fixtures = 'myfixture.json' def test_option_was_set(self): app = MyApp() app.do_the_thing() conn = self.ldap_faker.connections[0] self.assertEqual('add_s" in conn.calls.names)
- Returns
A list of method names, in the order they were called.
python-ldap replacements
- class ldap_faker.FakeLDAP(server_factory: LDAPServerFactory)[source]
We use this class to house our replacement code for these three prime
python-ldap
functions:The class takes a fully configured
LDAPServerFactory
as an argument, and will use that factory’s collection ofOptionStore
objects to construct newFakeLDAPObject
objects.As a test runs,
FakeLDAP
keeps track of each LDAP connection made and each global LDAP call made so that they can be inspected after your code has run.Note
This is meant to be a disposable object, recreated for each test method. When used properly, all internal state (connections made, calls made, options set) will be empty at the start of every test.
- Parameters
server_factory – a fully configured
LDAPServerFactory
- connections: List[FakeLDAPObject]
list of
FakeLDAPObject
connections created in the order in which they were requested
- calls: CallHistory
The call history for global ldap function calls
- options: OptionStore
A dictionary of LDAP options set
- initialize(uri: str, trace_level: int = 0, trace_file: ~typing.TextIO = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>, trace_stack_limit: int = None, fileno: ~typing.Any = None) FakeLDAPObject [source]
This is the method we use to patch
ldap.initialize
when we are testing our LDAP code. When it is called, we will ask ourFakeLDAP.server_factory
factory for theObjectStore
most appropriate for the LDAP uriuri
, create aFakeLDAPObject
with acopy.deepcopy
of thatObjectStore
, and return theFakeLDAPObject
.Note
Of all the arguments in our signature, we only actually use
uri
. The other arguments are recorded in ourFakeLDAP.calls
call history, but are otherwise ignored.- Parameters
uri – an LDAP URI
trace_level – logging level (ignored)
trace_file – file descriptor to which to write traces (ignored)
trace_stack_limit – stack limit of tracebacks in the debug log (ignored)
fileno – a socket or file descriptor (ignored)
- Raises
ldap.SERVER_DOWN – could not find an appropriate
ObjectStore
foruri
- Returns
A properly configured
FakeLDAPObject
- set_option(option: int, invalue: ldap_faker.types.LDAPOptionValue) None [source]
Set a global
python-ldap option
. This will create a keyoption
in ourFakeLDAP.options
dictionary and set its value tovalue
.Example
In your test code, you can thus test whether your code set the proper global LDAP option like so:
from unittest import TestCase import ldap from ldap_faker import LDAPFakerMixin from my_code import App class MyTest(LDAPFakerMixin, TestCase): ldap_modules = ['my_code'] ldap_fixtures = 'myfixture.json' def test_option_was_set(self): app = MyApp() app.set_the_option(ldap.OPT_DEBUG_LEVEL, 1) self.assertEqual(self.ldap_faker.options[ldap.OPT_DEBUG_LEVEL], 1)
- Parameters
option – an option from python-ldap
invalue – the value to set for the option
- get_option(option: int) ldap_faker.types.LDAPOptionValue [source]
Get a global python-ldap option. If our code hasn’t set an
option
yet, return the default frompython-ldap
for that option.- Parameters
option – an option from python-ldap
- Returns
The value currently set for the option.
- has_connection(uri: str) bool [source]
Test to see whether an
ldap.initialize
call was made with LDAP URI ofuri
.- Parameters
uri – The LDAP URI to look for in our connection history
- Returns
True
if at least one connection touri
was made,False
otherwise.
- get_connections(uri: str) List[FakeLDAPObject] [source]
Return a list of
FakeLDAPObject
connections to LDAP URIuri
.- Parameters
uri – The LDAP URI to look for in our connection history
- Returns
A list of
FakeLDAPObject
objects associated with LDAP URIuri
.
- connection_calls(api_name: Optional[str] = None, uri: Optional[str] = None) CallHistory [source]
Filter our the call history for our connections by function name and optionally LDAP URI.
Args:
- Keyword Arguments
api_name – restrict through our history for calls to this function
uri – restrict our search to only calls to this URI
- Returns
A
CallHistory
with combined calls from the filtered connections.
- class ldap_faker.FakeLDAPObject(uri: str, store: Optional[ObjectStore] = None)[source]
This class simulates most of the interface of
ldap.ldapobject.LDAPObject
which is the object that gets returned when you callldap.initialize()
.Note
This is a disposable object that should be recreated for each test, mostly because changes to our
ObjectStore
can’t be undone without re-copying from its source inServers
.- Parameters
uri – the LDAP URI of the connection
- Keyword Arguments
directory – a populated
ObjectStore
- hostname
port for this connection
- Type
the host
- options: OptionStore
we store data from
set_option
calls here
- store: ObjectStore
our copy of our ObjectStore for this connection
- calls: CallHistory
The method call history
- tls_enabled: bool
Set to True if
start_tls_s
was called
- bound_dn: Optional[str]
Set by
simple_bind_s
to the dn of the user after success
- set_option(option: int, invalue: ldap_faker.types.LDAPOptionValue) None [source]
This method sets the value of the
ldap.ldap.ldapobject.LDAPObject`
option specified byoption
toinvalue
.- Parameters
option – the option
value – the value to set the option to
- Raises
ValueError –
option
is not a validpython-ldap
option
- get_option(option: int) ldap_faker.types.LDAPOptionValue [source]
This method returns the value of the
ldap.ldap.ldapobject.LDAPObject`
option specified byoption
.Note
If your code did not call
FakeLDAPOption.set_option
for this option, we’ll getKeyError
- Parameters
option – the option
- Raises
ValueError –
option
is not a validpython-ldap
optionKeyError –
option
is not a validpython-ldap
option
- Returns
The value of the option
- simple_bind_s(who: str = None, cred: str = None, serverctrls: List[LDAPControl] = None, clientctrls: List[LDAPControl] = None) Optional[Tuple[Union[int, str], List[Tuple[str, Dict[str, List[bytes]]]], int, List[LDAPControl]]] [source]
Perform a bind. This will look in the object store for an object with dn of
who
and comparecred
to theuserPassword
attribute for that object.- Keyword Arguments
who – the dn of the user with which to bind
cred – the password for that user
- Raises
ldap.INVALID_CREDENTIALS – the
who
did not match thecred
- whoami_s() str [source]
This synchronous method implements the LDAP “Who Am I?” extended operation.
It is useful for finding out to find out which identity is assumed by the LDAP server after a bind.
- Returns
{the dn}”
- Return type
Empty string if we haven’t bound as an identity, otherwise “dn
- search_ext(base: str, scope: int, filterstr: str = '(objectClass=*)', attrlist: List[str] = None, attrsonly: int = 0, serverctrls: List[LDAPControl] = None, clientctrls: List[LDAPControl] = None, timeout: int = -1, sizelimit: int = 0) int [source]
- result3(msgid: int = -1, all: int = 1, timeout: int = None) Tuple[Union[int, str], List[Tuple[str, Dict[str, List[bytes]]]], int, List[LDAPControl]] [source]
Retrieve the results of our
FakeLDAPObject.search_ext
call.Note
The
all
andtimeout
keyword arguments are ignored here.- Keyword Arguments
msgid – the
msgid
returned by theFakeLDAPObject.search_ext
callall – if 1, return all results at once; if 0, return them one at a time (ignored)
- Returns
A
ldap.result3
4-tuple.
- search_s(base: str, scope: int, filterstr: str = '(objectClass=*)', attrlist: List[str] = None, attrsonly: int = 0) List[ldap_faker.types.LDAPRecord] [source]
- start_tls_s() None [source]
Negotiate TLS with server.
This sets our
tls_enabled
attribute toTrue
.- Raises
ldap.LOCAL_ERROR –
start_tls_s
was done twice on the same connection
- compare_s(dn: str, attr: str, value: bytes) bool [source]
Perform an LDAP comparison between the attribute named
attr
of entrydn
, and the valuevalue
. For multi-valued attributes, the test is whether any of the values matchvalue
.- Parameters
dn – the dn of the object to look at
attr – the name of the attribute on our object to compare
value – the value to which to compare the object value
- Raises
ldap.NO_SUCH_OBJECT – no object with dn of
dn
exists in our object store- Returns
True
if the values are equal,False
otherwise.
- modify_s(dn, modlist: ldap_faker.types.ModList) Tuple[Union[int, str], List[Tuple[str, Dict[str, List[bytes]]]], int, List[LDAPControl]] [source]
Modify the object with dn of
dn
using the modlistmodlist
.Each element in the list modlist should be a tuple of the form
(mod_op: int, mod_type: str, mod_vals: Union[bytes, List[bytes]])
, wheremod_op
indicates the operation (one ofldap.MOD_ADD
,ldap.MOD_DELETE
, orldap.MOD_REPLACE
,mod_type
is a string indicating the attribute type name, andmod_vals
is either a bytes value or a list of bytes values to add, delete or replace respectively. For the delete operation,mod_vals
may beNone
indicating that all attributes are to be deleted.Note
ldap.modlist.modifyModlist
MAY be your friend here for generating modlists. Do read the note in those docs aboutldap.MOD_DELETE
/ldap.MOD_ADD
vs.ldap.MOD_REPLACE
to see whether that will affect you poorly.Example
Here is an example of constructing a modlist for
modify_s
:>>> import ldap >>> modlist = [ (ldap.MOD_ADD, 'mail', [b'user@example.com', b'user+foo@example.com']), (ldap.MOD_REPLACE, 'cn', [b'My Name']), (ldap.MOD_DELETE, 'gecos', None) ]
- Parameters
dn – the dn of the object to delete
modlist – a modlist suitable for
modify_s
- Raises
ldap.NO_SUCH_OBJECT – no object with dn of
dn
exists in our object storeldap.TYPE_OR_VALUE_EXISTS – you tried to add an value to an attribute, but it was already in the value list
ldap.INSUFFICIENT_ACCESS – you need to do a non-anonymous bind before doing this
- Returns
A
ldap.result3
type 4-tuple.
- delete_s(dn: str) None [source]
Delete the object with dn of
dn
from our object store.Each element in the list modlist should be a tuple of the form
(mod_type: str, mod_vals: List[bytes])
, wheremod_type
is a string indicating the attribute type name, andmod_vals
is either a string value or a list of string values to add, delete or replace respectively. For the delete operation, mod_vals may beNone
indicating that all attributes are to be deleted.- Parameters
dn – the dn of the object to delete
- Raises
ldap.NO_SUCH_OBJECT – no object with dn of
dn
exists in our object storeldap.INSUFFICIENT_ACCESS – you need to do a non-anonymous bind before doing this
- add_s(dn: str, modlist: ldap_faker.types.AddModList) None [source]
Add an object with dn of
dn
.modlist
is similar the one passed tomodify_s
, except that the operation integer is omitted from the tuples inmodlist
. You might want to look into sub-module refmodule{ldap.modlist} for generating the modlist.Example
Here is an example of constructing a modlist for
add_s
:>>> modlist = [ ('uid', [b'user']), ('gidNumber', [b'1000']), ('uidNumber', [b'1000']), ('loginShell', [b'/bin/bash']), ('homeDirectory', [b'/home/user']), ('userPassword', [b'the password']), ('cn', [b'My Name']), ('objectClass', [b'top', b'posixAccount']), ]
- Parameters
dn – the dn of the object to add
modlist – the add modlist
- Raises
ldap.ALREADY_EXISTS – an object with dn of
dn
already exists in our object storeldap.INSUFFICIENT_ACCESS – you need to do a non-anonymous bind before doing this
- rename_s(dn: str, newrdn: str, newsuperior: str = None, delold: int = 1, serverctrls: List[LDAPControl] = None, clientctrls: List[LDAPControl] = None) None [source]
Take
dn
(the DN of the entry whose RDN is to be changed, andnewrdn
, the new RDN to give to the entry. The optional parameternewsuperior
is used to specify a new parent DN for moving an entry in the tree (not all LDAP servers support this).- Parameters
dn – the dn of the object to rename
newrdn – the new RDN
- Keyword Arguments
newsuperior – the new basedn
delold – if 1, delete the old entry after renaming, if 0, don’t.
- Raises
ldap.NO_SUCH_OBJECT – no object with dn of
dn
exists in our object storeldap.INSUFFICIENT_ACCESS – you need to do a non-anonymous bind before doing this
LDAP Server like objects
- class ldap_faker.LDAPServerFactory[source]
This class registers
ObjectStore
objects to be used byFakeLDAP.initialize()
in constructingFakeLDAPObject
objects.ObjectStore
objects are named registered here by LDAP uri (in reality, any string).You may do one of two things, but not both:
Configure a default
ObjectStore
that will be used for allldap.initialize
calls regardless ofuri
Assign a specific
ObjectStore
for eachuri
you will be using in your code.
Example
To register a default
ObjectStore
that will be used for everyuri
passed toFakeLDAP.initialize
:>>> from ldap_faker import ObjectStore, LDAPServerFactory, FakeLDAP >>> data = [ ... ] # some LDAP records >>> factory = LDAPServerFactory() >>> store = ObjectStore(objects=data) >>> factory.register(store) >>> fake_ldap = FakeLDAP(factory)
Now any time your code does an
ldap.initialize()
to our patched version of that function, it will get a aFakeLDAPObject
configured with acopy.deepcopy
of theObjectStore
store
, no matter whaturi
it passes toldap.initialize()
.To register a different
ObjectStores
that will be used for specificuris
:>>> from ldap_faker import ObjectStore, Servers >>> data1 = [ ... ] # some LDAP records >>> factory = LDAPServerFactory() >>> store1 = ObjectStore(objects=data1) >>> factory.register(store1, uri='ldap://server1') >>> data2 = [ ... ] # some different LDAP records >>> store2 = ObjectStore(objects=data2) >>> factory.register(store2, uri='ldap://server2') >>> fake_ldap = FakeLDAP(factory)
Now if your code does
ldap.initialize('ldap://server1')
, it will get aFakeLDAPObject
configured with acopy.deepcopy
of theObjectStore
objectstore1
, while if it doesldap.initialize('ldap://server2' )
, it will get aFakeLDAPObject
configured with acopy.deepcopy
of theObjectStore
objectstore2
.- load_from_file(filename: str, uri: Optional[str] = None, tags: Optional[List[str]] = None) None [source]
Given a file path to a JSON file with the objects for an
ObjectStore
, create a newObjectStore
, load it with that JSON File and register it with uri ofuri
.- Parameters
filename – the full path to our JSON file
- Keyword Arguments
uri – the uri to assign to the
ObjectStore
we createtags – the list of tags to apply to the the
ObjectStore
- Raises
ValueError – raised if a default is already configured while trying to register the
ObjectStore
with a specificuri
RuntimeWarning – raised if we try to overwrite an already registered object store with our new one
- register(store: ObjectStore, uri: Optional[str] = None) None [source]
Register a new
ObjectStore
to be used as our fake LDAP server for when we run our fakeinitialize
function.- Parameters
store – a configured
ObjectStore
- Keyword Arguments
uri – the LDAP uri to associated with
directory
- Raises
ValueError – raised if a default is already configured while trying to register an
ObjectStore
with a specificuri
RuntimeWarning – raised if we try to overwrite an already registered object store with a new one
- get(uri: str) ObjectStore [source]
Return a
copy.deepcopy
of theObjectStore
identified byuri
.- Parameters
uri – use this uri to look up which
ObjectStore
to use- Raises
ldap.SERVER_DOWN – no
ObjectStore
could be found foruri
- Returns
A
copy.deepcopy
of theObjectStore
- class ldap_faker.ObjectStore(tags: Optional[List[str]] = None)[source]
This class represents our actual simulated LDAP object store. Copies of this will be used to configure
FakeLDAPObject
objects.- raw_objects: ldap_faker.types.RawLDAPObjectStore
LDAP records as they would have been returned by
python-ldap`
- objects: ldap_faker.types.LDAPObjectStore
LDAP records set up to make searching better
- convert_LDAPData(data: ldap_faker.types.LDAPData) ldap_faker.types.CILDAPData [source]
Convert an incoming
LDAPData` dict (``Dict[str, List[bytes]
]) to aCILDAPData
dict (CaseInsensitiveDict[str, List[str]])
)We need the data dict to have values as
List[str]
so that our filtering works properly –ldap_filter.Filter.match
only works with strings, not bytes.- Parameters
data – the LDAPData dict to convert
- Returns
The convered CILDAPData dict.
- load_objects(filename: str) None [source]
Load a list of LDAP records stored as JSON from a file into our internal database. Use this when setting up the data you will use to run your tests.
Note
One caveat with this method vs.
ObjectStore.register_objects
is that the records returned bypython-ldap
are of typeTuple[str, Dict[str, List[bytes]]]
but JSON has no concept ofbytes
ortuple
. Thus we will expect the LDAP records in the file to have typeList[str, Dict[str, List[str]]]
and we will convert them toTuple[str, Dict[str, List[bytes]]]
before saving toraw_objects
- Parameters
filename – the path to the JSON file to load
- Raises
ldap.ALREADY_EXISTS – there is already an object in our object store with this dn
ldap.INVALID_DN_SYNTAX – one of the object DNs is not well formed
- register_objects(objs: List[ldap_faker.types.LDAPRecord]) None [source]
Load a list of LDAP records into our internal database. Use this when setting up the data you will use to run your tests. Each record in the list should be in exactly the format that
python-ldap
itself returns: a 2-tuple with dn as the first element and the attribute/value dict as the second element.Example
Adding a several PosixAccount objects:
>>> obj = [ ( 'uid=user,ou=mydept,o=myorg,c=country', { 'cn': [b'Firstname User1'], 'uid': [b'user'], 'uidNumber': [b'123'], 'gidNumber': [b'456'], 'homeDirectory': [b'/home/user'], 'loginShell': [b'/bin/bash'], 'userPassword': [b'the password'], 'objectclass': [b'posixAccount', b'top'] } ), ( 'uid=user2,ou=mydept,o=myorg,c=country', { 'cn': [b'Firstname User2'], 'uid': [b'user2'], 'uidNumber': [b'124'], 'gidNumber': [b'457'], 'homeDirectory': [b'/home/user1'], 'loginShell': [b'/bin/bash'], 'userPassword': [b'the password'], 'objectclass': [b'posixAccount', b'top'] } ) ] >>> directory = ObjectStore() >>> directory.register_objects(obj)
- Parameters
objs – A list of LDAP records as they would have been returned by
ldap.ldapobject.LDAPObject.search_s()
. These are 2-tuples, where the first element is the dn (astr
) and the second element is a dict where the keys arestr
and the values are lists ofbytes
.- Raises
ldap.ALREADY_EXISTS – there is already an object in our object store with this dn
ldap.INVALID_DN_SYNTAX – one of the object DNs is not well formed
TypeError – the LDAPData portion for an object was not of type
Dict[str, List[bytes]]
- register_object(obj: ldap_faker.types.LDAPRecord) None [source]
Add an LDAP record our internal database. Use this to add a single record when setting up the data you will use to run your tests. The data should be in exactly the format that python-ldap itself returns: a 2-tuple with dn as the first element and the attribute/value dict as the second element.
Example
Adding a PosixAccount object:
>>> obj = ( 'uid=user,ou=mydept,o=myorg,c=country', { 'cn': [b'Firstname Lastname'], 'uid': [b'user'], 'uidNumber': [b'123'], 'gidNumber': [b'456'], 'homeDirectory': [b'/home/user'], 'loginShell': [b'/bin/bash'], 'userPassword': [b'the password'] 'objectclass': [b'posixAccount', b'top'] } ) >>> directory = ObjectStore() >>> directory.register_object(obj)
- Parameters
obj – An LDAP record as it would have been returned by
ldap.ldapobject.LDAPObject.search_s()
. This is a 2-tuple, where the first element is the dn (astr
) and the second element is a dict where the keys arestr
and the values are lists ofbytes
.- Raises
ldap.ALREADY_EXISTS – there is already an object in our object store with this dn
ldap.INVALID_DN_SYNTAX – the DN is not well formed
TypeError – the LDAPData portion was not of type
Dict[str, List[bytes]]
- property count
- exists(dn: str, validate: bool = True) bool [source]
Test whether an object with dn
dn
exists.- Parameters
dn – the dn of the object to look for
- Keyword Arguments
validate – if
True
, validate thatdn
is a valid dn- Returns
True
if the object exists,False
otherwise.
- get(dn: str) ldap_faker.types.LDAPData [source]
Return all data for an object from our object store.
- Parameters
dn – the dn of the object to copy.
- Raises
ldap.NO_SUCH_OBJECT – no object with dn of
dn
exists in our object store- Returns
The data for an LDAP object
- copy(dn: str) ldap_faker.types.LDAPData [source]
Return a copy of the data for an object from our object store.
- Parameters
dn – the dn of the object to copy.
- Raises
ldap.NO_SUCH_OBJECT – no object with dn of
dn
exists in our object store- Returns
The data for an LDAP object
- set(dn: str, data: ldap_faker.types.LDAPData, bind_dn: Optional[str] = None) None [source]
Add or update data for the object with dn
dn
.- Parameters
dn – the dn of the object to copy.
data – the dict of data for this object
- Keyword Arguments
bind_dn – the dn of the user doing the set, if any
- Raises
ldap.INVALID_DN_SYNTAX – the DN is not well formed
TypeError – the LDAPData portion was not of type
Dict[str, List[bytes]]
- update(dn: str, modlist: ldap_faker.types.ModList, bind_dn: Optional[str] = None) None [source]
Modify the object with dn of
dn
using the modlistmodlist
.Each element in the list modlist should be a tuple of the form
(mod_op: int, mod_type: str, mod_vals: Union[bytes, List[bytes]])
, wheremod_op
indicates the operation (one ofldap.MOD_ADD
,ldap.MOD_DELETE
, orldap.MOD_REPLACE
,mod_type
is a string indicating the attribute type name, andmod_vals
is either a bytes value or a list of bytes values to add, delete or replace respectively. For the delete operation,mod_vals
may beNone
indicating that all attributes are to be deleted.Note
ldap.modlist.modifyModlist
MAY be your friend here for generating modlists. Do read the note in those docs aboutldap.MOD_DELETE
/ldap.MOD_ADD
vs.ldap.MOD_REPLACE
to see whether that will affect you poorly.Example
Here is an example of constructing a modlist for
modify_s
:>>> import ldap >>> modlist = [ (ldap.MOD_ADD, 'mail', [b'user@example.com', b'user+foo@example.com']), (ldap.MOD_REPLACE, 'cn', [b'My Name']), (ldap.MOD_DELETE, 'gecos', None) ]
- Parameters
dn – the dn of the object to delete
modlist – a modlist suitable for
modify_s
- Keyword Arguments
bind_dn – the dn of the user doing the update, if any
- Raises
ldap.INVALID_DN_SYNTAX – the dn was not well-formed
ldap.NO_SUCH_OBJECT – no object with dn of
dn
exists in our object storeldap.TYPE_OR_VALUE_EXISTS – you tried to add an value to an attribute, but it was already in the value list
ldap.INSUFFICIENT_ACCESS – you need to do a non-anonymous bind before doing this
- create(dn: str, modlist: ldap_faker.types.AddModList, bind_dn: Optional[str] = None) None [source]
Create an object in our store with dn of
dn
.modlist
is similar the one passed tomodify_s
, except that the operation integer is omitted from the tuples inmodlist
. You might want to look into sub-module ldap.modlist for generating the modlist.Example
Here is an example of constructing a modlist for
create
:>>> modlist = [ ('uid', [b'user']), ('gidNumber', [b'1000']), ('uidNumber', [b'1000']), ('loginShell', [b'/bin/bash']), ('homeDirectory', [b'/home/user']), ('userPassword', [b'the password']), ('cn', [b'My Name']), ('objectClass', [b'top', b'posixAccount']), ]
- Parameters
dn – the dn of the object to add
modlist – the add modlist
- Keyword Arguments
bind_dn – the dn of the user doing the create, if any
- Raises
ldap.INVALID_DN_SYNTAX – the dn was not well-formed
ldap.ALREADY_EXISTS – an object with dn of
dn
already exists in our object storeldap.INSUFFICIENT_ACCESS – you need to do a non-anonymous bind before doing this
- delete(dn: str, bind_dn: Optional[str] = None) None [source]
Delete an object from our objects directory.
- Parameters
dn – the dn of the object to delete
- Keyword Arguments
bind_dn – the dn of the user doing the delete, if any
- Raises
ldap.INVALID_DN_SYNTAX – the dn was not well-formed
- search_base(base: str, filterstr: str, attrlist: Optional[List[str]] = None) ldap_faker.types.LDAPSearchResult [source]
Do a
ldap.SCOPE_BASE
search. Return the requested attributes of the object in our object store withdn
ofbase
that also matchesfilterstr
.Note
We return a
copy.deepcopy
of the object, not the actual object. This ensures that if the caller modifies the object they don’t update the objects in us unintentionally.Note
Some attributes are “operational” and are not returned by default They must be named specifically if you want them. Example:
>>> store.search_base('thebasedn', '(objectclass=*)', ['*', 'createTimestamp'])
- Parameters
base – the dn of the object to return
filterstr – the ldap filter string
- Keyword Arguments
attrlist – the list of attributes to return for each object
- Raises
ldap.INVALID_DN_SYNTAX –
base
was not a well-formed DNldap.FILTER_ERROR –
filterstr
is has bad filter syntaxldap.NO_SUCH_OBJECT – no object with dn of
base
exists in the object store
- Returns
A list with one element – the object with dn of
base
.
- search_onelevel(base: str, filterstr: str, attrlist: Optional[List[str]] = None) ldap_faker.types.LDAPSearchResult [source]
Do a
ldap.SCOPE_ONELEVEL
search, for objects directly under basednbase
that matchfilterstr
.Note
We return a
copy.deepcopy
of each object, not the actual object. This ensures that if the caller modifies the object they don’t update the objects in us unintentionally.- Parameters
base – the dn of the object to return
filterstr – the ldap filter string
- Keyword Arguments
attrlist – the list of attributes to return for each object
- Raises
ldap.INVALID_DN_SYNTAX –
base
was not a well-formed DNldap.FILTER_ERROR –
filterstr
is has bad filter syntax
- Returns
A list of LDAP objects – 2-tuples of
(dn, data)
.
- search_subtree(base: str, filterstr: str, attrlist: Optional[List[str]] = None, include_operational_attributes: bool = False) ldap_faker.types.LDAPSearchResult [source]
Do a
ldap.SCOPE_SUBTREE
search, for objects under basednbase
that matchfilterstr
.- Parameters
base – the dn of the object to return
filterstr – the ldap filter string
Note
We return a
copy.deepcopy
of each object, not the actual object. This ensures that if the caller modifies the object they don’t update the objects in us unintentionally.- Keyword Arguments
attrlist – the list of attributes to return for each object
include_operational_attributes – include all operational attributes even if they are not named in
attrlist
- Raises
ldap.INVALID_DN_SYNTAX –
base
was not a well-formed DNldap.FILTER_ERROR –
filterstr
is has bad filter syntax
- Returns
A list of LDAP objects – 2-tuples of
(dn, data)
.
- class ldap_faker.OptionStore[source]
We use this to store options set via
set_option
.- set(option: int, invalue: ldap_faker.types.LDAPOptionValue) None [source]
Set an option.
- Parameters
option – the code for the option (e.g.
ldap.OPT_X_TLS_NEWCTX
)value – the value we want the option to be set to
- Raises
ValueError –
option
is not a validpython-ldap
option
- get(option: int) ldap_faker.types.LDAPOptionValue [source]
Get the value for a previosly set option that was set via
OptionStore.set
.- Parameters
option – the code for the option (e.g.
ldap.OPT_X_TLS_NEWCTX
)- Raises
ValueError –
option
is not a validpython-ldap
option- Returns
The value for the option, or the default.
Hook management
- ldap_faker.hooks = <ldap_faker.hooks.HookRegistry object>
- class ldap_faker.HookDefinition(name: str, signature: str)[source]
The definition for a hook. This is comprised of a name and a signature.
Example
>>> hook_def = HookDefinition( name='pre_save", signature="Callable[[ObjectStore, LDAPRecord], None] ) >>> hook_def.name "pre_save" >>> hook_def.signature "Callable[[ObjectStore, LDAPRecord], None]"
- signature
the python type annotation signature that the hook should implement, e.g. “Callable[[ObjectStore, LDAPRecord], None]”
- Type
- class ldap_faker.HookRegistry[source]
- property definitions: List[HookDefinition]
Return a list of known hooks definitions as
- register_hook_definition(hook_name: str, signature: str) None [source]
Register a hook definition. Hook definitions define what hooks exist, and what their function signature must be.
Example
>>> hooks = HookRegistry() >>> hooks.register_definition('pre_set', 'Callable[[ObjectStore, LDAPRecord], None]')
- Parameters
hook_name – the name of the hook
signature – A string in Python type annotation format describing the signature the hook must have
- register_hook(hook_name: str, func: Callable, tags: Optional[List[str]] = None) None [source]
Register a hook for this object store. Hooks are functions with this signature:
def myhook(store: ObjectStore, record: LDAPRecord) -> None:
Use hooks to implement side-effects on select
ObjectStore
methods.Example
To register a hook that updates a an attribute named ``modifyTimestamp` before saving a record to the object store, you could define the hook like so:
- def update_modifyTimestamp(store: ObjectStore, record: LDAPRecord) -> None:
record[1][‘modifyTimestamp’] = datetime.datetime.utcnow().strftime(‘%Y%m%d%H%M%SZ’)
and register it as a pre_modify method like so:
>>> store = ObjectStore() >>> store.register_hook('pre_set', update_modifyTimestamp)
Note
Hooks for a particular
hook_name
are applied in the order they are registered.- Parameters
hook_name – the name of the known hook to which register this
func
func – the hook function
- Raises
ValueError –
hook_name
is not a known hook
- get(hook_name: str, tags: Optional[List[str]] = None) List[Callable] [source]
Get a list of hook callables for the hook named by
name
, possibly filtering hooks by tag.Tag filtering rules:
If a hook has no tags associated with it, it always applies.
Otherwise, if at least one of the hooks tags are present in
tags
, the hook applies.
- Parameters
hook_name – the name of the hook for which to return functions
- Keyword Arguments
tags – if provided, filter the available hook functions to include only those with tags listed in
tags
- Raises
ValueError – there is no known hook with name
hook_name
- Returns
A list of callables.
Type Aliases
- ldap_faker.types.LDAPOptionValue
The central part of internal API.
This represents a generic version of type ‘origin’ with type arguments ‘params’. There are two kind of these aliases: user defined and special. The special ones are wrappers around builtin collections and ABCs in collections.abc. These must have ‘name’ always set. If ‘inst’ is False, then the alias can’t be instantiated, this is used by e.g. typing.List and typing.Dict.
- ldap_faker.types.LDAPData
The central part of internal API.
This represents a generic version of type ‘origin’ with type arguments ‘params’. There are two kind of these aliases: user defined and special. The special ones are wrappers around builtin collections and ABCs in collections.abc. These must have ‘name’ always set. If ‘inst’ is False, then the alias can’t be instantiated, this is used by e.g. typing.List and typing.Dict.
- ldap_faker.types.LDAPRecord
The central part of internal API.
This represents a generic version of type ‘origin’ with type arguments ‘params’. There are two kind of these aliases: user defined and special. The special ones are wrappers around builtin collections and ABCs in collections.abc. These must have ‘name’ always set. If ‘inst’ is False, then the alias can’t be instantiated, this is used by e.g. typing.List and typing.Dict.
- ldap_faker.types.LDAPSearchResult
The central part of internal API.
This represents a generic version of type ‘origin’ with type arguments ‘params’. There are two kind of these aliases: user defined and special. The special ones are wrappers around builtin collections and ABCs in collections.abc. These must have ‘name’ always set. If ‘inst’ is False, then the alias can’t be instantiated, this is used by e.g. typing.List and typing.Dict.
- ldap_faker.types.ModList
The central part of internal API.
This represents a generic version of type ‘origin’ with type arguments ‘params’. There are two kind of these aliases: user defined and special. The special ones are wrappers around builtin collections and ABCs in collections.abc. These must have ‘name’ always set. If ‘inst’ is False, then the alias can’t be instantiated, this is used by e.g. typing.List and typing.Dict.
- ldap_faker.types.AddModList
The central part of internal API.
This represents a generic version of type ‘origin’ with type arguments ‘params’. There are two kind of these aliases: user defined and special. The special ones are wrappers around builtin collections and ABCs in collections.abc. These must have ‘name’ always set. If ‘inst’ is False, then the alias can’t be instantiated, this is used by e.g. typing.List and typing.Dict.
- ldap_faker.types.LDAPFixtureList
The central part of internal API.
This represents a generic version of type ‘origin’ with type arguments ‘params’. There are two kind of these aliases: user defined and special. The special ones are wrappers around builtin collections and ABCs in collections.abc. These must have ‘name’ always set. If ‘inst’ is False, then the alias can’t be instantiated, this is used by e.g. typing.List and typing.Dict.
alias of
Union
[str
,Tuple
[str
,List
[str
]],List
[Tuple
[str
,str
,List
[str
]]]]