Creating Extensions

The most important thing to note about “extensions” is that they are not necessarily LDAP extensions. In laurelin, they are simply a module that does any combination of: defining new schema elements, defining new controls, or defining new methods to be attached to LDAP or LDAPObject.

Extension System

Extensions live in any importable module or package. They must at minimum define a class called LaurelinExtension as follows:

from laurelin.ldap import BaseLaurelinExtension

class LaurelinExtension(BaseLaurelinExtension):
    NAME = 'some_name'

You’ll notice the BaseLaurelinExtension here - this is required. It is one of many weapons at your disposal.

Extension Classes

All of these share the same common end-user interface of being exposed as either a property or dynamic attribute on some other instance that the user typically will already use normally. Which class they are attached to depends on the name and base class of the defined extension class. Whether they are accessible as a property (with IDE auto-complete support) or a dynamic attribute depends on how the extension is loaded and defined (more below), but the user API is unchanged either way.

class LaurelinExtension( BaseLaurelinExtension ):
As described above, this is where you define the name of the property or dynamic attribute where all instances of these extension classes can be accessed. One instance of this class is created per Python interpreter when the extesion is first added or used (more on this later) and it is accessible to users at laurelin.ldap.extensions.<NAME>.
class LaurelinLDAPExtension( BaseLaurelinLDAPExtension ):
This is where you can bind methods, attributes, etc. that will be attached to LDAP by way of property or dynamic attribute with name corresponding to your LaurelinExtension.NAME. You can access the parent LDAP instance at self.parent. Up to one instance is created per LDAP instance when the property or dynamic attribute is first accessed on a particular instance.
class LaurelinLDAPObjectExtension( BaseLaurelinLDAPObjectExtension ):
This is where you can bind methods, attributes, etc. that will be attached to LDAPObject by way of property or dynamic attribute with name corresponding to your LaurelinExtension.NAME. You can access the parent LDAPObject instance at self.parent. Up to once instance is created per LDAPObject instance when the property or dynamic attribute is first accessed on a particular instance.

Schema and Controls Classes

These two simply attempt to register all public attributes defined within them as schema elements or controls. More about actually defining these below, the class signatures should look like this, though:

class LaurelinSchema( BaseLaurelinSchema ):
Define all SyntaxRule and EqualityMatchingRule classes as local classes within this class. Directly instantiate ObjectClass and AttributeType with standard spec strings and assign them to class attributes.
class LaurelinControls( BaseLaurelinControls ):
Define all Control classes as local classes within this class.

Note that the placement of schema and control definitions is fairly flexible and are not restricted to these 2 classes (but this kind of organization or a variation upon it is suggested). See the Schema and Controls sections below for more details.

Also note that if your schema depends on the base schema, you must require it at the top of your extension like so:

from laurelin.ldap import extensions

extensions.base_schema.require()

Depending on Extensions

Extension authors may want to duplicate and tailor some or all of this information in their own documentation for users.

There are two ways laurelin can be made aware of extensions:

  1. By passing a module name string to add_extension(). This will cause the extension class instances to be made available as dynamic attributes.
  2. By being defined in Extensible.AVAILABLE_EXTENSIONS. A script will automatically generate properties that are inherited by the appropriate parent class (LDAP or LDAPObject). This has the benefit that IDEs can auto-complete extension instances if the extension is installed (tested with PyCharm). Also defined with your extension is the string module name, so your users do not need to copy this themselves, as well as the pip package name, which will be included in the exception if users attempt to use your extension when its not installed.

There are clear pros and cons to each approach, and extension authors are welcome to instruct users to take either approach. #1 may be preferred during development, or if you do not intend to publish your extension publicly.

One caveat to #2 above if you define schema or controls, is your users will need to explicitly require your extension like so:

from laurelin.ldap import extensions

extensions.<NAME>.require()

This happens implicitly in the following situations:

  • When add_extension() is called, as in #1 above
  • When the user accesses your <NAME> extension property/attribute on LDAP or LDAPObject, if you defined any extensions to those classes
  • Technically happens implicitly when extensions.<NAME> is accessed, so if you define any other user-exposed attributes on your LaurelinExtension class that all users must access, you can instruct them to use that instead.

So if you require any of these of your users by way of your own documentation, you can also have them skip the explicit require() call.

Regardless of whether your extension is added or defined, your users will need to explicitly add the dependency to their own package. Laurelin will never depend on an extension module, and only built-in extensions are guarnateed to be available.

Publishing Extensions

If you are planning on defining any standard LDAP extensions, schema, or controls, I suggest packaging your module under laurelin.extensions, which is a namespace package. This allows an exceedingly simple and easy path to eventual merging in as a built-in extension. You are welcome to package under any importable module, though.

If you choose to instruct your users to add your extension, please be sure to write clear and accessible documentation for them.

If you choose to define your extension, please submit a pull request on GitHub. You should include ONLY a ~5 line addition to Extensible.AVAILABLE_EXTENSIONS. The dict key should match your LaurelinExtension.NAME. The keys in the sub-dictionary should be pretty self-explanatory. Below is a contrived example patch:

diff --git a/laurelin/ldap/extensible/base.py b/laurelin/ldap/extensible/base.py
index 593e64b..bd7b233 100644
--- a/laurelin/ldap/extensible/base.py
+++ b/laurelin/ldap/extensible/base.py
@@ -132,6 +132,11 @@ class Extensible(object):
             'pip_package': None,  # built-in
             'docstring': 'Built-in extension defining standard paged results control for search'
         },
+        'some_ext': {
+            'module': 'your.extension.module',
+            'pip_package': 'laurelin-some-ext',
+            'docstring': 'A contrived example laurelin extension'
+        },
     }

     ADDITIONAL_EXTENSIONS = {}

Please keep your docstrings short. They will be rendered in laurelin’s documentation. You may include a Sphinx-formatted shortlink to your own docs.

If you have any questions, problems, or concerns, please open an issue on GitHub.

LDAP Extensions

When defining an actual LDAP extension with an OID and requiring server support, you’ll create the laurelin extension as shown above, but you’ll be calling the LDAP.send_extended_request() method from your extension methods within your LaurelinLDAPExtension or LaurelinLDAPObjectExtension.

As you can see, this accepts the OID of the LDAP extension and an optional request value. You can also pass control keywords, and the require_success keyword, which will automatically check for success on the final extendedResponse message (and raise an LDAPError on failure).

If your LDAP extension expects intermediateResponse messages, you can iterate the return from LDAP.send_extended_request(). You can also call ExtendedResponseHandle.recv_response() to get only one message at a time (preferred to iteration if you only expect the one extendedResponse message).

The built-in LDAP.who_am_i() method is an excellent example of a simple LDAP extension:

from laurelin.ldap import LDAP
from laurelin.ldap.protoutils import get_string_component

def who_am_i(self):
     handle = self.send_extended_request(LDAP.OID_WHOAMI, require_success=True, **ctrl_kwds)
     xr, res_ctrls = handle.recv_response()
     return get_string_component(xr, 'responseValue')

As a laurelin extension this might look like:

from laurelin.ldap import BaseLaurelinLDAPExtension

# ...

class LaurelinLDAPExtension(BaseLaurelinLDAPExtension):
    def who_am_i(self):
        handle = self.parent.send_extended_request(...)
        # ...

Note the use of self.parent to access LDAP.send_extended_request().

Controls

Extensions may wish to define controls for use on existing methods. You will need to define one or more Control classes, see Defining Controls for more information about this. The important part for the purposes of this document is where to place those class definitions in your extension module.

You must define a subclass of LaurelinTransiter, or the more semantically appropriate but functionally identical BaseLaurelinControls. Your subclass must then have local Control subclasses defined within it. For example:

from laurelin.ldap import BaseLaurelinExtension, BaseLaurelinControls, Control

class LaurelinExtension(BaseLaurelinExtension):
    NAME = 'your_name'

class LaurelinControls(BaseLaurelinControls):
    class YourControl(Control):
        method = ('search',)
        keyword = 'some_kwd'
        REQUEST_OID = '1.2.3.4'

Note that controls may alternatively be defined directly in your LaurelinExtension class.

Schema

Extensions may be associated with a set of new schema elements, including object classes, attribute types, matching rules, and syntax rules. Once defined, these will get used automatically by other parts of laurelin, including the SchemaValidator, and for comparing items in attribute value lists within an LDAPObject.

Like controls, all extension schema elements must be defined as attributes on a subclass of LaurelinTransiter. The more semantically appropriate BaseLaurelinSchema is provided as well. You can use these base classes to organize your schema and controls however appropriate. Alternatively, you may also define schema elements directly in your LaurelinExtension class.

If your schema depends on the laurelin built-in base schema, you must explicitly call laurelin.ldap.extensions.base_schema.require() near the top of your extension module.

Below is a simple example of defining a new object class depending on the base schema:

from laurelin.ldap import BaseLaurelinExtension, BaseLaurelinControls, ObjectClass, extensions

extensions.base_schema.require()

class LaurelinExtension(BaseLaurelinExtension):
    NAME = 'your_name'

class LaurelinSchema(BaseLaurelinSchema):
    MY_COMPANY_USER = ObjectClass('''
    ( 1.2.3.4 NAME 'myCompanyUser' SUP inetOrgPerson STRUCTURAL
      MUST ( companyAttribute $ anotherAttribute )
      MAY description
    ''')

The superclass of inetOrgPerson makes this example require the base schema. All schema instance elements must be defined as class attributes in this manner (for object classes and attribute types), and all class elements must be defined below the LaurelinSchema class (for syntax rules and matching rules).

Object Classes and Attribute Types

Creating object classes and attribute types is very simple. Just take the standard LDAP specification and pass it to the appropriate class constructor. Examples from the netgroups extension:

from laurelin.ldap.objectclass import ObjectClass
from laurelin.ldap.attributetype import AttributeType

 ObjectClass('''
 ( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' SUP top STRUCTURAL
   MUST cn
   MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )
 ''')

 AttributeType('''
 ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple'
   DESC 'Netgroup triple'
   EQUALITY caseExactMatch
   SYNTAX 1.3.6.1.1.1.0.0 )
 ''')

Matching Rules

Defining matching rules takes a little more effort. Matching rules must subclass EqualityMatchingRule. Required class attributes include:

  • OID - the numeric OID of this rule (see section below about OIDs).
  • NAME - the name of the rule. Must also be globally unique. This is usually how matching rules are referenced in attribute type specs (see caseExactMatch in above example).
  • SYNTAX - the numeric OID of the syntax rule that assertion values must match.

Matching rule classes may also optionally define the following attribute:

  • prep_methods - a sequence of callables that will be used to prepare both the attribute value and assertion value for comparison. These will typically be defined in laurelin.ldap.rfc4518. The initial attribute/assertion value will be passed into the first item in the sequence, and the return from each is passed into the next item.

If you prefer, you can also override the MatchingRule.prepare() method on your matching rule class.

You may also wish to override EqualityMatchingRule.do_match(). This is passed the two prepared values and must return a boolean. Overriding MatchingRule.match() is not recommended.

Below is an example matching rule from laurelin.extensions.base_schema:

from laurelin.ldap.rules import EqualityMatchingRule
from laurelin.ldap import rfc4518

 class numericStringMatch(EqualityMatchingRule):
     OID = '2.5.13.8'
     NAME = 'numericStringMatch'
     SYNTAX = '1.3.6.1.4.1.1466.115.121.1.36'
     prep_methods = (
         rfc4518.Transcode,
         rfc4518.Map.characters,
         rfc4518.Normalize,
         rfc4518.Prohibit,
         rfc4518.Insignificant.numeric_string,
     )

Syntax Rules

Syntax rules must subclass SyntaxRule, although in almost all cases you can use RegexSyntaxRule. If you do not use a regular expression, you must override SyntaxRule.validate(), which receives a single string argument, and must raise InvalidSyntaxError when it is incorrect.

In all cases, you must define the following attributes on your syntax rule class:

  • OID - the numeric OID of the rule (See section below about OIDs).
  • DESC - a brief description of the rule. This is mainly used in exception messages.

Regex syntax rules must also define:

  • regex - the regular expression.

Below are examples from laurelin.extensions.base_schema:

from laurelin.ldap.rules import SyntaxRule, RegexSyntaxRule
from laurelin.ldap.exceptions import InvalidSyntaxError
import six

 class DirectoryString(SyntaxRule):
     OID = '1.3.6.1.4.1.1466.115.121.1.15'
     DESC = 'Directory String'

     def validate(self, s):
         if not isinstance(s, six.string_types) or (len(s) == 0):
             raise InvalidSyntaxError('Not a valid {0}'.format(self.DESC))

 class Integer(RegexSyntaxRule):
     OID = '1.3.6.1.4.1.1466.115.121.1.27'
     DESC = 'INTEGER'
     regex = r'^-?[1-9][0-9]*$'

Schema/Controls Registration System

Schema and controls go through an identical 2-step registration system. The LaurelinTransiter class first stores a list of all schema and control attributes mapped to the module name that defined them. This occurs when the class is defined, i.e. at import time.

The LaurelinRegistrar.require() method then invokes the .register() method on each schema element or control class defined in the same module. This causes the element to be mapped according to its class, name, and OID - which are ultimately what is needed for laurelin to make use of the object.

OIDs

Controls and schema elements all need an OID to be defined. You should obtain a Private Enterprise Number from IANA for any OIDs that you publish to the internet (and probably for any that you don’t as well). This is completely free and usually only takes a few days to process.

The OID you get from IANA should be used as the root of your namespace, and you can define the structure below it as you see fit.

Validators

Validators must subclass Validator. The public interface includes Validator.validate_object() and Validator.validate_modify(). You will usually just want to override these, however they do include a default implementation which checks all attributes using the abstract Validator._validate_attribute(). Check method docs for more information about how to define these.

When defining validators in your extension, you can ensure your users don’t need to import the module again by attaching the class to your LaurelinExtension class like so:

from laurelin.ldap import BaseLaurelinExtension, Validator

class LaurelinExtension(BaseLaurelinExtension):
   NAME = 'my_ext'

   class MyValidator(Validator):
      # ...
      pass

Users can then access it like so:

from laurelin.ldap import LDAP, extensions

with LDAP('ldaps://dir.example.org', validators=[extensions.my_ext.MyValidator]) as ldap:
   # do stuff

SchemaValidator

Laurelin ships with SchemaValidator which, when applied to a connection, automatically checks write operations for schema validity before sending the request to the server. This includes any schema you define in your extensions. Users can enable this like so:

from laurelin.ldap import LDAP
from laurelin.ldap.schema import SchemaValidator

with LDAP('ldaps://dir.example.org', validators=[SchemaValidator]) as ldap:
   # do stuff

Class Diagram

The extension subsystem has several interconnecting classes. Blue are auto-generated classes, and green are defined in extension modules. Unlabeled arrows indicate class inheritance or are self-explanatory.

Laurelin extensions class and instance diagram