added ldap3 lib and ldap3 backend

This commit is contained in:
Esther Kleinhenz
2018-06-28 17:24:46 +02:00
parent 00c10dbaa8
commit 95b1878141
281 changed files with 67200 additions and 6 deletions
@@ -0,0 +1,105 @@
"""
ldap - base module
See https://www.python-ldap.org/ for details.
"""
# This is also the overall release version number
from ldap.pkginfo import __version__, __author__, __license__
import os
import sys
if __debug__:
# Tracing is only supported in debugging mode
import atexit
import traceback
_trace_level = int(os.environ.get("PYTHON_LDAP_TRACE_LEVEL", 0))
_trace_file = os.environ.get("PYTHON_LDAP_TRACE_FILE")
if _trace_file is None:
_trace_file = sys.stderr
else:
_trace_file = open(_trace_file, 'a')
atexit.register(_trace_file.close)
_trace_stack_limit = None
import _ldap
assert _ldap.__version__==__version__, \
ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__))
from _ldap import *
# call into libldap to initialize it right now
LIBLDAP_API_INFO = _ldap.get_option(_ldap.OPT_API_INFO)
OPT_NAMES_DICT = {}
for k,v in vars(_ldap).items():
if k.startswith('OPT_'):
OPT_NAMES_DICT[v]=k
class DummyLock:
"""Define dummy class with methods compatible to threading.Lock"""
def __init__(self):
pass
def acquire(self):
pass
def release(self):
pass
try:
# Check if Python installation was build with thread support
import thread
except ImportError:
LDAPLockBaseClass = DummyLock
else:
import threading
LDAPLockBaseClass = threading.Lock
class LDAPLock:
"""
Mainly a wrapper class to log all locking events.
Note that this cumbersome approach with _lock attribute was taken
since threading.Lock is not suitable for sub-classing.
"""
_min_trace_level = 3
def __init__(self,lock_class=None,desc=''):
"""
lock_class
Class compatible to threading.Lock
desc
Description shown in debug log messages
"""
self._desc = desc
self._lock = (lock_class or LDAPLockBaseClass)()
def acquire(self):
if __debug__:
global _trace_level
if _trace_level>=self._min_trace_level:
_trace_file.write('***%s.acquire() %s %s\n' % (self.__class__.__name__,repr(self),self._desc))
return self._lock.acquire()
def release(self):
if __debug__:
global _trace_level
if _trace_level>=self._min_trace_level:
_trace_file.write('***%s.release() %s %s\n' % (self.__class__.__name__,repr(self),self._desc))
return self._lock.release()
# Create module-wide lock for serializing all calls into underlying LDAP lib
_ldap_module_lock = LDAPLock(desc='Module wide')
from ldap.functions import initialize,get_option,set_option,escape_str,strf_secs,strp_secs
from ldap.ldapobject import NO_UNIQUE_ENTRY, LDAPBytesWarning
from ldap.dn import explode_dn,explode_rdn,str2dn,dn2str
del str2dn
del dn2str
# More constants
# For compatibility of 2.3 and 2.4 OpenLDAP API
OPT_DIAGNOSTIC_MESSAGE = OPT_ERROR_STRING
@@ -0,0 +1,15 @@
"""
ldap.asyncsearch - handle async LDAP search operations
See https://www.python-ldap.org/ for details.
"""
import warnings
from ldap.asyncsearch import *
from ldap.asyncsearch import __version__
warnings.warn(
"'ldap.async module' is deprecated, import 'ldap.asyncsearch' instead.",
DeprecationWarning,
stacklevel=2
)
@@ -0,0 +1,284 @@
"""
ldap.asyncsearch - handle async LDAP search operations
See https://www.python-ldap.org/ for details.
"""
import ldap
from ldap import __version__
import ldif
SEARCH_RESULT_TYPES = {
ldap.RES_SEARCH_ENTRY,
ldap.RES_SEARCH_RESULT,
ldap.RES_SEARCH_REFERENCE,
}
ENTRY_RESULT_TYPES = {
ldap.RES_SEARCH_ENTRY,
ldap.RES_SEARCH_RESULT,
}
class WrongResultType(Exception):
def __init__(self,receivedResultType,expectedResultTypes):
self.receivedResultType = receivedResultType
self.expectedResultTypes = expectedResultTypes
Exception.__init__(self)
def __str__(self):
return 'Received wrong result type %s (expected one of %s).' % (
self.receivedResultType,
', '.join(self.expectedResultTypes),
)
class AsyncSearchHandler:
"""
Class for stream-processing LDAP search results
Arguments:
l
LDAPObject instance
"""
def __init__(self,l):
self._l = l
self._msgId = None
self._afterFirstResult = 1
def startSearch(
self,
searchRoot,
searchScope,
filterStr,
attrList=None,
attrsOnly=0,
timeout=-1,
sizelimit=0,
serverctrls=None,
clientctrls=None
):
"""
searchRoot
See parameter base of method LDAPObject.search()
searchScope
See parameter scope of method LDAPObject.search()
filterStr
See parameter filter of method LDAPObject.search()
attrList=None
See parameter attrlist of method LDAPObject.search()
attrsOnly
See parameter attrsonly of method LDAPObject.search()
timeout
Maximum time the server shall use for search operation
sizelimit
Maximum number of entries a server should return
(request client-side limit)
serverctrls
list of server-side LDAP controls
clientctrls
list of client-side LDAP controls
"""
self._msgId = self._l.search_ext(
searchRoot,searchScope,filterStr,
attrList,attrsOnly,serverctrls,clientctrls,timeout,sizelimit
)
self._afterFirstResult = 1
return # startSearch()
def preProcessing(self):
"""
Do anything you want after starting search but
before receiving and processing results
"""
def afterFirstResult(self):
"""
Do anything you want right after successfully receiving but before
processing first result
"""
def postProcessing(self):
"""
Do anything you want after receiving and processing all results
"""
def processResults(self,ignoreResultsNumber=0,processResultsCount=0,timeout=-1):
"""
ignoreResultsNumber
Don't process the first ignoreResultsNumber results.
processResultsCount
If non-zero this parameters indicates the number of results
processed is limited to processResultsCount.
timeout
See parameter timeout of ldap.LDAPObject.result()
"""
self.preProcessing()
result_counter = 0
end_result_counter = ignoreResultsNumber+processResultsCount
go_ahead = 1
partial = 0
self.beginResultsDropped = 0
self.endResultBreak = result_counter
try:
result_type,result_list = None,None
while go_ahead:
while result_type is None and not result_list:
result_type,result_list,result_msgid,result_serverctrls = self._l.result3(self._msgId,0,timeout)
if self._afterFirstResult:
self.afterFirstResult()
self._afterFirstResult = 0
if not result_list:
break
if result_type not in SEARCH_RESULT_TYPES:
raise WrongResultType(result_type,SEARCH_RESULT_TYPES)
# Loop over list of search results
for result_item in result_list:
if result_counter<ignoreResultsNumber:
self.beginResultsDropped = self.beginResultsDropped+1
elif processResultsCount==0 or result_counter<end_result_counter:
self._processSingleResult(result_type,result_item)
else:
go_ahead = 0 # break-out from while go_ahead
partial = 1
break # break-out from this for-loop
result_counter = result_counter+1
result_type,result_list = None,None
self.endResultBreak = result_counter
finally:
if partial and self._msgId!=None:
self._l.abandon(self._msgId)
self.postProcessing()
return partial # processResults()
def _processSingleResult(self,resultType,resultItem):
"""
Process single entry
resultType
result type
resultItem
Single item of a result list
"""
pass
class List(AsyncSearchHandler):
"""
Class for collecting all search results.
This does not seem to make sense in the first place but think
of retrieving exactly a certain portion of the available search
results.
"""
def __init__(self,l):
AsyncSearchHandler.__init__(self,l)
self.allResults = []
def _processSingleResult(self,resultType,resultItem):
self.allResults.append((resultType,resultItem))
class Dict(AsyncSearchHandler):
"""
Class for collecting all search results into a dictionary {dn:entry}
"""
def __init__(self,l):
AsyncSearchHandler.__init__(self,l)
self.allEntries = {}
def _processSingleResult(self,resultType,resultItem):
if resultType in ENTRY_RESULT_TYPES:
# Search continuations are ignored
dn,entry = resultItem
self.allEntries[dn] = entry
class IndexedDict(Dict):
"""
Class for collecting all search results into a dictionary {dn:entry}
and maintain case-sensitive equality indexes to entries
"""
def __init__(self,l,indexed_attrs=None):
Dict.__init__(self,l)
self.indexed_attrs = indexed_attrs or ()
self.index = {}.fromkeys(self.indexed_attrs,{})
def _processSingleResult(self,resultType,resultItem):
if resultType in ENTRY_RESULT_TYPES:
# Search continuations are ignored
dn,entry = resultItem
self.allEntries[dn] = entry
for a in self.indexed_attrs:
if a in entry:
for v in entry[a]:
try:
self.index[a][v].append(dn)
except KeyError:
self.index[a][v] = [ dn ]
class FileWriter(AsyncSearchHandler):
"""
Class for writing a stream of LDAP search results to a file object
Arguments:
l
LDAPObject instance
f
File object instance where the LDIF data is written to
"""
def __init__(self,l,f,headerStr='',footerStr=''):
AsyncSearchHandler.__init__(self,l)
self._f = f
self.headerStr = headerStr
self.footerStr = footerStr
def preProcessing(self):
"""
The headerStr is written to output after starting search but
before receiving and processing results.
"""
self._f.write(self.headerStr)
def postProcessing(self):
"""
The footerStr is written to output after receiving and
processing results.
"""
self._f.write(self.footerStr)
class LDIFWriter(FileWriter):
"""
Class for writing a stream LDAP search results to a LDIF file
Arguments:
l
LDAPObject instance
writer_obj
Either a file-like object or a ldif.LDIFWriter instance used for output
"""
def __init__(self,l,writer_obj,headerStr='',footerStr=''):
if isinstance(writer_obj,ldif.LDIFWriter):
self._ldif_writer = writer_obj
else:
self._ldif_writer = ldif.LDIFWriter(writer_obj)
FileWriter.__init__(self,l,self._ldif_writer._output_file,headerStr,footerStr)
def _processSingleResult(self,resultType,resultItem):
if resultType in ENTRY_RESULT_TYPES:
# Search continuations are ignored
dn,entry = resultItem
self._ldif_writer.unparse(dn,entry)
@@ -0,0 +1,100 @@
"""
This is a convenience wrapper for dictionaries
returned from LDAP servers containing attribute
names of variable case.
See https://www.python-ldap.org/ for details.
"""
from ldap import __version__
from ldap.compat import IterableUserDict
class cidict(IterableUserDict):
"""
Case-insensitive but case-respecting dictionary.
"""
def __init__(self,default=None):
self._keys = {}
IterableUserDict.__init__(self,{})
self.update(default or {})
def __getitem__(self,key):
return self.data[key.lower()]
def __setitem__(self,key,value):
lower_key = key.lower()
self._keys[lower_key] = key
self.data[lower_key] = value
def __delitem__(self,key):
lower_key = key.lower()
del self._keys[lower_key]
del self.data[lower_key]
def update(self,dict):
for key, value in dict.items():
self[key] = value
def has_key(self,key):
return key in self
def __contains__(self,key):
return IterableUserDict.__contains__(self, key.lower())
def __iter__(self):
return iter(self.keys())
def keys(self):
return self._keys.values()
def items(self):
result = []
for k in self._keys.values():
result.append((k,self[k]))
return result
def strlist_minus(a,b):
"""
Return list of all items in a which are not in b (a - b).
a,b are supposed to be lists of case-insensitive strings.
"""
temp = cidict()
for elt in b:
temp[elt] = elt
result = [
elt
for elt in a
if elt not in temp
]
return result
def strlist_intersection(a,b):
"""
Return intersection of two lists of case-insensitive strings a,b.
"""
temp = cidict()
for elt in a:
temp[elt] = elt
result = [
temp[elt]
for elt in b
if elt in temp
]
return result
def strlist_union(a,b):
"""
Return union of two lists of case-insensitive strings a,b.
"""
temp = cidict()
for elt in a:
temp[elt] = elt
for elt in b:
temp[elt] = elt
return temp.values()
@@ -0,0 +1,113 @@
"""Compatibility wrappers for Py2/Py3."""
import sys
import os
if sys.version_info[0] < 3:
from UserDict import UserDict, IterableUserDict
from urllib import quote
from urllib import quote_plus
from urllib import unquote as urllib_unquote
from urllib import urlopen
from urlparse import urlparse
def unquote(uri):
"""Specialized unquote that uses UTF-8 for parsing."""
uri = uri.encode('ascii')
unquoted = urllib_unquote(uri)
return unquoted.decode('utf-8')
# Old-style of re-raising an exception is SyntaxError in Python 3,
# so hide behind exec() so the Python 3 parser doesn't see it
exec('''def reraise(exc_type, exc_value, exc_traceback):
"""Re-raise an exception given information from sys.exc_info()
Note that unlike six.reraise, this does not support replacing the
traceback. All arguments must come from a single sys.exc_info() call.
"""
raise exc_type, exc_value, exc_traceback
''')
else:
from collections import UserDict
IterableUserDict = UserDict
from urllib.parse import quote, quote_plus, unquote, urlparse
from urllib.request import urlopen
def reraise(exc_type, exc_value, exc_traceback):
"""Re-raise an exception given information from sys.exc_info()
Note that unlike six.reraise, this does not support replacing the
traceback. All arguments must come from a single sys.exc_info() call.
"""
# In Python 3, all exception info is contained in one object.
raise exc_value
try:
from shutil import which
except ImportError:
# shutil.which() from Python 3.6
# "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
# 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation;
# All Rights Reserved"
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
of os.environ.get("PATH"), or can be overridden with a custom search
path.
"""
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return (os.path.exists(fn) and os.access(fn, mode)
and not os.path.isdir(fn))
# If we're given a path with a directory part, look it up directly rather
# than referring to PATH directories. This includes checking relative to the
# current directory, e.g. ./script
if os.path.dirname(cmd):
if _access_check(cmd, mode):
return cmd
return None
if path is None:
path = os.environ.get("PATH", os.defpath)
if not path:
return None
path = path.split(os.pathsep)
if sys.platform == "win32":
# The current directory takes precedence on Windows.
if not os.curdir in path:
path.insert(0, os.curdir)
# PATHEXT is necessary to check on Windows.
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
# See if the given file matches any of the expected path extensions.
# This will allow us to short circuit when given "python.exe".
# If it does match, only test that one, otherwise we have to try
# others.
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
files = [cmd]
else:
files = [cmd + ext for ext in pathext]
else:
# On other platforms you don't have things like PATHEXT to tell you
# what file suffixes are executable, so just pass on cmd as-is.
files = [cmd]
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if not normdir in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
if _access_check(name, mode):
return name
return None
@@ -0,0 +1,404 @@
"""Definitions for constants exported by OpenLDAP
This file lists all constants we know about, even those that aren't
available in the OpenLDAP version python-ldap is compiled against.
The information serves two purposes:
- Generate a C header with the constants
- Provide support for building documentation without compiling python-ldap
"""
# This module cannot import anything from ldap.
# When building documentation, it is used to initialize ldap.__init__.
from __future__ import print_function
class Constant(object):
"""Base class for a definition of an OpenLDAP constant
"""
def __init__(self, name, optional=False, requirements=(), doc=None):
self.name = name
if optional:
self_requirement = 'defined(LDAP_{})'.format(self.name)
requirements = list(requirements) + [self_requirement]
self.requirements = requirements
self.doc = self.__doc__ = doc
class Error(Constant):
"""Definition for an OpenLDAP error code
This is a constant at the C level; in Python errors are provided as
exception classes.
"""
c_template = 'add_err({self.name});'
class Int(Constant):
"""Definition for an OpenLDAP integer constant"""
c_template = 'add_int({self.name});'
class TLSInt(Int):
"""Definition for a TLS integer constant -- requires HAVE_TLS"""
def __init__(self, *args, **kwargs):
requrements = list(kwargs.get('requirements', ()))
kwargs['requirements'] = ['HAVE_TLS'] + requrements
super(TLSInt, self).__init__(*args, **kwargs)
class Feature(Constant):
"""Definition for a feature: 0 or 1 based on a C #ifdef
"""
c_template = '\n'.join([
'',
'#ifdef {self.c_feature}',
'if (PyModule_AddIntConstant(m, "{self.name}", 1) != 0) return -1;',
'#else',
'if (PyModule_AddIntConstant(m, "{self.name}", 0) != 0) return -1;',
'#endif',
'',
])
def __init__(self, name, c_feature, **kwargs):
super(Feature, self).__init__(name, **kwargs)
self.c_feature = c_feature
class Str(Constant):
c_template = 'add_string({self.name});'
API_2004 = 'LDAP_API_VERSION >= 2004'
CONSTANTS = (
Error('ADMINLIMIT_EXCEEDED'),
Error('AFFECTS_MULTIPLE_DSAS'),
Error('ALIAS_DEREF_PROBLEM'),
Error('ALIAS_PROBLEM'),
Error('ALREADY_EXISTS'),
Error('AUTH_METHOD_NOT_SUPPORTED'),
Error('AUTH_UNKNOWN'),
Error('BUSY'),
Error('CLIENT_LOOP'),
Error('COMPARE_FALSE'),
Error('COMPARE_TRUE'),
Error('CONFIDENTIALITY_REQUIRED'),
Error('CONNECT_ERROR'),
Error('CONSTRAINT_VIOLATION'),
Error('CONTROL_NOT_FOUND'),
Error('DECODING_ERROR'),
Error('ENCODING_ERROR'),
Error('FILTER_ERROR'),
Error('INAPPROPRIATE_AUTH'),
Error('INAPPROPRIATE_MATCHING'),
Error('INSUFFICIENT_ACCESS'),
Error('INVALID_CREDENTIALS'),
Error('INVALID_DN_SYNTAX'),
Error('INVALID_SYNTAX'),
Error('IS_LEAF'),
Error('LOCAL_ERROR'),
Error('LOOP_DETECT'),
Error('MORE_RESULTS_TO_RETURN'),
Error('NAMING_VIOLATION'),
Error('NO_MEMORY'),
Error('NO_OBJECT_CLASS_MODS'),
Error('NO_OBJECT_CLASS_MODS'),
Error('NO_RESULTS_RETURNED'),
Error('NO_SUCH_ATTRIBUTE'),
Error('NO_SUCH_OBJECT'),
Error('NOT_ALLOWED_ON_NONLEAF'),
Error('NOT_ALLOWED_ON_RDN'),
Error('NOT_SUPPORTED'),
Error('OBJECT_CLASS_VIOLATION'),
Error('OPERATIONS_ERROR'),
Error('OTHER'),
Error('PARAM_ERROR'),
Error('PARTIAL_RESULTS'),
Error('PROTOCOL_ERROR'),
Error('REFERRAL'),
Error('REFERRAL_LIMIT_EXCEEDED'),
Error('RESULTS_TOO_LARGE'),
Error('SASL_BIND_IN_PROGRESS'),
Error('SERVER_DOWN'),
Error('SIZELIMIT_EXCEEDED'),
Error('STRONG_AUTH_NOT_SUPPORTED'),
Error('STRONG_AUTH_REQUIRED'),
Error('SUCCESS'),
Error('TIMELIMIT_EXCEEDED'),
Error('TIMEOUT'),
Error('TYPE_OR_VALUE_EXISTS'),
Error('UNAVAILABLE'),
Error('UNAVAILABLE_CRITICAL_EXTENSION'),
Error('UNDEFINED_TYPE'),
Error('UNWILLING_TO_PERFORM'),
Error('USER_CANCELLED'),
Error('VLV_ERROR'),
Error('X_PROXY_AUTHZ_FAILURE'),
Error('CANCELLED', requirements=['defined(LDAP_API_FEATURE_CANCEL)']),
Error('NO_SUCH_OPERATION', requirements=['defined(LDAP_API_FEATURE_CANCEL)']),
Error('TOO_LATE', requirements=['defined(LDAP_API_FEATURE_CANCEL)']),
Error('CANNOT_CANCEL', requirements=['defined(LDAP_API_FEATURE_CANCEL)']),
Error('ASSERTION_FAILED', optional=True),
Error('PROXIED_AUTHORIZATION_DENIED', optional=True),
# simple constants
Int('API_VERSION'),
Int('VENDOR_VERSION'),
Int('PORT'),
Int('VERSION1'),
Int('VERSION2'),
Int('VERSION3'),
Int('VERSION_MIN'),
Int('VERSION'),
Int('VERSION_MAX'),
Int('TAG_MESSAGE'),
Int('TAG_MSGID'),
Int('REQ_BIND'),
Int('REQ_UNBIND'),
Int('REQ_SEARCH'),
Int('REQ_MODIFY'),
Int('REQ_ADD'),
Int('REQ_DELETE'),
Int('REQ_MODRDN'),
Int('REQ_COMPARE'),
Int('REQ_ABANDON'),
Int('TAG_LDAPDN'),
Int('TAG_LDAPCRED'),
Int('TAG_CONTROLS'),
Int('TAG_REFERRAL'),
Int('REQ_EXTENDED'),
Int('TAG_NEWSUPERIOR', requirements=[API_2004]),
Int('TAG_EXOP_REQ_OID', requirements=[API_2004]),
Int('TAG_EXOP_REQ_VALUE', requirements=[API_2004]),
Int('TAG_EXOP_RES_OID', requirements=[API_2004]),
Int('TAG_EXOP_RES_VALUE', requirements=[API_2004]),
Int('TAG_SASL_RES_CREDS', requirements=[API_2004, 'defined(HAVE_SASL)']),
Int('SASL_AUTOMATIC'),
Int('SASL_INTERACTIVE'),
Int('SASL_QUIET'),
# reversibles
Int('RES_BIND'),
Int('RES_SEARCH_ENTRY'),
Int('RES_SEARCH_RESULT'),
Int('RES_MODIFY'),
Int('RES_ADD'),
Int('RES_DELETE'),
Int('RES_MODRDN'),
Int('RES_COMPARE'),
Int('RES_ANY'),
Int('RES_SEARCH_REFERENCE'),
Int('RES_EXTENDED'),
Int('RES_UNSOLICITED'),
Int('RES_INTERMEDIATE'),
# non-reversibles
Int('AUTH_NONE'),
Int('AUTH_SIMPLE'),
Int('SCOPE_BASE'),
Int('SCOPE_ONELEVEL'),
Int('SCOPE_SUBTREE'),
Int('SCOPE_SUBORDINATE', optional=True),
Int('MOD_ADD'),
Int('MOD_DELETE'),
Int('MOD_REPLACE'),
Int('MOD_INCREMENT'),
Int('MOD_BVALUES'),
Int('MSG_ONE'),
Int('MSG_ALL'),
Int('MSG_RECEIVED'),
# (error constants handled above)
Int('DEREF_NEVER'),
Int('DEREF_SEARCHING'),
Int('DEREF_FINDING'),
Int('DEREF_ALWAYS'),
Int('NO_LIMIT'),
Int('OPT_API_INFO'),
Int('OPT_DEREF'),
Int('OPT_SIZELIMIT'),
Int('OPT_TIMELIMIT'),
Int('OPT_REFERRALS', optional=True),
Int('OPT_ERROR_NUMBER'),
Int('OPT_RESTART'),
Int('OPT_PROTOCOL_VERSION'),
Int('OPT_SERVER_CONTROLS'),
Int('OPT_CLIENT_CONTROLS'),
Int('OPT_API_FEATURE_INFO'),
Int('OPT_HOST_NAME'),
Int('OPT_DESC'),
Int('OPT_DIAGNOSTIC_MESSAGE'),
Int('OPT_ERROR_STRING'),
Int('OPT_MATCHED_DN'),
Int('OPT_DEBUG_LEVEL'),
Int('OPT_TIMEOUT'),
Int('OPT_REFHOPLIMIT'),
Int('OPT_NETWORK_TIMEOUT'),
Int('OPT_URI'),
Int('OPT_DEFBASE', optional=True),
TLSInt('OPT_X_TLS', optional=True),
TLSInt('OPT_X_TLS_CTX'),
TLSInt('OPT_X_TLS_CACERTFILE'),
TLSInt('OPT_X_TLS_CACERTDIR'),
TLSInt('OPT_X_TLS_CERTFILE'),
TLSInt('OPT_X_TLS_KEYFILE'),
TLSInt('OPT_X_TLS_REQUIRE_CERT'),
TLSInt('OPT_X_TLS_CIPHER_SUITE'),
TLSInt('OPT_X_TLS_RANDOM_FILE'),
TLSInt('OPT_X_TLS_DHFILE'),
TLSInt('OPT_X_TLS_NEVER'),
TLSInt('OPT_X_TLS_HARD'),
TLSInt('OPT_X_TLS_DEMAND'),
TLSInt('OPT_X_TLS_ALLOW'),
TLSInt('OPT_X_TLS_TRY'),
TLSInt('OPT_X_TLS_PEERCERT', optional=True),
TLSInt('OPT_X_TLS_VERSION', optional=True),
TLSInt('OPT_X_TLS_CIPHER', optional=True),
TLSInt('OPT_X_TLS_PEERCERT', optional=True),
# only available if OpenSSL supports it => might cause
# backward compatibility problems
TLSInt('OPT_X_TLS_CRLCHECK', optional=True),
TLSInt('OPT_X_TLS_CRLFILE', optional=True),
TLSInt('OPT_X_TLS_CRL_NONE'),
TLSInt('OPT_X_TLS_CRL_PEER'),
TLSInt('OPT_X_TLS_CRL_ALL'),
TLSInt('OPT_X_TLS_NEWCTX', optional=True),
TLSInt('OPT_X_TLS_PROTOCOL_MIN', optional=True),
TLSInt('OPT_X_TLS_PACKAGE', optional=True),
Int('OPT_X_SASL_MECH'),
Int('OPT_X_SASL_REALM'),
Int('OPT_X_SASL_AUTHCID'),
Int('OPT_X_SASL_AUTHZID'),
Int('OPT_X_SASL_SSF'),
Int('OPT_X_SASL_SSF_EXTERNAL'),
Int('OPT_X_SASL_SECPROPS'),
Int('OPT_X_SASL_SSF_MIN'),
Int('OPT_X_SASL_SSF_MAX'),
Int('OPT_X_SASL_NOCANON', optional=True),
Int('OPT_X_SASL_USERNAME', optional=True),
Int('OPT_CONNECT_ASYNC', optional=True),
Int('OPT_X_KEEPALIVE_IDLE', optional=True),
Int('OPT_X_KEEPALIVE_PROBES', optional=True),
Int('OPT_X_KEEPALIVE_INTERVAL', optional=True),
Int('DN_FORMAT_LDAP'),
Int('DN_FORMAT_LDAPV3'),
Int('DN_FORMAT_LDAPV2'),
Int('DN_FORMAT_DCE'),
Int('DN_FORMAT_UFN'),
Int('DN_FORMAT_AD_CANONICAL'),
# Int('DN_FORMAT_LBER'), # for testing only
Int('DN_FORMAT_MASK'),
Int('DN_PRETTY'),
Int('DN_SKIP'),
Int('DN_P_NOLEADTRAILSPACES'),
Int('DN_P_NOSPACEAFTERRDN'),
Int('DN_PEDANTIC'),
Int('AVA_NULL'),
Int('AVA_STRING'),
Int('AVA_BINARY'),
Int('AVA_NONPRINTABLE'),
Int('OPT_SUCCESS'),
# XXX - these should be errors
Int('URL_ERR_BADSCOPE'),
Int('URL_ERR_MEM'),
# Int('LIBLDAP_R'),
Feature('LIBLDAP_R', 'HAVE_LIBLDAP_R'),
Feature('SASL_AVAIL', 'HAVE_SASL'),
Feature('TLS_AVAIL', 'HAVE_TLS'),
Str("CONTROL_MANAGEDSAIT"),
Str("CONTROL_PROXY_AUTHZ"),
Str("CONTROL_SUBENTRIES"),
Str("CONTROL_VALUESRETURNFILTER"),
Str("CONTROL_ASSERT"),
Str("CONTROL_PRE_READ"),
Str("CONTROL_POST_READ"),
Str("CONTROL_SORTREQUEST"),
Str("CONTROL_SORTRESPONSE"),
Str("CONTROL_PAGEDRESULTS"),
Str("CONTROL_SYNC"),
Str("CONTROL_SYNC_STATE"),
Str("CONTROL_SYNC_DONE"),
Str("SYNC_INFO"),
Str("CONTROL_PASSWORDPOLICYREQUEST"),
Str("CONTROL_PASSWORDPOLICYRESPONSE"),
Str("CONTROL_RELAX"),
)
def print_header(): # pragma: no cover
"""Print the C header file to standard output"""
print('/*')
print(' * Generated with:')
print(' * python Lib/ldap/constants.py > Modules/constants_generated.h')
print(' *')
print(' * Please do any modifications there, then re-generate this file')
print(' */')
print('')
current_requirements = []
def pop_requirement():
popped = current_requirements.pop()
print('#endif')
print()
for definition in CONSTANTS:
while not set(current_requirements).issubset(definition.requirements):
pop_requirement()
for requirement in definition.requirements:
if requirement not in current_requirements:
current_requirements.append(requirement)
print()
print('#if {}'.format(requirement))
print(definition.c_template.format(self=definition))
while current_requirements:
pop_requirement()
if __name__ == '__main__':
print_header()
@@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
"""
controls.py - support classes for LDAP controls
See https://www.python-ldap.org/ for details.
Description:
The ldap.controls module provides LDAPControl classes.
Each class provides support for a certain control.
"""
from ldap.pkginfo import __version__
import _ldap
assert _ldap.__version__==__version__, \
ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__))
import ldap
from pyasn1.error import PyAsn1Error
__all__ = [
'KNOWN_RESPONSE_CONTROLS',
# Classes
'AssertionControl',
'BooleanControl',
'LDAPControl',
'ManageDSAITControl',
'MatchedValuesControl',
'RelaxRulesControl',
'RequestControl',
'ResponseControl',
'SimplePagedResultsControl',
'ValueLessRequestControl',
# Functions
'RequestControlTuples',
'DecodeControlTuples',
]
# response control OID to class registry
KNOWN_RESPONSE_CONTROLS = {}
class RequestControl:
"""
Base class for all request controls
controlType
OID as string of the LDAPv3 extended request control
criticality
sets the criticality of the control (boolean)
encodedControlValue
control value of the LDAPv3 extended request control
(here it is the BER-encoded ASN.1 control value)
"""
def __init__(self,controlType=None,criticality=False,encodedControlValue=None):
self.controlType = controlType
self.criticality = criticality
self.encodedControlValue = encodedControlValue
def encodeControlValue(self):
"""
sets class attribute encodedControlValue to the BER-encoded ASN.1
control value composed by class attributes set before
"""
return self.encodedControlValue
class ResponseControl:
"""
Base class for all response controls
controlType
OID as string of the LDAPv3 extended response control
criticality
sets the criticality of the received control (boolean)
"""
def __init__(self,controlType=None,criticality=False):
self.controlType = controlType
self.criticality = criticality
def decodeControlValue(self,encodedControlValue):
"""
decodes the BER-encoded ASN.1 control value and sets the appropriate
class attributes
"""
self.encodedControlValue = encodedControlValue
class LDAPControl(RequestControl,ResponseControl):
"""
Base class for combined request/response controls mainly
for backward-compatibility to python-ldap 2.3.x
"""
def __init__(self,controlType=None,criticality=False,controlValue=None,encodedControlValue=None):
self.controlType = controlType
self.criticality = criticality
self.controlValue = controlValue
self.encodedControlValue = encodedControlValue
def RequestControlTuples(ldapControls):
"""
Return list of readily encoded 3-tuples which can be directly
passed to C module _ldap
ldapControls
sequence-type of RequestControl objects
"""
if ldapControls is None:
return None
else:
result = [
(c.controlType,c.criticality,c.encodeControlValue())
for c in ldapControls
]
return result
def DecodeControlTuples(ldapControlTuples,knownLDAPControls=None):
"""
Returns list of readily decoded ResponseControl objects
ldapControlTuples
Sequence-type of 3-tuples returned by _ldap.result4() containing
the encoded ASN.1 control values of response controls.
knownLDAPControls
Dictionary mapping extended control's OID to ResponseControl class
of response controls known by the application. If None
ldap.controls.KNOWN_RESPONSE_CONTROLS is used here.
"""
knownLDAPControls = knownLDAPControls or KNOWN_RESPONSE_CONTROLS
result = []
for controlType,criticality,encodedControlValue in ldapControlTuples or []:
try:
control = knownLDAPControls[controlType]()
except KeyError:
if criticality:
raise ldap.UNAVAILABLE_CRITICAL_EXTENSION('Received unexpected critical response control with controlType %s' % (repr(controlType)))
else:
control.controlType,control.criticality = controlType,criticality
try:
control.decodeControlValue(encodedControlValue)
except PyAsn1Error:
if criticality:
raise
else:
result.append(control)
return result
# Import the standard sub-modules
from ldap.controls.simple import *
from ldap.controls.libldap import *
@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.deref - classes for
(see https://tools.ietf.org/html/draft-masarati-ldap-deref)
See https://www.python-ldap.org/ for project details.
"""
__all__ = [
'DEREF_CONTROL_OID',
'DereferenceControl',
]
import ldap.controls
from ldap.controls import LDAPControl,KNOWN_RESPONSE_CONTROLS
import pyasn1_modules.rfc2251
from pyasn1.type import namedtype,univ,tag
from pyasn1.codec.ber import encoder,decoder
from pyasn1_modules.rfc2251 import LDAPDN,AttributeDescription,AttributeDescriptionList,AttributeValue
DEREF_CONTROL_OID = '1.3.6.1.4.1.4203.666.5.16'
# Request types
#---------------------------------------------------------------------------
# For compatibility with ASN.1 declaration in I-D
AttributeList = AttributeDescriptionList
class DerefSpec(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType(
'derefAttr',
AttributeDescription()
),
namedtype.NamedType(
'attributes',
AttributeList()
),
)
class DerefSpecs(univ.SequenceOf):
componentType = DerefSpec()
# Response types
#---------------------------------------------------------------------------
class AttributeValues(univ.SetOf):
componentType = AttributeValue()
class PartialAttribute(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('type', AttributeDescription()),
namedtype.NamedType('vals', AttributeValues()),
)
class PartialAttributeList(univ.SequenceOf):
componentType = PartialAttribute()
tagSet = univ.Sequence.tagSet.tagImplicitly(
tag.Tag(tag.tagClassContext,tag.tagFormatConstructed,0)
)
class DerefRes(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('derefAttr', AttributeDescription()),
namedtype.NamedType('derefVal', LDAPDN()),
namedtype.OptionalNamedType('attrVals', PartialAttributeList()),
)
class DerefResultControlValue(univ.SequenceOf):
componentType = DerefRes()
class DereferenceControl(LDAPControl):
controlType = DEREF_CONTROL_OID
def __init__(self,criticality=False,derefSpecs=None):
LDAPControl.__init__(self,self.controlType,criticality)
self.derefSpecs = derefSpecs or {}
def _derefSpecs(self):
deref_specs = DerefSpecs()
i = 0
for deref_attr,deref_attribute_names in self.derefSpecs.items():
deref_spec = DerefSpec()
deref_attributes = AttributeList()
for j in range(len(deref_attribute_names)):
deref_attributes.setComponentByPosition(j,deref_attribute_names[j])
deref_spec.setComponentByName('derefAttr',AttributeDescription(deref_attr))
deref_spec.setComponentByName('attributes',deref_attributes)
deref_specs.setComponentByPosition(i,deref_spec)
i += 1
return deref_specs
def encodeControlValue(self):
return encoder.encode(self._derefSpecs())
def decodeControlValue(self,encodedControlValue):
decodedValue,_ = decoder.decode(encodedControlValue,asn1Spec=DerefResultControlValue())
self.derefRes = {}
for deref_res in decodedValue:
deref_attr,deref_val,deref_vals = deref_res[0],deref_res[1],deref_res[2]
partial_attrs_dict = {
str(tv[0]): [str(v) for v in tv[1]]
for tv in deref_vals or []
}
try:
self.derefRes[str(deref_attr)].append((str(deref_val),partial_attrs_dict))
except KeyError:
self.derefRes[str(deref_attr)] = [(str(deref_val),partial_attrs_dict)]
KNOWN_RESPONSE_CONTROLS[DereferenceControl.controlType] = DereferenceControl
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""
controls.libldap - LDAP controls wrapper classes with en-/decoding done
by OpenLDAP functions
See https://www.python-ldap.org/ for details.
"""
from ldap.pkginfo import __version__
import _ldap
assert _ldap.__version__==__version__, \
ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__))
import ldap
from ldap.controls import RequestControl,LDAPControl,KNOWN_RESPONSE_CONTROLS
class AssertionControl(RequestControl):
"""
LDAP Assertion control, as defined in RFC 4528
filterstr
LDAP filter string specifying which assertions have to match
so that the server processes the operation
"""
controlType = ldap.CONTROL_ASSERT
def __init__(self,criticality=True,filterstr='(objectClass=*)'):
self.criticality = criticality
self.filterstr = filterstr
def encodeControlValue(self):
return _ldap.encode_assertion_control(self.filterstr)
KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_ASSERT] = AssertionControl
class MatchedValuesControl(RequestControl):
"""
LDAP Matched Values control, as defined in RFC 3876
filterstr
LDAP filter string specifying which attribute values
should be returned
"""
controlType = ldap.CONTROL_VALUESRETURNFILTER
def __init__(self,criticality=False,filterstr='(objectClass=*)'):
self.criticality = criticality
self.filterstr = filterstr
def encodeControlValue(self):
return _ldap.encode_valuesreturnfilter_control(self.filterstr)
KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_VALUESRETURNFILTER] = MatchedValuesControl
class SimplePagedResultsControl(LDAPControl):
"""
LDAP Control Extension for Simple Paged Results Manipulation
size
Page size requested (number of entries to be returned)
cookie
Cookie string received with last page
"""
controlType = ldap.CONTROL_PAGEDRESULTS
def __init__(self,criticality=False,size=None,cookie=None):
self.criticality = criticality
self.size,self.cookie = size,cookie
def encodeControlValue(self):
return _ldap.encode_page_control(self.size,self.cookie)
def decodeControlValue(self,encodedControlValue):
self.size,self.cookie = _ldap.decode_page_control(encodedControlValue)
KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_PAGEDRESULTS] = SimplePagedResultsControl
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.openldap - classes for OpenLDAP-specific controls
See https://www.python-ldap.org/ for project details.
"""
import ldap.controls
from ldap.controls import ValueLessRequestControl,ResponseControl
from pyasn1.type import univ
from pyasn1.codec.ber import decoder
__all__ = [
'SearchNoOpControl',
'SearchNoOpMixIn',
]
class SearchNoOpControl(ValueLessRequestControl,ResponseControl):
"""
No-op control attached to search operations implementing sort of a
count operation
see https://www.openldap.org/its/index.cgi?findid=6598
"""
controlType = '1.3.6.1.4.1.4203.666.5.18'
def __init__(self,criticality=False):
self.criticality = criticality
class SearchNoOpControlValue(univ.Sequence):
pass
def decodeControlValue(self,encodedControlValue):
decodedValue,_ = decoder.decode(encodedControlValue,asn1Spec=self.SearchNoOpControlValue())
self.resultCode = int(decodedValue[0])
self.numSearchResults = int(decodedValue[1])
self.numSearchContinuations = int(decodedValue[2])
ldap.controls.KNOWN_RESPONSE_CONTROLS[SearchNoOpControl.controlType] = SearchNoOpControl
class SearchNoOpMixIn:
"""
Mix-in class to be used with class LDAPObject and friends.
It adds a convenience method noop_search_st() to LDAPObject
for easily using the no-op search control.
"""
def noop_search_st(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*)',timeout=-1):
try:
msg_id = self.search_ext(
base,
scope,
filterstr=filterstr,
attrlist=['1.1'],
timeout=timeout,
serverctrls=[SearchNoOpControl(criticality=True)],
)
_,_,_,search_response_ctrls = self.result3(msg_id,all=1,timeout=timeout)
except (
ldap.TIMEOUT,
ldap.TIMELIMIT_EXCEEDED,
ldap.SIZELIMIT_EXCEEDED,
ldap.ADMINLIMIT_EXCEEDED
) as e:
self.abandon(msg_id)
raise e
else:
noop_srch_ctrl = [
c
for c in search_response_ctrls
if c.controlType==SearchNoOpControl.controlType
]
if noop_srch_ctrl:
return noop_srch_ctrl[0].numSearchResults,noop_srch_ctrl[0].numSearchContinuations
else:
return (None,None)
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.paged - classes for Simple Paged control
(see RFC 2696)
See https://www.python-ldap.org/ for project details.
"""
__all__ = [
'SimplePagedResultsControl'
]
# Imports from python-ldap 2.4+
import ldap.controls
from ldap.controls import RequestControl,ResponseControl,KNOWN_RESPONSE_CONTROLS
# Imports from pyasn1
from pyasn1.type import tag,namedtype,univ,constraint
from pyasn1.codec.ber import encoder,decoder
from pyasn1_modules.rfc2251 import LDAPString
class PagedResultsControlValue(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('size',univ.Integer()),
namedtype.NamedType('cookie',LDAPString()),
)
class SimplePagedResultsControl(RequestControl,ResponseControl):
controlType = '1.2.840.113556.1.4.319'
def __init__(self,criticality=False,size=10,cookie=''):
self.criticality = criticality
self.size = size
self.cookie = cookie or ''
def encodeControlValue(self):
pc = PagedResultsControlValue()
pc.setComponentByName('size',univ.Integer(self.size))
pc.setComponentByName('cookie',LDAPString(self.cookie))
return encoder.encode(pc)
def decodeControlValue(self,encodedControlValue):
decodedValue,_ = decoder.decode(encodedControlValue,asn1Spec=PagedResultsControlValue())
self.size = int(decodedValue.getComponentByName('size'))
self.cookie = bytes(decodedValue.getComponentByName('cookie'))
KNOWN_RESPONSE_CONTROLS[SimplePagedResultsControl.controlType] = SimplePagedResultsControl
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.ppolicy - classes for Password Policy controls
(see https://tools.ietf.org/html/draft-behera-ldap-password-policy)
See https://www.python-ldap.org/ for project details.
"""
__all__ = [
'PasswordPolicyControl'
]
# Imports from python-ldap 2.4+
from ldap.controls import (
ResponseControl, ValueLessRequestControl, KNOWN_RESPONSE_CONTROLS
)
# Imports from pyasn1
from pyasn1.type import tag,namedtype,namedval,univ,constraint
from pyasn1.codec.der import decoder
class PasswordPolicyWarning(univ.Choice):
componentType = namedtype.NamedTypes(
namedtype.NamedType('timeBeforeExpiration',univ.Integer().subtype(
implicitTag=tag.Tag(tag.tagClassContext,tag.tagFormatSimple,0)
)),
namedtype.NamedType('graceAuthNsRemaining',univ.Integer().subtype(
implicitTag=tag.Tag(tag.tagClassContext,tag.tagFormatSimple,1)
)),
)
class PasswordPolicyError(univ.Enumerated):
namedValues = namedval.NamedValues(
('passwordExpired',0),
('accountLocked',1),
('changeAfterReset',2),
('passwordModNotAllowed',3),
('mustSupplyOldPassword',4),
('insufficientPasswordQuality',5),
('passwordTooShort',6),
('passwordTooYoung',7),
('passwordInHistory',8)
)
subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(0,1,2,3,4,5,6,7,8)
class PasswordPolicyResponseValue(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.OptionalNamedType(
'warning',
PasswordPolicyWarning().subtype(
implicitTag=tag.Tag(tag.tagClassContext,tag.tagFormatSimple,0)
),
),
namedtype.OptionalNamedType(
'error',PasswordPolicyError().subtype(
implicitTag=tag.Tag(tag.tagClassContext,tag.tagFormatSimple,1)
)
),
)
class PasswordPolicyControl(ValueLessRequestControl,ResponseControl):
controlType = '1.3.6.1.4.1.42.2.27.8.5.1'
def __init__(self,criticality=False):
self.criticality = criticality
def decodeControlValue(self,encodedControlValue):
ppolicyValue,_ = decoder.decode(encodedControlValue,asn1Spec=PasswordPolicyResponseValue())
self.timeBeforeExpiration = None
self.graceAuthNsRemaining = None
self.error = None
warning = ppolicyValue.getComponentByName('warning')
if warning.hasValue():
if 'timeBeforeExpiration' in warning:
self.timeBeforeExpiration = int(
warning.getComponentByName('timeBeforeExpiration'))
if 'graceAuthNsRemaining' in warning:
self.graceAuthNsRemaining = int(
warning.getComponentByName('graceAuthNsRemaining'))
error = ppolicyValue.getComponentByName('error')
if error.hasValue():
self.error = int(error)
KNOWN_RESPONSE_CONTROLS[PasswordPolicyControl.controlType] = PasswordPolicyControl
@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.psearch - classes for Persistent Search Control
(see https://tools.ietf.org/html/draft-ietf-ldapext-psearch)
See https://www.python-ldap.org/ for project details.
"""
__all__ = [
'PersistentSearchControl',
'EntryChangeNotificationControl',
'CHANGE_TYPES_INT',
'CHANGE_TYPES_STR',
]
# Imports from python-ldap 2.4+
import ldap.controls
from ldap.controls import RequestControl,ResponseControl,KNOWN_RESPONSE_CONTROLS
# Imports from pyasn1
from pyasn1.type import namedtype,namedval,univ,constraint
from pyasn1.codec.ber import encoder,decoder
from pyasn1_modules.rfc2251 import LDAPDN
#---------------------------------------------------------------------------
# Constants and classes for Persistent Search Control
#---------------------------------------------------------------------------
CHANGE_TYPES_INT = {
'add':1,
'delete':2,
'modify':4,
'modDN':8,
}
CHANGE_TYPES_STR = {v: k for k,v in CHANGE_TYPES_INT.items()}
class PersistentSearchControl(RequestControl):
"""
Implements the request control for persistent search.
changeTypes
List of strings specifying the types of changes returned by the server.
Setting to None requests all changes.
changesOnly
Boolean which indicates whether only changes are returned by the server.
returnECs
Boolean which indicates whether the server should return an
Entry Change Notification response control
"""
class PersistentSearchControlValue(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('changeTypes',univ.Integer()),
namedtype.NamedType('changesOnly',univ.Boolean()),
namedtype.NamedType('returnECs',univ.Boolean()),
)
controlType = "2.16.840.1.113730.3.4.3"
def __init__(self,criticality=True,changeTypes=None,changesOnly=False,returnECs=True):
self.criticality,self.changesOnly,self.returnECs = \
criticality,changesOnly,returnECs
self.changeTypes = changeTypes or CHANGE_TYPES_INT.values()
def encodeControlValue(self):
if not type(self.changeTypes)==type(0):
# Assume a sequence type of integers to be OR-ed
changeTypes_int = 0
for ct in self.changeTypes:
changeTypes_int = changeTypes_int|CHANGE_TYPES_INT.get(ct,ct)
self.changeTypes = changeTypes_int
p = self.PersistentSearchControlValue()
p.setComponentByName('changeTypes',univ.Integer(self.changeTypes))
p.setComponentByName('changesOnly',univ.Boolean(self.changesOnly))
p.setComponentByName('returnECs',univ.Boolean(self.returnECs))
return encoder.encode(p)
class ChangeType(univ.Enumerated):
namedValues = namedval.NamedValues(
('add',1),
('delete',2),
('modify',4),
('modDN',8),
)
subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(1,2,4,8)
class EntryChangeNotificationValue(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('changeType',ChangeType()),
namedtype.OptionalNamedType('previousDN', LDAPDN()),
namedtype.OptionalNamedType('changeNumber',univ.Integer()),
)
class EntryChangeNotificationControl(ResponseControl):
"""
Implements the response control for persistent search.
Class attributes with values extracted from the response control:
changeType
String indicating the type of change causing this result to be
returned by the server
previousDN
Old DN of the entry in case of a modrdn change
changeNumber
A change serial number returned by the server (optional).
"""
controlType = "2.16.840.1.113730.3.4.7"
def decodeControlValue(self,encodedControlValue):
ecncValue,_ = decoder.decode(encodedControlValue,asn1Spec=EntryChangeNotificationValue())
self.changeType = int(ecncValue.getComponentByName('changeType'))
previousDN = ecncValue.getComponentByName('previousDN')
if previousDN.hasValue():
self.previousDN = str(previousDN)
else:
self.previousDN = None
changeNumber = ecncValue.getComponentByName('changeNumber')
if changeNumber.hasValue():
self.changeNumber = int(changeNumber)
else:
self.changeNumber = None
return (self.changeType,self.previousDN,self.changeNumber)
KNOWN_RESPONSE_CONTROLS[EntryChangeNotificationControl.controlType] = EntryChangeNotificationControl
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.pwdpolicy - classes for Password Policy controls
(see https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy)
See https://www.python-ldap.org/ for project details.
"""
__all__ = [
'PasswordExpiringControl',
'PasswordExpiredControl',
]
# Imports from python-ldap 2.4+
import ldap.controls
from ldap.controls import RequestControl,ResponseControl,ValueLessRequestControl,KNOWN_RESPONSE_CONTROLS
class PasswordExpiringControl(ResponseControl):
"""
Indicates time in seconds when password will expire
"""
controlType = '2.16.840.1.113730.3.4.5'
def decodeControlValue(self,encodedControlValue):
self.gracePeriod = int(encodedControlValue)
KNOWN_RESPONSE_CONTROLS[PasswordExpiringControl.controlType] = PasswordExpiringControl
class PasswordExpiredControl(ResponseControl):
"""
Indicates that password is expired
"""
controlType = '2.16.840.1.113730.3.4.4'
def decodeControlValue(self,encodedControlValue):
self.passwordExpired = encodedControlValue=='0'
KNOWN_RESPONSE_CONTROLS[PasswordExpiredControl.controlType] = PasswordExpiredControl
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.readentry - classes for the Read Entry controls
(see RFC 4527)
See https://www.python-ldap.org/ for project details.
"""
import ldap
from pyasn1.codec.ber import encoder,decoder
from ldap.controls import LDAPControl,KNOWN_RESPONSE_CONTROLS
from pyasn1_modules.rfc2251 import AttributeDescriptionList,SearchResultEntry
class ReadEntryControl(LDAPControl):
"""
Base class for read entry control described in RFC 4527
attrList
list of attribute type names requested
Class attributes with values extracted from the response control:
dn
string holding the distinguished name of the LDAP entry
entry
dictionary holding the LDAP entry
"""
def __init__(self,criticality=False,attrList=None):
self.criticality,self.attrList,self.entry = criticality,attrList or [],None
def encodeControlValue(self):
attributeSelection = AttributeDescriptionList()
for i in range(len(self.attrList)):
attributeSelection.setComponentByPosition(i,self.attrList[i])
return encoder.encode(attributeSelection)
def decodeControlValue(self,encodedControlValue):
decodedEntry,_ = decoder.decode(encodedControlValue,asn1Spec=SearchResultEntry())
self.dn = str(decodedEntry[0])
self.entry = {}
for attr in decodedEntry[1]:
self.entry[str(attr[0])] = [ str(attr_value) for attr_value in attr[1] ]
class PreReadControl(ReadEntryControl):
"""
Class for pre-read control described in RFC 4527
attrList
list of attribute type names requested
Class attributes with values extracted from the response control:
dn
string holding the distinguished name of the LDAP entry
before the operation was done by the server
entry
dictionary holding the LDAP entry
before the operation was done by the server
"""
controlType = ldap.CONTROL_PRE_READ
KNOWN_RESPONSE_CONTROLS[PreReadControl.controlType] = PreReadControl
class PostReadControl(ReadEntryControl):
"""
Class for post-read control described in RFC 4527
attrList
list of attribute type names requested
Class attributes with values extracted from the response control:
dn
string holding the distinguished name of the LDAP entry
after the operation was done by the server
entry
dictionary holding the LDAP entry
after the operation was done by the server
"""
controlType = ldap.CONTROL_POST_READ
KNOWN_RESPONSE_CONTROLS[PostReadControl.controlType] = PostReadControl
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.sessiontrack - class for session tracking control
(see draft-wahl-ldap-session)
See https://www.python-ldap.org/ for project details.
"""
from ldap.controls import RequestControl
from pyasn1.type import namedtype,univ
from pyasn1.codec.ber import encoder
from pyasn1_modules.rfc2251 import LDAPString,LDAPOID
# OID constants
SESSION_TRACKING_CONTROL_OID = "1.3.6.1.4.1.21008.108.63.1"
SESSION_TRACKING_FORMAT_OID_RADIUS_ACCT_SESSION_ID = SESSION_TRACKING_CONTROL_OID+".1"
SESSION_TRACKING_FORMAT_OID_RADIUS_ACCT_MULTI_SESSION_ID = SESSION_TRACKING_CONTROL_OID+".2"
SESSION_TRACKING_FORMAT_OID_USERNAME = SESSION_TRACKING_CONTROL_OID+".3"
class SessionTrackingControl(RequestControl):
"""
Class for Session Tracking Control
Because criticality MUST be false for this control it cannot be set
from the application.
sessionSourceIp
IP address of the request source as string
sessionSourceName
Name of the request source as string
formatOID
OID as string specifying the format
sessionTrackingIdentifier
String containing a specific tracking ID
"""
class SessionIdentifierControlValue(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('sessionSourceIp',LDAPString()),
namedtype.NamedType('sessionSourceName',LDAPString()),
namedtype.NamedType('formatOID',LDAPOID()),
namedtype.NamedType('sessionTrackingIdentifier',LDAPString()),
)
controlType = SESSION_TRACKING_CONTROL_OID
def __init__(self,sessionSourceIp,sessionSourceName,formatOID,sessionTrackingIdentifier):
# criticality MUST be false for this control
self.criticality = False
self.sessionSourceIp,self.sessionSourceName,self.formatOID,self.sessionTrackingIdentifier = \
sessionSourceIp,sessionSourceName,formatOID,sessionTrackingIdentifier
def encodeControlValue(self):
s = self.SessionIdentifierControlValue()
s.setComponentByName('sessionSourceIp',LDAPString(self.sessionSourceIp))
s.setComponentByName('sessionSourceName',LDAPString(self.sessionSourceName))
s.setComponentByName('formatOID',LDAPOID(self.formatOID))
s.setComponentByName('sessionTrackingIdentifier',LDAPString(self.sessionTrackingIdentifier))
return encoder.encode(s)
@@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.simple - classes for some very simple LDAP controls
See https://www.python-ldap.org/ for details.
"""
import struct,ldap
from ldap.controls import RequestControl,ResponseControl,LDAPControl,KNOWN_RESPONSE_CONTROLS
class ValueLessRequestControl(RequestControl):
"""
Base class for controls without a controlValue.
The presence of the control in a LDAPv3 request changes the server's
behaviour when processing the request simply based on the controlType.
controlType
OID of the request control
criticality
criticality request control
"""
def __init__(self,controlType=None,criticality=False):
self.controlType = controlType
self.criticality = criticality
def encodeControlValue(self):
return None
class OctetStringInteger(LDAPControl):
"""
Base class with controlValue being unsigend integer values
integerValue
Integer to be sent as OctetString
"""
def __init__(self,controlType=None,criticality=False,integerValue=None):
self.controlType = controlType
self.criticality = criticality
self.integerValue = integerValue
def encodeControlValue(self):
return struct.pack('!Q',self.integerValue)
def decodeControlValue(self,encodedControlValue):
self.integerValue = struct.unpack('!Q',encodedControlValue)[0]
class BooleanControl(LDAPControl):
"""
Base class for simple request controls with boolean control value.
Constructor argument and class attribute:
booleanValue
Boolean (True/False or 1/0) which is the boolean controlValue.
"""
boolean2ber = { 1:'\x01\x01\xFF', 0:'\x01\x01\x00' }
ber2boolean = { '\x01\x01\xFF':1, '\x01\x01\x00':0 }
def __init__(self,controlType=None,criticality=False,booleanValue=False):
self.controlType = controlType
self.criticality = criticality
self.booleanValue = booleanValue
def encodeControlValue(self):
return self.boolean2ber[int(self.booleanValue)]
def decodeControlValue(self,encodedControlValue):
self.booleanValue = self.ber2boolean[encodedControlValue]
class ManageDSAITControl(ValueLessRequestControl):
"""
Manage DSA IT Control
"""
def __init__(self,criticality=False):
ValueLessRequestControl.__init__(self,ldap.CONTROL_MANAGEDSAIT,criticality=False)
KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_MANAGEDSAIT] = ManageDSAITControl
class RelaxRulesControl(ValueLessRequestControl):
"""
Relax Rules Control
"""
def __init__(self,criticality=False):
ValueLessRequestControl.__init__(self,ldap.CONTROL_RELAX,criticality=False)
KNOWN_RESPONSE_CONTROLS[ldap.CONTROL_RELAX] = RelaxRulesControl
class ProxyAuthzControl(RequestControl):
"""
Proxy Authorization Control
authzId
string containing the authorization ID indicating the identity
on behalf which the server should process the request
"""
def __init__(self,criticality,authzId):
RequestControl.__init__(self,ldap.CONTROL_PROXY_AUTHZ,criticality,authzId)
class AuthorizationIdentityRequestControl(ValueLessRequestControl):
"""
Authorization Identity Request and Response Controls
"""
controlType = '2.16.840.1.113730.3.4.16'
def __init__(self,criticality):
ValueLessRequestControl.__init__(self,self.controlType,criticality)
class AuthorizationIdentityResponseControl(ResponseControl):
"""
Authorization Identity Request and Response Controls
Class attributes:
authzId
decoded authorization identity
"""
controlType = '2.16.840.1.113730.3.4.15'
def decodeControlValue(self,encodedControlValue):
self.authzId = encodedControlValue
KNOWN_RESPONSE_CONTROLS[AuthorizationIdentityResponseControl.controlType] = AuthorizationIdentityResponseControl
class GetEffectiveRightsControl(RequestControl):
"""
Get Effective Rights Control
"""
def __init__(self,criticality,authzId=None):
RequestControl.__init__(self,'1.3.6.1.4.1.42.2.27.9.5.2',criticality,authzId)
@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.sss - classes for Server Side Sorting
(see RFC 2891)
See https://www.python-ldap.org/ for project details.
"""
__all__ = [
'SSSRequestControl',
'SSSResponseControl',
]
import ldap
from ldap.ldapobject import LDAPObject
from ldap.controls import (RequestControl, ResponseControl,
KNOWN_RESPONSE_CONTROLS, DecodeControlTuples)
from pyasn1.type import univ, namedtype, tag, namedval, constraint
from pyasn1.codec.ber import encoder, decoder
# SortKeyList ::= SEQUENCE OF SEQUENCE {
# attributeType AttributeDescription,
# orderingRule [0] MatchingRuleId OPTIONAL,
# reverseOrder [1] BOOLEAN DEFAULT FALSE }
class SortKeyType(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('attributeType', univ.OctetString()),
namedtype.OptionalNamedType('orderingRule',
univ.OctetString().subtype(
implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0)
)
),
namedtype.DefaultedNamedType('reverseOrder', univ.Boolean(False).subtype(
implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))))
class SortKeyListType(univ.SequenceOf):
componentType = SortKeyType()
class SSSRequestControl(RequestControl):
'''Order result server side
>>> s = SSSRequestControl(ordering_rules=['-cn'])
'''
controlType = '1.2.840.113556.1.4.473'
def __init__(
self,
criticality=False,
ordering_rules=None,
):
RequestControl.__init__(self,self.controlType,criticality)
self.ordering_rules = ordering_rules
if isinstance(ordering_rules, basestring):
ordering_rules = [ordering_rules]
for rule in ordering_rules:
rule = rule.split(':')
assert len(rule) < 3, 'syntax for ordering rule: [-]<attribute-type>[:ordering-rule]'
def asn1(self):
p = SortKeyListType()
for i, rule in enumerate(self.ordering_rules):
q = SortKeyType()
reverse_order = rule.startswith('-')
if reverse_order:
rule = rule[1:]
if ':' in rule:
attribute_type, ordering_rule = rule.split(':')
else:
attribute_type, ordering_rule = rule, None
q.setComponentByName('attributeType', attribute_type)
if ordering_rule:
q.setComponentByName('orderingRule', ordering_rule)
if reverse_order:
q.setComponentByName('reverseOrder', 1)
p.setComponentByPosition(i, q)
return p
def encodeControlValue(self):
return encoder.encode(self.asn1())
class SortResultType(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('sortResult', univ.Enumerated().subtype(
namedValues=namedval.NamedValues(
('success', 0),
('operationsError', 1),
('timeLimitExceeded', 3),
('strongAuthRequired', 8),
('adminLimitExceeded', 11),
('noSuchAttribute', 16),
('inappropriateMatching', 18),
('insufficientAccessRights', 50),
('busy', 51),
('unwillingToPerform', 53),
('other', 80)),
subtypeSpec=univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(
0, 1, 3, 8, 11, 16, 18, 50, 51, 53, 80))),
namedtype.OptionalNamedType('attributeType',
univ.OctetString().subtype(
implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0)
)
))
class SSSResponseControl(ResponseControl):
controlType = '1.2.840.113556.1.4.474'
def __init__(self,criticality=False):
ResponseControl.__init__(self,self.controlType,criticality)
def decodeControlValue(self, encoded):
p, rest = decoder.decode(encoded, asn1Spec=SortResultType())
assert not rest, 'all data could not be decoded'
sort_result = p.getComponentByName('sortResult')
self.sortResult = int(sort_result)
attribute_type = p.getComponentByName('attributeType')
if attribute_type.hasValue():
self.attributeType = attribute_type
else:
self.attributeType = None
# backward compatibility class attributes
self.result = self.sortResult
self.attribute_type_error = self.attributeType
KNOWN_RESPONSE_CONTROLS[SSSResponseControl.controlType] = SSSResponseControl
@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
"""
ldap.controls.vlv - classes for Virtual List View
(see draft-ietf-ldapext-ldapv3-vlv)
See https://www.python-ldap.org/ for project details.
"""
__all__ = [
'VLVRequestControl',
'VLVResponseControl',
]
import ldap
from ldap.ldapobject import LDAPObject
from ldap.controls import (RequestControl, ResponseControl,
KNOWN_RESPONSE_CONTROLS, DecodeControlTuples)
from pyasn1.type import univ, namedtype, tag, namedval, constraint
from pyasn1.codec.ber import encoder, decoder
class ByOffsetType(univ.Sequence):
tagSet = univ.Sequence.tagSet.tagImplicitly(
tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0))
componentType = namedtype.NamedTypes(
namedtype.NamedType('offset', univ.Integer()),
namedtype.NamedType('contentCount', univ.Integer()))
class TargetType(univ.Choice):
componentType = namedtype.NamedTypes(
namedtype.NamedType('byOffset', ByOffsetType()),
namedtype.NamedType('greaterThanOrEqual', univ.OctetString().subtype(
implicitTag=tag.Tag(tag.tagClassContext,
tag.tagFormatSimple, 1))))
class VirtualListViewRequestType(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('beforeCount', univ.Integer()),
namedtype.NamedType('afterCount', univ.Integer()),
namedtype.NamedType('target', TargetType()),
namedtype.OptionalNamedType('contextID', univ.OctetString()))
class VLVRequestControl(RequestControl):
controlType = '2.16.840.1.113730.3.4.9'
def __init__(
self,
criticality=False,
before_count=0,
after_count=0,
offset=None,
content_count=None,
greater_than_or_equal=None,
context_id=None,
):
RequestControl.__init__(self,self.controlType,criticality)
assert (offset is not None and content_count is not None) or \
greater_than_or_equal, \
ValueError(
'offset and content_count must be set together or greater_than_or_equal must be used'
)
self.before_count = before_count
self.after_count = after_count
self.offset = offset
self.content_count = content_count
self.greater_than_or_equal = greater_than_or_equal
self.context_id = context_id
def encodeControlValue(self):
p = VirtualListViewRequestType()
p.setComponentByName('beforeCount', self.before_count)
p.setComponentByName('afterCount', self.after_count)
if self.offset is not None and self.content_count is not None:
by_offset = ByOffsetType()
by_offset.setComponentByName('offset', self.offset)
by_offset.setComponentByName('contentCount', self.content_count)
target = TargetType()
target.setComponentByName('byOffset', by_offset)
elif self.greater_than_or_equal:
target = TargetType()
target.setComponentByName('greaterThanOrEqual',
self.greater_than_or_equal)
else:
raise NotImplementedError
p.setComponentByName('target', target)
return encoder.encode(p)
KNOWN_RESPONSE_CONTROLS[VLVRequestControl.controlType] = VLVRequestControl
class VirtualListViewResultType(univ.Enumerated):
namedValues = namedval.NamedValues(
('success', 0),
('operationsError', 1),
('protocolError', 3),
('unwillingToPerform', 53),
('insufficientAccessRights', 50),
('adminLimitExceeded', 11),
('innapropriateMatching', 18),
('sortControlMissing', 60),
('offsetRangeError', 61),
('other', 80),
)
class VirtualListViewResponseType(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('targetPosition', univ.Integer()),
namedtype.NamedType('contentCount', univ.Integer()),
namedtype.NamedType('virtualListViewResult',
VirtualListViewResultType()),
namedtype.OptionalNamedType('contextID', univ.OctetString()))
class VLVResponseControl(ResponseControl):
controlType = '2.16.840.1.113730.3.4.10'
def __init__(self,criticality=False):
ResponseControl.__init__(self,self.controlType,criticality)
def decodeControlValue(self,encoded):
p, rest = decoder.decode(encoded, asn1Spec=VirtualListViewResponseType())
assert not rest, 'all data could not be decoded'
self.targetPosition = int(p.getComponentByName('targetPosition'))
self.contentCount = int(p.getComponentByName('contentCount'))
virtual_list_view_result = p.getComponentByName('virtualListViewResult')
self.virtualListViewResult = int(virtual_list_view_result)
context_id = p.getComponentByName('contextID')
if context_id.hasValue():
self.contextID = str(context_id)
else:
self.contextID = None
# backward compatibility class attributes
self.target_position = self.targetPosition
self.content_count = self.contentCount
self.result = self.virtualListViewResult
self.context_id = self.contextID
KNOWN_RESPONSE_CONTROLS[VLVResponseControl.controlType] = VLVResponseControl
@@ -0,0 +1,122 @@
"""
dn.py - misc stuff for handling distinguished names (see RFC 4514)
See https://www.python-ldap.org/ for details.
"""
import sys
from ldap.pkginfo import __version__
import _ldap
assert _ldap.__version__==__version__, \
ImportError('ldap %s and _ldap %s version mismatch!' % (__version__,_ldap.__version__))
import ldap.functions
def escape_dn_chars(s):
"""
Escape all DN special characters found in s
with a back-slash (see RFC 4514, section 2.4)
"""
if s:
s = s.replace('\\','\\\\')
s = s.replace(',' ,'\\,')
s = s.replace('+' ,'\\+')
s = s.replace('"' ,'\\"')
s = s.replace('<' ,'\\<')
s = s.replace('>' ,'\\>')
s = s.replace(';' ,'\\;')
s = s.replace('=' ,'\\=')
s = s.replace('\000' ,'\\\000')
if s[0]=='#' or s[0]==' ':
s = ''.join(('\\',s))
if s[-1]==' ':
s = ''.join((s[:-1],'\\ '))
return s
def str2dn(dn,flags=0):
"""
This function takes a DN as string as parameter and returns
a decomposed DN. It's the inverse to dn2str().
flags describes the format of the dn
See also the OpenLDAP man-page ldap_str2dn(3)
"""
if not dn:
return []
if sys.version_info[0] < 3 and isinstance(dn, unicode):
dn = dn.encode('utf-8')
return ldap.functions._ldap_function_call(None,_ldap.str2dn,dn,flags)
def dn2str(dn):
"""
This function takes a decomposed DN as parameter and returns
a single string. It's the inverse to str2dn() but will always
return a DN in LDAPv3 format compliant to RFC 4514.
"""
return ','.join([
'+'.join([
'='.join((atype,escape_dn_chars(avalue or '')))
for atype,avalue,dummy in rdn])
for rdn in dn
])
def explode_dn(dn, notypes=False, flags=0):
"""
explode_dn(dn [, notypes=False [, flags=0]]) -> list
This function takes a DN and breaks it up into its component parts.
The notypes parameter is used to specify that only the component's
attribute values be returned and not the attribute types.
"""
if not dn:
return []
dn_decomp = str2dn(dn,flags)
rdn_list = []
for rdn in dn_decomp:
if notypes:
rdn_list.append('+'.join([
escape_dn_chars(avalue or '')
for atype,avalue,dummy in rdn
]))
else:
rdn_list.append('+'.join([
'='.join((atype,escape_dn_chars(avalue or '')))
for atype,avalue,dummy in rdn
]))
return rdn_list
def explode_rdn(rdn, notypes=False, flags=0):
"""
explode_rdn(rdn [, notypes=0 [, flags=0]]) -> list
This function takes a RDN and breaks it up into its component parts
if it is a multi-valued RDN.
The notypes parameter is used to specify that only the component's
attribute values be returned and not the attribute types.
"""
if not rdn:
return []
rdn_decomp = str2dn(rdn,flags)[0]
if notypes:
return [avalue or '' for atype,avalue,dummy in rdn_decomp]
else:
return ['='.join((atype,escape_dn_chars(avalue or ''))) for atype,avalue,dummy in rdn_decomp]
def is_dn(s,flags=0):
"""
Returns True is `s' can be parsed by ldap.dn.str2dn() like as a
distinguished host_name (DN), otherwise False is returned.
"""
try:
str2dn(s,flags)
except Exception:
return False
else:
return True
@@ -0,0 +1,67 @@
"""
controls.py - support classes for LDAPv3 extended operations
See https://www.python-ldap.org/ for details.
Description:
The ldap.extop module provides base classes for LDAPv3 extended operations.
Each class provides support for a certain extended operation request and
response.
"""
from ldap import __version__
class ExtendedRequest:
"""
Generic base class for a LDAPv3 extended operation request
requestName
OID as string of the LDAPv3 extended operation request
requestValue
value of the LDAPv3 extended operation request
(here it is the BER-encoded ASN.1 request value)
"""
def __init__(self,requestName,requestValue):
self.requestName = requestName
self.requestValue = requestValue
def __repr__(self):
return '%s(%s,%s)' % (self.__class__.__name__,self.requestName,self.requestValue)
def encodedRequestValue(self):
"""
returns the BER-encoded ASN.1 request value composed by class attributes
set before
"""
return self.requestValue
class ExtendedResponse:
"""
Generic base class for a LDAPv3 extended operation response
requestName
OID as string of the LDAPv3 extended operation response
encodedResponseValue
BER-encoded ASN.1 value of the LDAPv3 extended operation response
"""
def __init__(self,responseName,encodedResponseValue):
self.responseName = responseName
self.responseValue = self.decodeResponseValue(encodedResponseValue)
def __repr__(self):
return '%s(%s,%s)' % (self.__class__.__name__,self.responseName,self.responseValue)
def decodeResponseValue(self,value):
"""
decodes the BER-encoded ASN.1 extended operation response value and
sets the appropriate class attributes
"""
return value
# Import sub-modules
from ldap.extop.dds import *
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""
ldap.extop.dds - Classes for Dynamic Entries extended operations
(see RFC 2589)
See https://www.python-ldap.org/ for details.
"""
from ldap.extop import ExtendedRequest,ExtendedResponse
# Imports from pyasn1
from pyasn1.type import namedtype,univ,tag
from pyasn1.codec.der import encoder,decoder
from pyasn1_modules.rfc2251 import LDAPDN
class RefreshRequest(ExtendedRequest):
requestName = '1.3.6.1.4.1.1466.101.119.1'
defaultRequestTtl = 86400
class RefreshRequestValue(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType(
'entryName',
LDAPDN().subtype(
implicitTag=tag.Tag(tag.tagClassContext,tag.tagFormatSimple,0)
)
),
namedtype.NamedType(
'requestTtl',
univ.Integer().subtype(
implicitTag=tag.Tag(tag.tagClassContext,tag.tagFormatSimple,1)
)
),
)
def __init__(self,requestName=None,entryName=None,requestTtl=None):
self.entryName = entryName
self.requestTtl = requestTtl or self.defaultRequestTtl
def encodedRequestValue(self):
p = self.RefreshRequestValue()
p.setComponentByName(
'entryName',
LDAPDN(self.entryName).subtype(
implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple,0)
)
)
p.setComponentByName(
'requestTtl',
univ.Integer(self.requestTtl).subtype(
implicitTag=tag.Tag(tag.tagClassContext,tag.tagFormatSimple,1)
)
)
return encoder.encode(p)
class RefreshResponse(ExtendedResponse):
responseName = '1.3.6.1.4.1.1466.101.119.1'
class RefreshResponseValue(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType(
'responseTtl',
univ.Integer().subtype(
implicitTag=tag.Tag(tag.tagClassContext,tag.tagFormatSimple,1)
)
)
)
def decodeResponseValue(self,value):
respValue,_ = decoder.decode(value,asn1Spec=self.RefreshResponseValue())
self.responseTtl = int(respValue.getComponentByName('responseTtl'))
return self.responseTtl
@@ -0,0 +1,89 @@
"""
filters.py - misc stuff for handling LDAP filter strings (see RFC2254)
See https://www.python-ldap.org/ for details.
Compatibility:
- Tested with Python 2.0+
"""
from ldap import __version__
from ldap.functions import strf_secs
import time
def escape_filter_chars(assertion_value,escape_mode=0):
"""
Replace all special characters found in assertion_value
by quoted notation.
escape_mode
If 0 only special chars mentioned in RFC 4515 are escaped.
If 1 all NON-ASCII chars are escaped.
If 2 all chars are escaped.
"""
if escape_mode:
r = []
if escape_mode==1:
for c in assertion_value:
if c < '0' or c > 'z' or c in "\\*()":
c = "\\%02x" % ord(c)
r.append(c)
elif escape_mode==2:
for c in assertion_value:
r.append("\\%02x" % ord(c))
else:
raise ValueError('escape_mode must be 0, 1 or 2.')
s = ''.join(r)
else:
s = assertion_value.replace('\\', r'\5c')
s = s.replace(r'*', r'\2a')
s = s.replace(r'(', r'\28')
s = s.replace(r')', r'\29')
s = s.replace('\x00', r'\00')
return s
def filter_format(filter_template,assertion_values):
"""
filter_template
String containing %s as placeholder for assertion values.
assertion_values
List or tuple of assertion values. Length must match
count of %s in filter_template.
"""
return filter_template % tuple(escape_filter_chars(v) for v in assertion_values)
def time_span_filter(
filterstr='',
from_timestamp=0,
until_timestamp=None,
delta_attr='modifyTimestamp',
):
"""
If last_run_timestr is non-zero filterstr will be extended
"""
if until_timestamp is None:
until_timestamp = time.time()
if from_timestamp < 0:
from_timestamp = until_timestamp + from_timestamp
if from_timestamp > until_timestamp:
raise ValueError('from_timestamp %r must not be greater than until_timestamp %r' % (
from_timestamp, until_timestamp
))
return (
'(&'
'{filterstr}'
'({delta_attr}>={from_timestr})'
'(!({delta_attr}>={until_timestr}))'
')'
).format(
filterstr=filterstr,
delta_attr=delta_attr,
from_timestr=strf_secs(from_timestamp),
until_timestr=strf_secs(until_timestamp),
)
# end of time_span_filter()
@@ -0,0 +1,125 @@
"""
functions.py - wraps functions of module _ldap
See https://www.python-ldap.org/ for details.
"""
from ldap import __version__
__all__ = [
'open','initialize','init',
'explode_dn','explode_rdn',
'get_option','set_option',
'escape_str',
'strf_secs','strp_secs',
]
import sys,pprint,time,_ldap,ldap
from calendar import timegm
from ldap import LDAPError
from ldap.dn import explode_dn,explode_rdn
from ldap.ldapobject import LDAPObject
if __debug__:
# Tracing is only supported in debugging mode
import traceback
# See _raise_byteswarning in ldapobject.py
_LDAP_WARN_SKIP_FRAME = True
def _ldap_function_call(lock,func,*args,**kwargs):
"""
Wrapper function which locks and logs calls to function
lock
Instance of threading.Lock or compatible
func
Function to call with arguments passed in via *args and **kwargs
"""
if lock:
lock.acquire()
if __debug__:
if ldap._trace_level>=1:
ldap._trace_file.write('*** %s.%s %s\n' % (
'_ldap',func.__name__,
pprint.pformat((args,kwargs))
))
if ldap._trace_level>=9:
traceback.print_stack(limit=ldap._trace_stack_limit,file=ldap._trace_file)
try:
try:
result = func(*args,**kwargs)
finally:
if lock:
lock.release()
except LDAPError as e:
if __debug__ and ldap._trace_level>=2:
ldap._trace_file.write('=> LDAPError: %s\n' % (str(e)))
raise
if __debug__ and ldap._trace_level>=2:
ldap._trace_file.write('=> result:\n%s\n' % (pprint.pformat(result)))
return result
def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None, bytes_mode=None):
"""
Return LDAPObject instance by opening LDAP connection to
LDAP host specified by LDAP URL
Parameters:
uri
LDAP URL containing at least connection scheme and hostport,
e.g. ldap://localhost:389
trace_level
If non-zero a trace output of LDAP calls is generated.
trace_file
File object where to write the trace output to.
Default is to use stdout.
bytes_mode
Whether to enable :ref:`bytes_mode` for backwards compatibility under Py2.
"""
return LDAPObject(uri,trace_level,trace_file,trace_stack_limit,bytes_mode)
def get_option(option):
"""
get_option(name) -> value
Get the value of an LDAP global option.
"""
return _ldap_function_call(None,_ldap.get_option,option)
def set_option(option,invalue):
"""
set_option(name, value)
Set the value of an LDAP global option.
"""
return _ldap_function_call(None,_ldap.set_option,option,invalue)
def escape_str(escape_func,s,*args):
"""
Applies escape_func() to all items of `args' and returns a string based
on format string `s'.
"""
return s % tuple(escape_func(v) for v in args)
def strf_secs(secs):
"""
Convert seconds since epoch to a string compliant to LDAP syntax GeneralizedTime
"""
return time.strftime('%Y%m%d%H%M%SZ', time.gmtime(secs))
def strp_secs(dt_str):
"""
Convert LDAP syntax GeneralizedTime to seconds since epoch
"""
return timegm(time.strptime(dt_str, '%Y%m%d%H%M%SZ'))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
Helper class for using logging as trace file object
"""
import logging
class logging_file_class(object):
def __init__(self, logging_level):
self._logging_level = logging_level
def write(self, msg):
logging.log(self._logging_level, msg[:-1])
def flush(self):
return
logging_file_obj = logging_file_class(logging.DEBUG)
@@ -0,0 +1,97 @@
"""
ldap.modlist - create add/modify modlist's
See https://www.python-ldap.org/ for details.
"""
from ldap import __version__
import ldap
def addModlist(entry,ignore_attr_types=None):
"""Build modify list for call of method LDAPObject.add()"""
ignore_attr_types = {v.lower() for v in ignore_attr_types or []}
modlist = []
for attrtype, value in entry.items():
if attrtype.lower() in ignore_attr_types:
# This attribute type is ignored
continue
# Eliminate empty attr value strings in list
attrvaluelist = [item for item in value if item is not None]
if attrvaluelist:
modlist.append((attrtype, value))
return modlist # addModlist()
def modifyModlist(
old_entry,new_entry,ignore_attr_types=None,ignore_oldexistent=0,case_ignore_attr_types=None
):
"""
Build differential modify list for calling LDAPObject.modify()/modify_s()
old_entry
Dictionary holding the old entry
new_entry
Dictionary holding what the new entry should be
ignore_attr_types
List of attribute type names to be ignored completely
ignore_oldexistent
If non-zero attribute type names which are in old_entry
but are not found in new_entry at all are not deleted.
This is handy for situations where your application
sets attribute value to '' for deleting an attribute.
In most cases leave zero.
case_ignore_attr_types
List of attribute type names for which comparison will be made
case-insensitive
"""
ignore_attr_types = {v.lower() for v in ignore_attr_types or []}
case_ignore_attr_types = {v.lower() for v in case_ignore_attr_types or []}
modlist = []
attrtype_lower_map = {}
for a in old_entry.keys():
attrtype_lower_map[a.lower()]=a
for attrtype, value in new_entry.items():
attrtype_lower = attrtype.lower()
if attrtype_lower in ignore_attr_types:
# This attribute type is ignored
continue
# Filter away null-strings
new_value = [item for item in value if item is not None]
if attrtype_lower in attrtype_lower_map:
old_value = old_entry.get(attrtype_lower_map[attrtype_lower],[])
old_value = [item for item in old_value if item is not None]
del attrtype_lower_map[attrtype_lower]
else:
old_value = []
if not old_value and new_value:
# Add a new attribute to entry
modlist.append((ldap.MOD_ADD,attrtype,new_value))
elif old_value and new_value:
# Replace existing attribute
replace_attr_value = len(old_value)!=len(new_value)
if not replace_attr_value:
if attrtype_lower in case_ignore_attr_types:
old_value_set = {v.lower() for v in old_value}
new_value_set = {v.lower() for v in new_value}
else:
old_value_set = set(old_value)
new_value_set = set(new_value)
replace_attr_value = new_value_set != old_value_set
if replace_attr_value:
modlist.append((ldap.MOD_DELETE,attrtype,None))
modlist.append((ldap.MOD_ADD,attrtype,new_value))
elif old_value and not new_value:
# Completely delete an existing attribute
modlist.append((ldap.MOD_DELETE,attrtype,None))
if not ignore_oldexistent:
# Remove all attributes of old_entry which are not present
# in new_entry at all
for a, val in attrtype_lower_map.items():
if a in ignore_attr_types:
# This attribute type is ignored
continue
attrtype = val
modlist.append((ldap.MOD_DELETE,attrtype,None))
return modlist # modifyModlist()
@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
"""
meta attributes for packaging which does not import any dependencies
"""
__version__ = '3.1.0'
__author__ = u'python-ldap project'
__license__ = 'Python style'
@@ -0,0 +1,41 @@
"""
ldap.resiter - processing LDAP results with iterators
See https://www.python-ldap.org/ for details.
"""
from ldap.pkginfo import __version__, __author__, __license__
class ResultProcessor:
"""
Mix-in class used with ldap.ldapopbject.LDAPObject or derived classes.
"""
def allresults(self, msgid, timeout=-1, add_ctrls=0):
"""
Generator function which returns an iterator for processing all LDAP operation
results of the given msgid like retrieved with LDAPObject.result3() -> 4-tuple
"""
result_type, result_list, result_msgid, result_serverctrls, _, _ = \
self.result4(
msgid,
0,
timeout,
add_ctrls=add_ctrls
)
while result_type and result_list:
yield (
result_type,
result_list,
result_msgid,
result_serverctrls
)
result_type, result_list, result_msgid, result_serverctrls, _, _ = \
self.result4(
msgid,
0,
timeout,
add_ctrls=add_ctrls
)
return # allresults()
@@ -0,0 +1,134 @@
"""
sasl.py - support for SASL mechanism
See https://www.python-ldap.org/ for details.
Description:
The ldap.sasl module provides SASL authentication classes.
Each class provides support for one SASL mechanism. This is done by
implementing a callback() - method, which will be called by the
LDAPObject's sasl_bind_s() method
Implementing support for new sasl mechanism is very easy --- see
the examples of digest_md5 and gssapi.
"""
from ldap import __version__
if __debug__:
# Tracing is only supported in debugging mode
from ldap import _trace_level, _trace_file
# These are the SASL callback id's , as defined in sasl.h
CB_USER = 0x4001
CB_AUTHNAME = 0x4002
CB_LANGUAGE = 0x4003
CB_PASS = 0x4004
CB_ECHOPROMPT = 0x4005
CB_NOECHOPROMPT = 0x4006
CB_GETREALM = 0x4008
class sasl:
"""
This class handles SASL interactions for authentication.
If an instance of this class is passed to ldap's sasl_bind_s()
method, the library will call its callback() method. For
specific SASL authentication mechanisms, this method can be
overridden
"""
def __init__(self, cb_value_dict, mech):
"""
The (generic) base class takes a cb_value_dictionary of
question-answer pairs. Questions are specified by the respective
SASL callback id's. The mech argument is a string that specifies
the SASL mechaninsm to be uesd.
"""
self.cb_value_dict = cb_value_dict or {}
if not isinstance(mech, bytes):
mech = mech.encode('utf-8')
self.mech = mech
def callback(self, cb_id, challenge, prompt, defresult):
"""
The callback method will be called by the sasl_bind_s()
method several times. Each time it will provide the id, which
tells us what kind of information is requested (the CB_*
constants above). The challenge might be a short (English) text
or some binary string, from which the return value is calculated.
The prompt argument is always a human-readable description string;
The defresult is a default value provided by the sasl library
Currently, we do not use the challenge and prompt information, and
return only information which is stored in the self.cb_value_dict
cb_value_dictionary. Note that the current callback interface is not very
useful for writing generic sasl GUIs, which would need to know all
the questions to ask, before the answers are returned to the sasl
lib (in contrast to one question at a time).
Unicode strings are always converted to bytes.
"""
# The following print command might be useful for debugging
# new sasl mechanisms. So it is left here
cb_result = self.cb_value_dict.get(cb_id, defresult) or ''
if __debug__:
if _trace_level >= 1:
_trace_file.write("*** id=%d, challenge=%s, prompt=%s, defresult=%s\n-> %s\n" % (
cb_id,
challenge,
prompt,
repr(defresult),
repr(self.cb_value_dict.get(cb_result))
))
if not isinstance(cb_result, bytes):
cb_result = cb_result.encode('utf-8')
return cb_result
class cram_md5(sasl):
"""
This class handles SASL CRAM-MD5 authentication.
"""
def __init__(self, authc_id, password, authz_id=""):
auth_dict = {
CB_AUTHNAME: authc_id,
CB_PASS: password,
CB_USER: authz_id,
}
sasl.__init__(self, auth_dict, "CRAM-MD5")
class digest_md5(sasl):
"""
This class handles SASL DIGEST-MD5 authentication.
"""
def __init__(self, authc_id, password, authz_id=""):
auth_dict = {
CB_AUTHNAME: authc_id,
CB_PASS: password,
CB_USER: authz_id,
}
sasl.__init__(self, auth_dict, "DIGEST-MD5")
class gssapi(sasl):
"""
This class handles SASL GSSAPI (i.e. Kerberos V) authentication.
"""
def __init__(self, authz_id=""):
sasl.__init__(self, {CB_USER: authz_id}, "GSSAPI")
class external(sasl):
"""
This class handles SASL EXTERNAL authentication
(i.e. X.509 client certificate)
"""
def __init__(self, authz_id=""):
sasl.__init__(self, {CB_USER: authz_id}, "EXTERNAL")
@@ -0,0 +1,10 @@
"""
ldap.schema - LDAPv3 schema handling
See https://www.python-ldap.org/ for details.
"""
from ldap import __version__
from ldap.schema.subentry import SubSchema,SCHEMA_ATTRS,SCHEMA_CLASS_MAPPING,SCHEMA_ATTR_MAPPING,urlfetch
from ldap.schema.models import *
@@ -0,0 +1,701 @@
"""
schema.py - support for subSchemaSubEntry information
See https://www.python-ldap.org/ for details.
"""
import sys
import ldap.cidict
from ldap.compat import IterableUserDict
from ldap.schema.tokenizer import split_tokens,extract_tokens
NOT_HUMAN_READABLE_LDAP_SYNTAXES = {
'1.3.6.1.4.1.1466.115.121.1.4', # Audio
'1.3.6.1.4.1.1466.115.121.1.5', # Binary
'1.3.6.1.4.1.1466.115.121.1.8', # Certificate
'1.3.6.1.4.1.1466.115.121.1.9', # Certificate List
'1.3.6.1.4.1.1466.115.121.1.10', # Certificate Pair
'1.3.6.1.4.1.1466.115.121.1.23', # G3 FAX
'1.3.6.1.4.1.1466.115.121.1.28', # JPEG
'1.3.6.1.4.1.1466.115.121.1.40', # Octet String
'1.3.6.1.4.1.1466.115.121.1.49', # Supported Algorithm
}
class SchemaElement:
"""
Base class for all schema element classes. Not used directly!
Arguments:
schema_element_str
String which contains the schema element description to be parsed.
(Bytestrings are decoded using UTF-8)
Class attributes:
schema_attribute
LDAP attribute type containing a certain schema element description
token_defaults
Dictionary internally used by the schema element parser
containing the defaults for certain schema description key-words
"""
token_defaults = {
'DESC':(None,),
}
def __init__(self,schema_element_str=None):
if sys.version_info >= (3, 0) and isinstance(schema_element_str, bytes):
schema_element_str = schema_element_str.decode('utf-8')
if schema_element_str:
l = split_tokens(schema_element_str)
self.set_id(l[1])
d = extract_tokens(l,self.token_defaults)
self._set_attrs(l,d)
def _set_attrs(self,l,d):
self.desc = d['DESC'][0]
return
def set_id(self,element_id):
self.oid = element_id
def get_id(self):
return self.oid
def key_attr(self,key,value,quoted=0):
assert value is None or type(value)==str,TypeError("value has to be of str, was %r" % value)
if value:
if quoted:
return " %s '%s'" % (key,value.replace("'","\\'"))
else:
return " %s %s" % (key,value)
else:
return ""
def key_list(self,key,values,sep=' ',quoted=0):
assert type(values)==tuple,TypeError("values has to be a tuple, was %r" % values)
if not values:
return ''
if quoted:
quoted_values = [ "'%s'" % value.replace("'","\\'") for value in values ]
else:
quoted_values = values
if len(values)==1:
return ' %s %s' % (key,quoted_values[0])
else:
return ' %s ( %s )' % (key,sep.join(quoted_values))
def __str__(self):
result = [str(self.oid)]
result.append(self.key_attr('DESC',self.desc,quoted=1))
return '( %s )' % ''.join(result)
class ObjectClass(SchemaElement):
"""
Arguments:
schema_element_str
String containing an ObjectClassDescription
Class attributes:
oid
OID assigned to the object class
names
This list of strings contains all NAMEs of the object class
desc
This string contains description text (DESC) of the object class
obsolete
Integer flag (0 or 1) indicating whether the object class is marked
as OBSOLETE in the schema
must
This list of strings contains NAMEs or OIDs of all attributes
an entry of the object class must have
may
This list of strings contains NAMEs or OIDs of additional attributes
an entry of the object class may have
kind
Kind of an object class:
0 = STRUCTURAL,
1 = ABSTRACT,
2 = AUXILIARY
sup
This list of strings contains NAMEs or OIDs of object classes
this object class is derived from
"""
schema_attribute = u'objectClasses'
token_defaults = {
'NAME':(()),
'DESC':(None,),
'OBSOLETE':None,
'SUP':(()),
'STRUCTURAL':None,
'AUXILIARY':None,
'ABSTRACT':None,
'MUST':(()),
'MAY':()
}
def _set_attrs(self,l,d):
self.obsolete = d['OBSOLETE']!=None
self.names = d['NAME']
self.desc = d['DESC'][0]
self.must = d['MUST']
self.may = d['MAY']
# Default is STRUCTURAL, see RFC2552 or draft-ietf-ldapbis-syntaxes
self.kind = 0
if d['ABSTRACT']!=None:
self.kind = 1
elif d['AUXILIARY']!=None:
self.kind = 2
if self.kind==0 and not d['SUP'] and self.oid!='2.5.6.0':
# STRUCTURAL object classes are sub-classes of 'top' by default
self.sup = ('top',)
else:
self.sup = d['SUP']
return
def __str__(self):
result = [str(self.oid)]
result.append(self.key_list('NAME',self.names,quoted=1))
result.append(self.key_attr('DESC',self.desc,quoted=1))
result.append(self.key_list('SUP',self.sup,sep=' $ '))
result.append({0:'',1:' OBSOLETE'}[self.obsolete])
result.append({0:' STRUCTURAL',1:' ABSTRACT',2:' AUXILIARY'}[self.kind])
result.append(self.key_list('MUST',self.must,sep=' $ '))
result.append(self.key_list('MAY',self.may,sep=' $ '))
return '( %s )' % ''.join(result)
AttributeUsage = ldap.cidict.cidict({
'userApplication':0, # work-around for non-compliant schema
'userApplications':0,
'directoryOperation':1,
'distributedOperation':2,
'dSAOperation':3,
})
class AttributeType(SchemaElement):
"""
Arguments:
schema_element_str
String containing an AttributeTypeDescription
Class attributes:
oid
OID assigned to the attribute type
names
This list of strings contains all NAMEs of the attribute type
desc
This string contains description text (DESC) of the attribute type
obsolete
Integer flag (0 or 1) indicating whether the attribute type is marked
as OBSOLETE in the schema
single_value
Integer flag (0 or 1) indicating whether the attribute must
have only one value
syntax
String contains OID of the LDAP syntax assigned to the attribute type
no_user_mod
Integer flag (0 or 1) indicating whether the attribute is modifiable
by a client application
equality
String contains NAME or OID of the matching rule used for
checking whether attribute values are equal
substr
String contains NAME or OID of the matching rule used for
checking whether an attribute value contains another value
ordering
String contains NAME or OID of the matching rule used for
checking whether attribute values are lesser-equal than
usage
USAGE of an attribute type:
0 = userApplications
1 = directoryOperation,
2 = distributedOperation,
3 = dSAOperation
sup
This list of strings contains NAMEs or OIDs of attribute types
this attribute type is derived from
"""
schema_attribute = u'attributeTypes'
token_defaults = {
'NAME':(()),
'DESC':(None,),
'OBSOLETE':None,
'SUP':(()),
'EQUALITY':(None,),
'ORDERING':(None,),
'SUBSTR':(None,),
'SYNTAX':(None,),
'SINGLE-VALUE':None,
'COLLECTIVE':None,
'NO-USER-MODIFICATION':None,
'USAGE':('userApplications',),
'X-ORIGIN':(None,),
'X-ORDERED':(None,),
}
def _set_attrs(self,l,d):
self.names = d['NAME']
self.desc = d['DESC'][0]
self.obsolete = d['OBSOLETE']!=None
self.sup = d['SUP']
self.equality = d['EQUALITY'][0]
self.ordering = d['ORDERING'][0]
self.substr = d['SUBSTR'][0]
self.x_origin = d['X-ORIGIN'][0]
self.x_ordered = d['X-ORDERED'][0]
try:
syntax = d['SYNTAX'][0]
except IndexError:
self.syntax = None
self.syntax_len = None
else:
if syntax is None:
self.syntax = None
self.syntax_len = None
else:
try:
self.syntax,syntax_len = d['SYNTAX'][0].split("{")
except ValueError:
self.syntax = d['SYNTAX'][0]
self.syntax_len = None
for i in l:
if i.startswith("{") and i.endswith("}"):
self.syntax_len = int(i[1:-1])
else:
self.syntax_len = int(syntax_len[:-1])
self.single_value = d['SINGLE-VALUE']!=None
self.collective = d['COLLECTIVE']!=None
self.no_user_mod = d['NO-USER-MODIFICATION']!=None
self.usage = AttributeUsage.get(d['USAGE'][0],0)
return
def __str__(self):
result = [str(self.oid)]
result.append(self.key_list('NAME',self.names,quoted=1))
result.append(self.key_attr('DESC',self.desc,quoted=1))
result.append(self.key_list('SUP',self.sup,sep=' $ '))
result.append({0:'',1:' OBSOLETE'}[self.obsolete])
result.append(self.key_attr('EQUALITY',self.equality))
result.append(self.key_attr('ORDERING',self.ordering))
result.append(self.key_attr('SUBSTR',self.substr))
result.append(self.key_attr('SYNTAX',self.syntax))
if self.syntax_len!=None:
result.append(('{%d}' % (self.syntax_len))*(self.syntax_len>0))
result.append({0:'',1:' SINGLE-VALUE'}[self.single_value])
result.append({0:'',1:' COLLECTIVE'}[self.collective])
result.append({0:'',1:' NO-USER-MODIFICATION'}[self.no_user_mod])
result.append(
{
0:"",
1:" USAGE directoryOperation",
2:" USAGE distributedOperation",
3:" USAGE dSAOperation",
}[self.usage]
)
result.append(self.key_attr('X-ORIGIN',self.x_origin,quoted=1))
result.append(self.key_attr('X-ORDERED',self.x_ordered,quoted=1))
return '( %s )' % ''.join(result)
class LDAPSyntax(SchemaElement):
"""
SyntaxDescription
oid
OID assigned to the LDAP syntax
desc
This string contains description text (DESC) of the LDAP syntax
not_human_readable
Integer flag (0 or 1) indicating whether the attribute type is marked
as not human-readable (X-NOT-HUMAN-READABLE)
"""
schema_attribute = u'ldapSyntaxes'
token_defaults = {
'DESC':(None,),
'X-NOT-HUMAN-READABLE':(None,),
'X-BINARY-TRANSFER-REQUIRED':(None,),
'X-SUBST':(None,),
}
def _set_attrs(self,l,d):
self.desc = d['DESC'][0]
self.x_subst = d['X-SUBST'][0]
self.not_human_readable = \
self.oid in NOT_HUMAN_READABLE_LDAP_SYNTAXES or \
d['X-NOT-HUMAN-READABLE'][0]=='TRUE'
self.x_binary_transfer_required = d['X-BINARY-TRANSFER-REQUIRED'][0]=='TRUE'
return
def __str__(self):
result = [str(self.oid)]
result.append(self.key_attr('DESC',self.desc,quoted=1))
result.append(self.key_attr('X-SUBST',self.x_subst,quoted=1))
result.append(
{0:'',1:" X-NOT-HUMAN-READABLE 'TRUE'"}[self.not_human_readable]
)
return '( %s )' % ''.join(result)
class MatchingRule(SchemaElement):
"""
Arguments:
schema_element_str
String containing an MatchingRuleDescription
Class attributes:
oid
OID assigned to the matching rule
names
This list of strings contains all NAMEs of the matching rule
desc
This string contains description text (DESC) of the matching rule
obsolete
Integer flag (0 or 1) indicating whether the matching rule is marked
as OBSOLETE in the schema
syntax
String contains OID of the LDAP syntax this matching rule is usable with
"""
schema_attribute = u'matchingRules'
token_defaults = {
'NAME':(()),
'DESC':(None,),
'OBSOLETE':None,
'SYNTAX':(None,),
}
def _set_attrs(self,l,d):
self.names = d['NAME']
self.desc = d['DESC'][0]
self.obsolete = d['OBSOLETE']!=None
self.syntax = d['SYNTAX'][0]
return
def __str__(self):
result = [str(self.oid)]
result.append(self.key_list('NAME',self.names,quoted=1))
result.append(self.key_attr('DESC',self.desc,quoted=1))
result.append({0:'',1:' OBSOLETE'}[self.obsolete])
result.append(self.key_attr('SYNTAX',self.syntax))
return '( %s )' % ''.join(result)
class MatchingRuleUse(SchemaElement):
"""
Arguments:
schema_element_str
String containing an MatchingRuleUseDescription
Class attributes:
oid
OID of the accompanying matching rule
names
This list of strings contains all NAMEs of the matching rule
desc
This string contains description text (DESC) of the matching rule
obsolete
Integer flag (0 or 1) indicating whether the matching rule is marked
as OBSOLETE in the schema
applies
This list of strings contains NAMEs or OIDs of attribute types
for which this matching rule is used
"""
schema_attribute = u'matchingRuleUse'
token_defaults = {
'NAME':(()),
'DESC':(None,),
'OBSOLETE':None,
'APPLIES':(()),
}
def _set_attrs(self,l,d):
self.names = d['NAME']
self.desc = d['DESC'][0]
self.obsolete = d['OBSOLETE']!=None
self.applies = d['APPLIES']
return
def __str__(self):
result = [str(self.oid)]
result.append(self.key_list('NAME',self.names,quoted=1))
result.append(self.key_attr('DESC',self.desc,quoted=1))
result.append({0:'',1:' OBSOLETE'}[self.obsolete])
result.append(self.key_list('APPLIES',self.applies,sep=' $ '))
return '( %s )' % ''.join(result)
class DITContentRule(SchemaElement):
"""
Arguments:
schema_element_str
String containing an DITContentRuleDescription
Class attributes:
oid
OID of the accompanying structural object class
names
This list of strings contains all NAMEs of the DIT content rule
desc
This string contains description text (DESC) of the DIT content rule
obsolete
Integer flag (0 or 1) indicating whether the DIT content rule is marked
as OBSOLETE in the schema
aux
This list of strings contains NAMEs or OIDs of all auxiliary
object classes usable in an entry of the object class
must
This list of strings contains NAMEs or OIDs of all attributes
an entry of the object class must have which may extend the
list of required attributes of the object classes of an entry
may
This list of strings contains NAMEs or OIDs of additional attributes
an entry of the object class may have which may extend the
list of optional attributes of the object classes of an entry
nots
This list of strings contains NAMEs or OIDs of attributes which
may not be present in an entry of the object class
"""
schema_attribute = u'dITContentRules'
token_defaults = {
'NAME':(()),
'DESC':(None,),
'OBSOLETE':None,
'AUX':(()),
'MUST':(()),
'MAY':(()),
'NOT':(()),
}
def _set_attrs(self,l,d):
self.names = d['NAME']
self.desc = d['DESC'][0]
self.obsolete = d['OBSOLETE']!=None
self.aux = d['AUX']
self.must = d['MUST']
self.may = d['MAY']
self.nots = d['NOT']
return
def __str__(self):
result = [str(self.oid)]
result.append(self.key_list('NAME',self.names,quoted=1))
result.append(self.key_attr('DESC',self.desc,quoted=1))
result.append({0:'',1:' OBSOLETE'}[self.obsolete])
result.append(self.key_list('AUX',self.aux,sep=' $ '))
result.append(self.key_list('MUST',self.must,sep=' $ '))
result.append(self.key_list('MAY',self.may,sep=' $ '))
result.append(self.key_list('NOT',self.nots,sep=' $ '))
return '( %s )' % ''.join(result)
class DITStructureRule(SchemaElement):
"""
Arguments:
schema_element_str
String containing an DITStructureRuleDescription
Class attributes:
ruleid
rule ID of the DIT structure rule (only locally unique)
names
This list of strings contains all NAMEs of the DIT structure rule
desc
This string contains description text (DESC) of the DIT structure rule
obsolete
Integer flag (0 or 1) indicating whether the DIT content rule is marked
as OBSOLETE in the schema
form
List of strings with NAMEs or OIDs of associated name forms
sup
List of strings with NAMEs or OIDs of allowed structural object classes
of superior entries in the DIT
"""
schema_attribute = u'dITStructureRules'
token_defaults = {
'NAME':(()),
'DESC':(None,),
'OBSOLETE':None,
'FORM':(None,),
'SUP':(()),
}
def set_id(self,element_id):
self.ruleid = element_id
def get_id(self):
return self.ruleid
def _set_attrs(self,l,d):
self.names = d['NAME']
self.desc = d['DESC'][0]
self.obsolete = d['OBSOLETE']!=None
self.form = d['FORM'][0]
self.sup = d['SUP']
return
def __str__(self):
result = [str(self.ruleid)]
result.append(self.key_list('NAME',self.names,quoted=1))
result.append(self.key_attr('DESC',self.desc,quoted=1))
result.append({0:'',1:' OBSOLETE'}[self.obsolete])
result.append(self.key_attr('FORM',self.form,quoted=0))
result.append(self.key_list('SUP',self.sup,sep=' $ '))
return '( %s )' % ''.join(result)
class NameForm(SchemaElement):
"""
Arguments:
schema_element_str
String containing an NameFormDescription
Class attributes:
oid
OID of the name form
names
This list of strings contains all NAMEs of the name form
desc
This string contains description text (DESC) of the name form
obsolete
Integer flag (0 or 1) indicating whether the name form is marked
as OBSOLETE in the schema
form
List of strings with NAMEs or OIDs of associated name forms
oc
String with NAME or OID of structural object classes this name form
is usable with
must
This list of strings contains NAMEs or OIDs of all attributes
an RDN must contain
may
This list of strings contains NAMEs or OIDs of additional attributes
an RDN may contain
"""
schema_attribute = u'nameForms'
token_defaults = {
'NAME':(()),
'DESC':(None,),
'OBSOLETE':None,
'OC':(None,),
'MUST':(()),
'MAY':(()),
}
def _set_attrs(self,l,d):
self.names = d['NAME']
self.desc = d['DESC'][0]
self.obsolete = d['OBSOLETE']!=None
self.oc = d['OC'][0]
self.must = d['MUST']
self.may = d['MAY']
return
def __str__(self):
result = [str(self.oid)]
result.append(self.key_list('NAME',self.names,quoted=1))
result.append(self.key_attr('DESC',self.desc,quoted=1))
result.append({0:'',1:' OBSOLETE'}[self.obsolete])
result.append(self.key_attr('OC',self.oc))
result.append(self.key_list('MUST',self.must,sep=' $ '))
result.append(self.key_list('MAY',self.may,sep=' $ '))
return '( %s )' % ''.join(result)
class Entry(IterableUserDict):
"""
Schema-aware implementation of an LDAP entry class.
Mainly it holds the attributes in a string-keyed dictionary with
the OID as key.
"""
def __init__(self,schema,dn,entry):
self._keytuple2attrtype = {}
self._attrtype2keytuple = {}
self._s = schema
self.dn = dn
IterableUserDict.IterableUserDict.__init__(self,{})
self.update(entry)
def _at2key(self,nameoroid):
"""
Return tuple of OID and all sub-types of attribute type specified
in nameoroid.
"""
try:
# Mapping already in cache
return self._attrtype2keytuple[nameoroid]
except KeyError:
# Mapping has to be constructed
oid = self._s.getoid(ldap.schema.AttributeType,nameoroid)
l = nameoroid.lower().split(';')
l[0] = oid
t = tuple(l)
self._attrtype2keytuple[nameoroid] = t
return t
def update(self,dict):
for key, value in dict.values():
self[key] = value
def __contains__(self,nameoroid):
return self._at2key(nameoroid) in self.data
def __getitem__(self,nameoroid):
return self.data[self._at2key(nameoroid)]
def __setitem__(self,nameoroid,attr_values):
k = self._at2key(nameoroid)
self._keytuple2attrtype[k] = nameoroid
self.data[k] = attr_values
def __delitem__(self,nameoroid):
k = self._at2key(nameoroid)
del self.data[k]
del self._attrtype2keytuple[nameoroid]
del self._keytuple2attrtype[k]
def has_key(self,nameoroid):
k = self._at2key(nameoroid)
return k in self.data
def keys(self):
return self._keytuple2attrtype.values()
def items(self):
return [
(k,self[k])
for k in self.keys()
]
def attribute_types(
self,attr_type_filter=None,raise_keyerror=1
):
"""
Convenience wrapper around SubSchema.attribute_types() which
passes object classes of this particular entry as argument to
SubSchema.attribute_types()
"""
return self._s.attribute_types(
self.get('objectClass',[]),attr_type_filter,raise_keyerror
)
@@ -0,0 +1,498 @@
"""
ldap.schema.subentry - subschema subentry handling
See https://www.python-ldap.org/ for details.
"""
import copy
import ldap.cidict,ldap.schema
from ldap.compat import urlopen
from ldap.schema.models import *
import ldapurl
import ldif
SCHEMA_CLASS_MAPPING = ldap.cidict.cidict()
SCHEMA_ATTR_MAPPING = {}
for o in list(vars().values()):
if hasattr(o,'schema_attribute'):
SCHEMA_CLASS_MAPPING[o.schema_attribute] = o
SCHEMA_ATTR_MAPPING[o] = o.schema_attribute
SCHEMA_ATTRS = SCHEMA_CLASS_MAPPING.keys()
class SubschemaError(ValueError):
pass
class OIDNotUnique(SubschemaError):
def __init__(self,desc):
self.desc = desc
def __str__(self):
return 'OID not unique for %s' % (self.desc)
class NameNotUnique(SubschemaError):
def __init__(self,desc):
self.desc = desc
def __str__(self):
return 'NAME not unique for %s' % (self.desc)
class SubSchema:
"""
Arguments:
sub_schema_sub_entry
Dictionary usually returned by LDAP search or the LDIF parser
containing the sub schema sub entry
check_uniqueness
Defines whether uniqueness of OIDs and NAME is checked.
0
no check
1
check but add schema description with work-around
2
check and raise exception if non-unique OID or NAME is found
Class attributes:
sed
Dictionary holding the subschema information as pre-parsed
SchemaElement objects (do not access directly!)
name2oid
Dictionary holding the mapping from NAMEs to OIDs
(do not access directly!)
non_unique_oids
List of OIDs used at least twice in the subschema
non_unique_names
List of NAMEs used at least twice in the subschema for the same schema element
"""
def __init__(self,sub_schema_sub_entry,check_uniqueness=1):
# Initialize all dictionaries
self.name2oid = {}
self.sed = {}
self.non_unique_oids = {}
self.non_unique_names = {}
for c in SCHEMA_CLASS_MAPPING.values():
self.name2oid[c] = ldap.cidict.cidict()
self.sed[c] = {}
self.non_unique_names[c] = ldap.cidict.cidict()
# Transform entry dict to case-insensitive dict
e = ldap.cidict.cidict(sub_schema_sub_entry)
# Build the schema registry in dictionaries
for attr_type in SCHEMA_ATTRS:
for attr_value in filter(None,e.get(attr_type,[])):
se_class = SCHEMA_CLASS_MAPPING[attr_type]
se_instance = se_class(attr_value)
se_id = se_instance.get_id()
if check_uniqueness and se_id in self.sed[se_class]:
self.non_unique_oids[se_id] = None
if check_uniqueness==1:
# Add to subschema by adding suffix to ID
suffix_counter = 1
new_se_id = se_id
while new_se_id in self.sed[se_class]:
new_se_id = ';'.join((se_id,str(suffix_counter)))
suffix_counter += 1
else:
se_id = new_se_id
elif check_uniqueness>=2:
raise OIDNotUnique(attr_value)
# Store the schema element instance in the central registry
self.sed[se_class][se_id] = se_instance
if hasattr(se_instance,'names'):
for name in ldap.cidict.cidict({}.fromkeys(se_instance.names)).keys():
if check_uniqueness and name in self.name2oid[se_class]:
self.non_unique_names[se_class][se_id] = None
raise NameNotUnique(attr_value)
else:
self.name2oid[se_class][name] = se_id
# Turn dict into list maybe more handy for applications
self.non_unique_oids = self.non_unique_oids.keys()
return # subSchema.__init__()
def ldap_entry(self):
"""
Returns a dictionary containing the sub schema sub entry
"""
# Initialize the dictionary with empty lists
entry = {}
# Collect the schema elements and store them in
# entry's attributes
for se_class, elements in self.sed.items():
for se in elements.values():
se_str = str(se)
try:
entry[SCHEMA_ATTR_MAPPING[se_class]].append(se_str)
except KeyError:
entry[SCHEMA_ATTR_MAPPING[se_class]] = [ se_str ]
return entry
def listall(self,schema_element_class,schema_element_filters=None):
"""
Returns a list of OIDs of all available schema
elements of a given schema element class.
"""
avail_se = self.sed[schema_element_class]
if schema_element_filters:
result = []
for se_key, se in avail_se.items():
for fk,fv in schema_element_filters:
try:
if getattr(se,fk) in fv:
result.append(se_key)
except AttributeError:
pass
else:
result = avail_se.keys()
return result
def tree(self,schema_element_class,schema_element_filters=None):
"""
Returns a ldap.cidict.cidict dictionary representing the
tree structure of the schema elements.
"""
assert schema_element_class in [ObjectClass,AttributeType]
avail_se = self.listall(schema_element_class,schema_element_filters)
top_node = '_'
tree = ldap.cidict.cidict({top_node:[]})
# 1. Pass: Register all nodes
for se in avail_se:
tree[se] = []
# 2. Pass: Register all sup references
for se_oid in avail_se:
se_obj = self.get_obj(schema_element_class,se_oid,None)
if se_obj.__class__!=schema_element_class:
# Ignore schema elements not matching schema_element_class.
# This helps with falsely assigned OIDs.
continue
assert se_obj.__class__==schema_element_class, \
"Schema element referenced by %s must be of class %s but was %s" % (
se_oid,schema_element_class.__name__,se_obj.__class__
)
for s in se_obj.sup or ('_',):
sup_oid = self.getoid(schema_element_class,s)
try:
tree[sup_oid].append(se_oid)
except:
pass
return tree
def getoid(self,se_class,nameoroid,raise_keyerror=0):
"""
Get an OID by name or OID
"""
nameoroid_stripped = nameoroid.split(';')[0].strip()
if nameoroid_stripped in self.sed[se_class]:
# name_or_oid is already a registered OID
return nameoroid_stripped
else:
try:
result_oid = self.name2oid[se_class][nameoroid_stripped]
except KeyError:
if raise_keyerror:
raise KeyError('No registered %s-OID for nameoroid %s' % (se_class.__name__,repr(nameoroid_stripped)))
else:
result_oid = nameoroid_stripped
return result_oid
def get_inheritedattr(self,se_class,nameoroid,name):
"""
Get a possibly inherited attribute specified by name
of a schema element specified by nameoroid.
Returns None if class attribute is not set at all.
Raises KeyError if no schema element is found by nameoroid.
"""
se = self.sed[se_class][self.getoid(se_class,nameoroid)]
try:
result = getattr(se,name)
except AttributeError:
result = None
if result is None and se.sup:
result = self.get_inheritedattr(se_class,se.sup[0],name)
return result
def get_obj(self,se_class,nameoroid,default=None,raise_keyerror=0):
"""
Get a schema element by name or OID
"""
se_oid = self.getoid(se_class,nameoroid)
try:
se_obj = self.sed[se_class][se_oid]
except KeyError:
if raise_keyerror:
raise KeyError('No ldap.schema.%s instance with nameoroid %s and se_oid %s' % (
se_class.__name__,repr(nameoroid),repr(se_oid))
)
else:
se_obj = default
return se_obj
def get_inheritedobj(self,se_class,nameoroid,inherited=None):
"""
Get a schema element by name or OID with all class attributes
set including inherited class attributes
"""
inherited = inherited or []
se = copy.copy(self.sed[se_class].get(self.getoid(se_class,nameoroid)))
if se and hasattr(se,'sup'):
for class_attr_name in inherited:
setattr(se,class_attr_name,self.get_inheritedattr(se_class,nameoroid,class_attr_name))
return se
def get_syntax(self,nameoroid):
"""
Get the syntax of an attribute type specified by name or OID
"""
at_oid = self.getoid(AttributeType,nameoroid)
try:
at_obj = self.get_inheritedobj(AttributeType,at_oid)
except KeyError:
return None
else:
return at_obj.syntax
def get_structural_oc(self,oc_list):
"""
Returns OID of structural object class in oc_list
if any is present. Returns None else.
"""
# Get tree of all STRUCTURAL object classes
oc_tree = self.tree(ObjectClass,[('kind',[0])])
# Filter all STRUCTURAL object classes
struct_ocs = {}
for oc_nameoroid in oc_list:
oc_se = self.get_obj(ObjectClass,oc_nameoroid,None)
if oc_se and oc_se.kind==0:
struct_ocs[oc_se.oid] = None
result = None
# Build a copy of the oid list, to be cleaned as we go.
struct_oc_list = list(struct_ocs)
while struct_oc_list:
oid = struct_oc_list.pop()
for child_oid in oc_tree[oid]:
if self.getoid(ObjectClass,child_oid) in struct_ocs:
break
else:
result = oid
return result
def get_applicable_aux_classes(self,nameoroid):
"""
Return a list of the applicable AUXILIARY object classes
for a STRUCTURAL object class specified by 'nameoroid'
if the object class is governed by a DIT content rule.
If there's no DIT content rule all available AUXILIARY
object classes are returned.
"""
content_rule = self.get_obj(DITContentRule,nameoroid)
if content_rule:
# Return AUXILIARY object classes from DITContentRule instance
return content_rule.aux
else:
# list all AUXILIARY object classes
return self.listall(ObjectClass,[('kind',[2])])
def attribute_types(
self,object_class_list,attr_type_filter=None,raise_keyerror=1,ignore_dit_content_rule=0
):
"""
Returns a 2-tuple of all must and may attributes including
all inherited attributes of superior object classes
by walking up classes along the SUP attribute.
The attributes are stored in a ldap.cidict.cidict dictionary.
object_class_list
list of strings specifying object class names or OIDs
attr_type_filter
list of 2-tuples containing lists of class attributes
which has to be matched
raise_keyerror
All KeyError exceptions for non-existent schema elements
are ignored
ignore_dit_content_rule
A DIT content rule governing the structural object class
is ignored
"""
AttributeType = ldap.schema.AttributeType
ObjectClass = ldap.schema.ObjectClass
# Map object_class_list to object_class_oids (list of OIDs)
object_class_oids = [
self.getoid(ObjectClass,o)
for o in object_class_list
]
# Initialize
oid_cache = {}
r_must,r_may = ldap.cidict.cidict(),ldap.cidict.cidict()
if '1.3.6.1.4.1.1466.101.120.111' in object_class_oids:
# Object class 'extensibleObject' MAY carry every attribute type
for at_obj in self.sed[AttributeType].values():
r_may[at_obj.oid] = at_obj
# Loop over OIDs of all given object classes
while object_class_oids:
object_class_oid = object_class_oids.pop(0)
# Check whether the objectClass with this OID
# has already been processed
if object_class_oid in oid_cache:
continue
# Cache this OID as already being processed
oid_cache[object_class_oid] = None
try:
object_class = self.sed[ObjectClass][object_class_oid]
except KeyError:
if raise_keyerror:
raise
# Ignore this object class
continue
assert isinstance(object_class,ObjectClass)
assert hasattr(object_class,'must'),ValueError(object_class_oid)
assert hasattr(object_class,'may'),ValueError(object_class_oid)
for a in object_class.must:
se_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
r_must[se_oid] = self.get_obj(AttributeType,se_oid,raise_keyerror=raise_keyerror)
for a in object_class.may:
se_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
r_may[se_oid] = self.get_obj(AttributeType,se_oid,raise_keyerror=raise_keyerror)
object_class_oids.extend([
self.getoid(ObjectClass,o)
for o in object_class.sup
])
# Process DIT content rules
if not ignore_dit_content_rule:
structural_oc = self.get_structural_oc(object_class_list)
if structural_oc:
# Process applicable DIT content rule
try:
dit_content_rule = self.get_obj(DITContentRule,structural_oc,raise_keyerror=1)
except KeyError:
# Not DIT content rule found for structural objectclass
pass
else:
for a in dit_content_rule.must:
se_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
r_must[se_oid] = self.get_obj(AttributeType,se_oid,raise_keyerror=raise_keyerror)
for a in dit_content_rule.may:
se_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
r_may[se_oid] = self.get_obj(AttributeType,se_oid,raise_keyerror=raise_keyerror)
for a in dit_content_rule.nots:
a_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
try:
del r_may[a_oid]
except KeyError:
pass
# Remove all mandantory attribute types from
# optional attribute type list
for a in list(r_may.keys()):
if a in r_must:
del r_may[a]
# Apply attr_type_filter to results
if attr_type_filter:
for l in [r_must,r_may]:
for a in list(l.keys()):
for afk,afv in attr_type_filter:
try:
schema_attr_type = self.sed[AttributeType][a]
except KeyError:
if raise_keyerror:
raise KeyError('No attribute type found in sub schema by name %s' % (a))
# If there's no schema element for this attribute type
# but still KeyError is to be ignored we filter it away
del l[a]
break
else:
if not getattr(schema_attr_type,afk) in afv:
del l[a]
break
return r_must,r_may # attribute_types()
def urlfetch(uri,trace_level=0):
"""
Fetches a parsed schema entry by uri.
If uri is a LDAP URL the LDAP server is queried directly.
Otherwise uri is assumed to point to a LDIF file which
is loaded with urllib.
"""
uri = uri.strip()
if uri.startswith(('ldap:', 'ldaps:', 'ldapi:')):
ldap_url = ldapurl.LDAPUrl(uri)
l=ldap.initialize(ldap_url.initializeUrl(),trace_level)
l.protocol_version = ldap.VERSION3
l.simple_bind_s(ldap_url.who or u'', ldap_url.cred or u'')
subschemasubentry_dn = l.search_subschemasubentry_s(ldap_url.dn)
if subschemasubentry_dn is None:
s_temp = None
else:
if ldap_url.attrs is None:
schema_attrs = SCHEMA_ATTRS
else:
schema_attrs = ldap_url.attrs
s_temp = l.read_subschemasubentry_s(
subschemasubentry_dn,attrs=schema_attrs
)
l.unbind_s()
del l
else:
ldif_file = urlopen(uri)
ldif_parser = ldif.LDIFRecordList(ldif_file,max_entries=1)
ldif_parser.parse()
subschemasubentry_dn,s_temp = ldif_parser.all_records[0]
# Work-around for mixed-cased attribute names
subschemasubentry_entry = ldap.cidict.cidict()
s_temp = s_temp or {}
for at,av in s_temp.items():
if at in SCHEMA_CLASS_MAPPING:
try:
subschemasubentry_entry[at].extend(av)
except KeyError:
subschemasubentry_entry[at] = av
# Finally parse the schema
if subschemasubentry_dn!=None:
parsed_sub_schema = ldap.schema.SubSchema(subschemasubentry_entry)
else:
parsed_sub_schema = None
return subschemasubentry_dn, parsed_sub_schema
@@ -0,0 +1,80 @@
"""
ldap.schema.tokenizer - Low-level parsing functions for schema element strings
See https://www.python-ldap.org/ for details.
"""
import re
TOKENS_FINDALL = re.compile(
r"(\()" # opening parenthesis
r"|" # or
r"(\))" # closing parenthesis
r"|" # or
r"([^'$()\s]+)" # string of length >= 1 without '$() or whitespace
r"|" # or
r"('.*?'(?!\w))" # any string or empty string surrounded by single quotes
# except if right quote is succeeded by alphanumeric char
r"|" # or
r"([^\s]+?)", # residue, all non-whitespace strings
).findall
def split_tokens(s):
"""
Returns list of syntax elements with quotes and spaces stripped.
"""
parts = []
parens = 0
for opar, cpar, unquoted, quoted, residue in TOKENS_FINDALL(s):
if unquoted:
parts.append(unquoted)
elif quoted:
parts.append(quoted[1:-1])
elif opar:
parens += 1
parts.append(opar)
elif cpar:
parens -= 1
parts.append(cpar)
elif residue == '$':
if not parens:
raise ValueError("'$' outside parenthesis in %r" % (s))
else:
raise ValueError(residue, s)
if parens:
raise ValueError("Unbalanced parenthesis in %r" % (s))
return parts
def extract_tokens(l,known_tokens):
"""
Returns dictionary of known tokens with all values
"""
assert l[0].strip()=="(" and l[-1].strip()==")",ValueError(l)
result = {}
result.update(known_tokens)
i = 0
l_len = len(l)
while i<l_len:
if l[i] in result:
token = l[i]
i += 1 # Consume token
if i<l_len:
if l[i] in result:
# non-valued
result[token] = (())
elif l[i]=="(":
# multi-valued
i += 1 # Consume left parentheses
start = i
while i<l_len and l[i]!=")":
i += 1
result[token] = tuple(filter(lambda v:v!='$',l[start:i]))
i += 1 # Consume right parentheses
else:
# single-valued
result[token] = l[i],
i += 1 # Consume single value
else:
i += 1 # Consume unrecognized item
return result
@@ -0,0 +1,536 @@
# -*- coding: utf-8 -*-
"""
ldap.syncrepl - for implementing syncrepl consumer (see RFC 4533)
See https://www.python-ldap.org/ for project details.
"""
from uuid import UUID
# Imports from pyasn1
from pyasn1.type import tag, namedtype, namedval, univ, constraint
from pyasn1.codec.ber import encoder, decoder
from ldap.pkginfo import __version__, __author__, __license__
from ldap.controls import RequestControl, ResponseControl, KNOWN_RESPONSE_CONTROLS
__all__ = [
'SyncreplConsumer',
]
class SyncUUID(univ.OctetString):
"""
syncUUID ::= OCTET STRING (SIZE(16))
"""
subtypeSpec = constraint.ValueSizeConstraint(16, 16)
class SyncCookie(univ.OctetString):
"""
syncCookie ::= OCTET STRING
"""
class SyncRequestMode(univ.Enumerated):
"""
mode ENUMERATED {
-- 0 unused
refreshOnly (1),
-- 2 reserved
refreshAndPersist (3)
},
"""
namedValues = namedval.NamedValues(
('refreshOnly', 1),
('refreshAndPersist', 3)
)
subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(1, 3)
class SyncRequestValue(univ.Sequence):
"""
syncRequestValue ::= SEQUENCE {
mode ENUMERATED {
-- 0 unused
refreshOnly (1),
-- 2 reserved
refreshAndPersist (3)
},
cookie syncCookie OPTIONAL,
reloadHint BOOLEAN DEFAULT FALSE
}
"""
componentType = namedtype.NamedTypes(
namedtype.NamedType('mode', SyncRequestMode()),
namedtype.OptionalNamedType('cookie', SyncCookie()),
namedtype.DefaultedNamedType('reloadHint', univ.Boolean(False))
)
class SyncRequestControl(RequestControl):
"""
The Sync Request Control is an LDAP Control [RFC4511] where the
controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.1 and the
controlValue, an OCTET STRING, contains a BER-encoded
syncRequestValue. The criticality field is either TRUE or FALSE.
[..]
The Sync Request Control is only applicable to the SearchRequest
Message.
"""
controlType = '1.3.6.1.4.1.4203.1.9.1.1'
def __init__(self, criticality=1, cookie=None, mode='refreshOnly', reloadHint=False):
self.criticality = criticality
self.cookie = cookie
self.mode = mode
self.reloadHint = reloadHint
def encodeControlValue(self):
rcv = SyncRequestValue()
rcv.setComponentByName('mode', SyncRequestMode(self.mode))
if self.cookie is not None:
rcv.setComponentByName('cookie', SyncCookie(self.cookie))
if self.reloadHint:
rcv.setComponentByName('reloadHint', univ.Boolean(self.reloadHint))
return encoder.encode(rcv)
class SyncStateOp(univ.Enumerated):
"""
state ENUMERATED {
present (0),
add (1),
modify (2),
delete (3)
},
"""
namedValues = namedval.NamedValues(
('present', 0),
('add', 1),
('modify', 2),
('delete', 3)
)
subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(0, 1, 2, 3)
class SyncStateValue(univ.Sequence):
"""
syncStateValue ::= SEQUENCE {
state ENUMERATED {
present (0),
add (1),
modify (2),
delete (3)
},
entryUUID syncUUID,
cookie syncCookie OPTIONAL
}
"""
componentType = namedtype.NamedTypes(
namedtype.NamedType('state', SyncStateOp()),
namedtype.NamedType('entryUUID', SyncUUID()),
namedtype.OptionalNamedType('cookie', SyncCookie())
)
class SyncStateControl(ResponseControl):
"""
The Sync State Control is an LDAP Control [RFC4511] where the
controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.2 and the
controlValue, an OCTET STRING, contains a BER-encoded SyncStateValue.
The criticality is FALSE.
[..]
The Sync State Control is only applicable to SearchResultEntry and
SearchResultReference Messages.
"""
controlType = '1.3.6.1.4.1.4203.1.9.1.2'
opnames = ('present', 'add', 'modify', 'delete')
def decodeControlValue(self, encodedControlValue):
d = decoder.decode(encodedControlValue, asn1Spec=SyncStateValue())
state = d[0].getComponentByName('state')
uuid = UUID(bytes=bytes(d[0].getComponentByName('entryUUID')))
cookie = d[0].getComponentByName('cookie')
if cookie is not None and cookie.hasValue():
self.cookie = str(cookie)
else:
self.cookie = None
self.state = self.__class__.opnames[int(state)]
self.entryUUID = str(uuid)
KNOWN_RESPONSE_CONTROLS[SyncStateControl.controlType] = SyncStateControl
class SyncDoneValue(univ.Sequence):
"""
syncDoneValue ::= SEQUENCE {
cookie syncCookie OPTIONAL,
refreshDeletes BOOLEAN DEFAULT FALSE
}
"""
componentType = namedtype.NamedTypes(
namedtype.OptionalNamedType('cookie', SyncCookie()),
namedtype.DefaultedNamedType('refreshDeletes', univ.Boolean(False))
)
class SyncDoneControl(ResponseControl):
"""
The Sync Done Control is an LDAP Control [RFC4511] where the
controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.3 and the
controlValue contains a BER-encoded syncDoneValue. The criticality
is FALSE (and hence absent).
[..]
The Sync Done Control is only applicable to the SearchResultDone
Message.
"""
controlType = '1.3.6.1.4.1.4203.1.9.1.3'
def decodeControlValue(self, encodedControlValue):
d = decoder.decode(encodedControlValue, asn1Spec=SyncDoneValue())
cookie = d[0].getComponentByName('cookie')
if cookie.hasValue():
self.cookie = str(cookie)
else:
self.cookie = None
refresh_deletes = d[0].getComponentByName('refreshDeletes')
if refresh_deletes.hasValue():
self.refreshDeletes = bool(refresh_deletes)
else:
self.refreshDeletes = None
KNOWN_RESPONSE_CONTROLS[SyncDoneControl.controlType] = SyncDoneControl
class RefreshDelete(univ.Sequence):
"""
refreshDelete [1] SEQUENCE {
cookie syncCookie OPTIONAL,
refreshDone BOOLEAN DEFAULT TRUE
},
"""
componentType = namedtype.NamedTypes(
namedtype.OptionalNamedType('cookie', SyncCookie()),
namedtype.DefaultedNamedType('refreshDone', univ.Boolean(True))
)
class RefreshPresent(univ.Sequence):
"""
refreshPresent [2] SEQUENCE {
cookie syncCookie OPTIONAL,
refreshDone BOOLEAN DEFAULT TRUE
},
"""
componentType = namedtype.NamedTypes(
namedtype.OptionalNamedType('cookie', SyncCookie()),
namedtype.DefaultedNamedType('refreshDone', univ.Boolean(True))
)
class SyncUUIDs(univ.SetOf):
"""
syncUUIDs SET OF syncUUID
"""
componentType = SyncUUID()
class SyncIdSet(univ.Sequence):
"""
syncIdSet [3] SEQUENCE {
cookie syncCookie OPTIONAL,
refreshDeletes BOOLEAN DEFAULT FALSE,
syncUUIDs SET OF syncUUID
}
"""
componentType = namedtype.NamedTypes(
namedtype.OptionalNamedType('cookie', SyncCookie()),
namedtype.DefaultedNamedType('refreshDeletes', univ.Boolean(False)),
namedtype.NamedType('syncUUIDs', SyncUUIDs())
)
class SyncInfoValue(univ.Choice):
"""
syncInfoValue ::= CHOICE {
newcookie [0] syncCookie,
refreshDelete [1] SEQUENCE {
cookie syncCookie OPTIONAL,
refreshDone BOOLEAN DEFAULT TRUE
},
refreshPresent [2] SEQUENCE {
cookie syncCookie OPTIONAL,
refreshDone BOOLEAN DEFAULT TRUE
},
syncIdSet [3] SEQUENCE {
cookie syncCookie OPTIONAL,
refreshDeletes BOOLEAN DEFAULT FALSE,
syncUUIDs SET OF syncUUID
}
}
"""
componentType = namedtype.NamedTypes(
namedtype.NamedType(
'newcookie',
SyncCookie().subtype(
implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0)
)
),
namedtype.NamedType(
'refreshDelete',
RefreshDelete().subtype(
implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1)
)
),
namedtype.NamedType(
'refreshPresent',
RefreshPresent().subtype(
implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2)
)
),
namedtype.NamedType(
'syncIdSet',
SyncIdSet().subtype(
implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3)
)
)
)
class SyncInfoMessage:
"""
The Sync Info Message is an LDAP Intermediate Response Message
[RFC4511] where responseName is the object identifier
1.3.6.1.4.1.4203.1.9.1.4 and responseValue contains a BER-encoded
syncInfoValue. The criticality is FALSE (and hence absent).
"""
responseName = '1.3.6.1.4.1.4203.1.9.1.4'
def __init__(self, encodedMessage):
d = decoder.decode(encodedMessage, asn1Spec=SyncInfoValue())
self.newcookie = None
self.refreshDelete = None
self.refreshPresent = None
self.syncIdSet = None
for attr in ['newcookie', 'refreshDelete', 'refreshPresent', 'syncIdSet']:
comp = d[0].getComponentByName(attr)
if comp is not None and comp.hasValue():
if attr == 'newcookie':
self.newcookie = str(comp)
return
val = {}
cookie = comp.getComponentByName('cookie')
if cookie.hasValue():
val['cookie'] = str(cookie)
if attr.startswith('refresh'):
val['refreshDone'] = bool(comp.getComponentByName('refreshDone'))
elif attr == 'syncIdSet':
uuids = []
ids = comp.getComponentByName('syncUUIDs')
for i in range(len(ids)):
uuid = UUID(bytes=bytes(ids.getComponentByPosition(i)))
uuids.append(str(uuid))
val['syncUUIDs'] = uuids
val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes'))
setattr(self, attr, val)
return
class SyncreplConsumer:
"""
SyncreplConsumer - LDAP syncrepl consumer object.
"""
def syncrepl_search(self, base, scope, mode='refreshOnly', cookie=None, **search_args):
"""
Starts syncrepl search operation.
base, scope, and search_args are passed along to
self.search_ext unmodified (aside from adding a Sync
Request control to any serverctrls provided).
mode provides syncrepl mode. Can be 'refreshOnly'
to finish after synchronization, or
'refreshAndPersist' to persist (continue to
receive updates) after synchronization.
cookie: an opaque value representing the replication
state of the client. Subclasses should override
the syncrepl_set_cookie() and syncrepl_get_cookie()
methods to store the cookie appropriately, rather than
passing it.
Only a single syncrepl search may be active on a SyncreplConsumer
object. Multiple concurrent syncrepl searches require multiple
separate SyncreplConsumer objects and thus multiple connections
(LDAPObject instances).
"""
if cookie is None:
cookie = self.syncrepl_get_cookie()
syncreq = SyncRequestControl(cookie=cookie, mode=mode)
if 'serverctrls' in search_args:
search_args['serverctrls'] += [syncreq]
else:
search_args['serverctrls'] = [syncreq]
self.__refreshDone = False
return self.search_ext(base, scope, **search_args)
def syncrepl_poll(self, msgid=-1, timeout=None, all=0):
"""
polls for and processes responses to the syncrepl_search() operation.
Returns False when operation finishes, True if it is in progress, or
raises an exception on error.
If timeout is specified, raises ldap.TIMEOUT in the event of a timeout.
If all is set to a nonzero value, poll() will return only when finished
or when an exception is raised.
"""
while True:
type, msg, mid, ctrls, n, v = self.result4(
msgid=msgid,
timeout=timeout,
add_intermediates=1,
add_ctrls=1,
all=0,
)
if type == 101:
# search result. This marks the end of a refreshOnly session.
# look for a SyncDone control, save the cookie, and if necessary
# delete non-present entries.
for c in ctrls:
if c.__class__.__name__ != 'SyncDoneControl':
continue
self.syncrepl_present(None, refreshDeletes=c.refreshDeletes)
if c.cookie is not None:
self.syncrepl_set_cookie(c.cookie)
return False
elif type == 100:
# search entry with associated SyncState control
for m in msg:
dn, attrs, ctrls = m
for c in ctrls:
if c.__class__.__name__ != 'SyncStateControl':
continue
if c.state == 'present':
self.syncrepl_present([c.entryUUID])
elif c.state == 'delete':
self.syncrepl_delete([c.entryUUID])
else:
self.syncrepl_entry(dn, attrs, c.entryUUID)
if self.__refreshDone is False:
self.syncrepl_present([c.entryUUID])
if c.cookie is not None:
self.syncrepl_set_cookie(c.cookie)
break
elif type == 121:
# Intermediate message. If it is a SyncInfoMessage, parse it
for m in msg:
rname, resp, ctrls = m
if rname != SyncInfoMessage.responseName:
continue
sim = SyncInfoMessage(resp)
if sim.newcookie is not None:
self.syncrepl_set_cookie(sim.newcookie)
elif sim.refreshPresent is not None:
self.syncrepl_present(None, refreshDeletes=False)
if 'cookie' in sim.refreshPresent:
self.syncrepl_set_cookie(sim.refreshPresent['cookie'])
if sim.refreshPresent['refreshDone']:
self.__refreshDone = True
self.syncrepl_refreshdone()
elif sim.refreshDelete is not None:
self.syncrepl_present(None, refreshDeletes=True)
if 'cookie' in sim.refreshDelete:
self.syncrepl_set_cookie(sim.refreshDelete['cookie'])
if sim.refreshDelete['refreshDone']:
self.__refreshDone = True
self.syncrepl_refreshdone()
elif sim.syncIdSet is not None:
if sim.syncIdSet['refreshDeletes'] is True:
self.syncrepl_delete(sim.syncIdSet['syncUUIDs'])
else:
self.syncrepl_present(sim.syncIdSet['syncUUIDs'])
if 'cookie' in sim.syncIdSet:
self.syncrepl_set_cookie(sim.syncIdSet['cookie'])
if all == 0:
return True
# virtual methods -- subclass must override these to do useful work
def syncrepl_set_cookie(self, cookie):
"""
Called by syncrepl_poll() to store a new cookie provided by the server.
"""
pass
def syncrepl_get_cookie(self):
"""
Called by syncrepl_search() to retrieve the cookie stored by syncrepl_set_cookie()
"""
pass
def syncrepl_present(self, uuids, refreshDeletes=False):
"""
Called by syncrepl_poll() whenever entry UUIDs are presented to the client.
syncrepl_present() is given a list of entry UUIDs (uuids) and a flag
(refreshDeletes) which indicates whether the server explicitly deleted
non-present entries during the refresh operation.
If called with a list of uuids, the syncrepl_present() implementation
should record those uuids as present in the directory.
If called with uuids set to None and refreshDeletes set to False,
syncrepl_present() should delete all non-present entries from the local
mirror, and reset the list of recorded uuids.
If called with uuids set to None and refreshDeletes set to True,
syncrepl_present() should reset the list of recorded uuids, without
deleting any entries.
"""
pass
def syncrepl_delete(self, uuids):
"""
Called by syncrepl_poll() to delete entries. A list
of UUIDs of the entries to be deleted is given in the
uuids parameter.
"""
pass
def syncrepl_entry(self, dn, attrs, uuid):
"""
Called by syncrepl_poll() for any added or modified entries.
The provided uuid is used to identify the provided entry in
any future modification (including dn modification), deletion,
and presentation operations.
"""
pass
def syncrepl_refreshdone(self):
"""
Called by syncrepl_poll() between refresh and persist phase.
It indicates that initial synchronization is done and persist phase
follows.
"""
pass