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 binds additional methods to base classes (LDAP
, or LDAPObject
).
Extension Activation¶
The LDAP.activate_extension()
method accepts a string containing the name of the module to import. After
importing, an activate_extension()
function will be called on the module itself if defined. Any setup can be done in
this function, including calls to EXTEND()
(see below).
Binding New Methods¶
In order to ensure all extensions play nicely together, do not bind methods to these yourself. Each extensible class
has a classmethod called EXTEND
which accepts a list of methods (or any callable object) to bind. If you need to
bind the method as a different name, override it’s __name__
attribute before calling EXTEND
.
No method may ever be overwritten, built-in or otherwise. If you want to modify the behavior of existing methods, you should create a subclass in your extension module and instruct your users to import this instead.
Below is a simple extension module:
from laurelin.ldap import LDAP, LDAPObject
def get_group_members(self, dn):
"""get_group_members(dn)
Get all members of a group at a particular dn.
:param str dn: The group's distinguished name
:return: A list of member usernames
:rtype: list[str]
"""
group = self.get(dn)
return group.get_attr('memberUid')
def obj_get_group_members(self):
"""obj_get_group_members()
Get all members of this group object.
:return: A list of member usernames
:rtype: list[str]
"""
return self.get_attr('memberUid')
obj_get_group_members.__name__ = 'get_group_members'
def activate_extension()
LDAP.EXTEND([get_group_members])
LDAPObject.EXTEND([obj_get_group_members])
The module can then be used like so:
from laurelin.ldap import LDAP
LDAP.activate_extension('extension.module.name')
with LDAP() as ldap:
print(ldap.get_group_members('cn=foo,ou=groups,o=example')) # Example LDAP usage
print(ldap.get('cn=foo,ou=groups,o=example').get_group_members()) # Example LDAPObject usage
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.
-
LDAP.
send_extended_request
(oid, value=None, **kwds)[source] Send an extended request, returns instance of
ExtendedResponseHandle
This is mainly meant to be called by other built-in methods and client extensions. Requires handling of raw pyasn1 protocol objects.
Parameters: Returns: An iterator yielding tuples of the form (
rfc4511.IntermediateResponse
,rfc4511.Controls
) or (rfc4511.ExtendedResponse
,rfc4511.Controls
).Return type: Raises: - LDAPSupportError – if the OID is not listed in the supportedExtension attribute of the root DSE
- TypeError – if the value parameter is not a valid type
Additional keyword arguments are handled as Controls and then passed through into the
ExtendedResponseHandle
constructor.
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')
If this were a laurelin extension, you could go on to bind it to LDAP
as follows:
def activate_extension()
LDAP.EXTEND([who_am_i])
Controls¶
Extensions may wish to define controls for use on existing methods. See Defining Controls for more information.
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
.
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. Note that this does not need to be IANA-registered to work in laurelin, but it still must be globally unique.NAME
- the name of the rule. Must also be globally unique. This is usually how matching rules are referenced in attribute type specs (seecaseExactMatch
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 inlaurelin.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.ldap.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. As with matching rules, there is no requirement that this is IANA-registered, but it must be globally unique.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.ldap.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]*$'
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
You can also define your own validators, see below.
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 avoid needing to import the module again by using the return value
from LDAP.activate_extension()
, like so:
from laurelin.ldap import LDAP
my_ext = LDAP.activate_extension('an.extension.module')
with LDAP('ldaps://dir.example.org', validators=[my_ext.MyValidator()]) as ldap:
# do stuff
Packaging¶
laurelin.extensions
is a
namespace package meaning you can
add your own modules and packages to it. You can use this on your private infrastructure, publish it in its own
package that way, or submit it as a pull request to be shipped as a built-in extension. You’re also welcome to package
in your own namespace, as long as it is reachable for import.