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. The dn is case-insensitive in all python-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:

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.

alias of Tuple[str, Dict[str, List[bytes]]]

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 a str

  • 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.`