added ldap3 lib and ldap3 backend
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user