# Ldaptor, a Pure-Python library for LDAP
# Copyright (C) 2003 Tommi Virtanen
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""LDAP protocol server"""
from ldaptor import interfaces, delta
from ldaptor.protocols import pureldap, pureber
from ldaptor.protocols.ldap import distinguishedname, ldaperrors
from twisted.python import log
from twisted.internet import protocol, defer
[docs]class LDAPServerConnectionLostException(ldaperrors.LDAPException):
pass
[docs]class BaseLDAPServer(protocol.Protocol):
debug = False
def __init__(self):
self.buffer = ''
self.connected = None
berdecoder = pureldap.LDAPBERDecoderContext_TopLevel(
inherit=pureldap.LDAPBERDecoderContext_LDAPMessage(
fallback=pureldap.LDAPBERDecoderContext(fallback=pureber.BERDecoderContext()),
inherit=pureldap.LDAPBERDecoderContext(fallback=pureber.BERDecoderContext())))
[docs] def dataReceived(self, recd):
self.buffer += recd
while 1:
try:
o, bytes=pureber.berDecodeObject(self.berdecoder, self.buffer)
except pureber.BERExceptionInsufficientData: #TODO
o, bytes=None, 0
self.buffer = self.buffer[bytes:]
if o is None:
break
self.handle(o)
[docs] def connectionMade(self):
"""TCP connection has opened"""
self.connected = 1
[docs] def connectionLost(self, reason=protocol.connectionDone):
"""Called when TCP connection has been lost"""
self.connected = 0
[docs] def queue(self, id, op):
if not self.connected:
raise LDAPServerConnectionLostException()
msg=pureldap.LDAPMessage(op, id=id)
if self.debug:
log.msg('S->C %s' % repr(msg), debug=True)
self.transport.write(str(msg))
[docs] def unsolicitedNotification(self, msg):
log.msg("Got unsolicited notification: %s" % repr(msg))
[docs] def checkControls(self, controls):
if controls is not None:
for controlType, criticality, controlValue in controls:
if criticality:
raise ldaperrors.LDAPUnavailableCriticalExtension, \
'Unknown control %s' % controlType
[docs] def handleUnknown(self, request, controls, callback):
log.msg('Unknown request: %r' % request)
msg = pureldap.LDAPExtendedResponse(resultCode=ldaperrors.LDAPProtocolError.resultCode,
responseName='1.3.6.1.4.1.1466.20036',
errorMessage='Unknown request')
return msg
def _cbLDAPError(self, reason, name):
reason.trap(ldaperrors.LDAPException)
return self._callErrorHandler(name=name,
resultCode=reason.value.resultCode,
errorMessage=reason.value.message)
def _cbHandle(self, response, id):
if response is not None:
self.queue(id, response)
[docs] def failDefault(self, resultCode, errorMessage):
return pureldap.LDAPExtendedResponse(resultCode=resultCode,
responseName='1.3.6.1.4.1.1466.20036',
errorMessage=errorMessage)
def _callErrorHandler(self, name, resultCode, errorMessage):
errh = getattr(self, 'fail_'+name, self.failDefault)
return errh(resultCode=resultCode, errorMessage=errorMessage)
def _cbOtherError(self, reason, name):
return self._callErrorHandler(name=name,
resultCode=ldaperrors.LDAPProtocolError.resultCode,
errorMessage=reason.getErrorMessage())
[docs] def handle(self, msg):
assert isinstance(msg.value, pureldap.LDAPProtocolRequest)
if self.debug:
log.msg('S<-C %s' % repr(msg), debug=True)
if msg.id==0:
self.unsolicitedNotification(msg.value)
else:
name = msg.value.__class__.__name__
handler = getattr(self, 'handle_'+name, self.handleUnknown)
d = defer.maybeDeferred(handler,
msg.value,
msg.controls,
lambda response: self._cbHandle(response, msg.id))
d.addErrback(self._cbLDAPError, name)
d.addErrback(defer.logError)
d.addErrback(self._cbOtherError, name)
d.addCallback(self._cbHandle, msg.id)
[docs]class LDAPServer(BaseLDAPServer):
"""An LDAP server"""
boundUser = None
fail_LDAPBindRequest = pureldap.LDAPBindResponse
[docs] def handle_LDAPBindRequest(self, request, controls, reply):
if request.version != 3:
raise ldaperrors.LDAPProtocolError, \
'Version %u not supported' % request.version
self.checkControls(controls)
if request.dn == '':
# anonymous bind
self.boundUser=None
return pureldap.LDAPBindResponse(resultCode=0)
else:
dn = distinguishedname.DistinguishedName(request.dn)
root = interfaces.IConnectedLDAPEntry(self.factory)
d = root.lookup(dn)
def _noEntry(fail):
fail.trap(ldaperrors.LDAPNoSuchObject)
return None
d.addErrback(_noEntry)
def _gotEntry(entry, auth):
if entry is None:
raise ldaperrors.LDAPInvalidCredentials
d = entry.bind(auth)
def _cb(entry):
self.boundUser=entry
msg = pureldap.LDAPBindResponse(
resultCode=ldaperrors.Success.resultCode,
matchedDN=str(entry.dn))
return msg
d.addCallback(_cb)
return d
d.addCallback(_gotEntry, request.auth)
return d
[docs] def handle_LDAPUnbindRequest(self, request, controls, reply):
# explicitly do not check unsupported critical controls -- we
# have no way to return an error, anyway.
self.transport.loseConnection()
[docs] def getRootDSE(self, request, reply):
root = interfaces.IConnectedLDAPEntry(self.factory)
reply(pureldap.LDAPSearchResultEntry(
objectName='',
attributes=[ ('supportedLDAPVersion', ['3']),
('namingContexts', [str(root.dn)]),
('supportedExtension', [
pureldap.LDAPPasswordModifyRequest.oid,
]),
],
))
return pureldap.LDAPSearchResultDone(resultCode=ldaperrors.Success.resultCode)
def _cbSearchGotBase(self, base, dn, request, reply):
def _sendEntryToClient(entry):
reply(pureldap.LDAPSearchResultEntry(
objectName=str(entry.dn),
attributes=entry.items(),
))
d = base.search(filterObject=request.filter,
attributes=request.attributes,
scope=request.scope,
derefAliases=request.derefAliases,
sizeLimit=request.sizeLimit,
timeLimit=request.timeLimit,
typesOnly=request.typesOnly,
callback=_sendEntryToClient)
def _done(_):
return pureldap.LDAPSearchResultDone(resultCode=ldaperrors.Success.resultCode)
d.addCallback(_done)
return d
def _cbSearchLDAPError(self, reason):
reason.trap(ldaperrors.LDAPException)
return pureldap.LDAPSearchResultDone(resultCode=reason.value.resultCode)
def _cbSearchOtherError(self, reason):
return pureldap.LDAPSearchResultDone(resultCode=ldaperrors.other,
errorMessage=reason.getErrorMessage())
fail_LDAPSearchRequest = pureldap.LDAPSearchResultDone
[docs] def handle_LDAPSearchRequest(self, request, controls, reply):
self.checkControls(controls)
if (request.baseObject == ''
and request.scope == pureldap.LDAP_SCOPE_baseObject
and request.filter == pureldap.LDAPFilter_present('objectClass')):
return self.getRootDSE(request, reply)
dn = distinguishedname.DistinguishedName(request.baseObject)
root = interfaces.IConnectedLDAPEntry(self.factory)
d = root.lookup(dn)
d.addCallback(self._cbSearchGotBase, dn, request, reply)
d.addErrback(self._cbSearchLDAPError)
d.addErrback(defer.logError)
d.addErrback(self._cbSearchOtherError)
return d
fail_LDAPDelRequest = pureldap.LDAPDelResponse
[docs] def handle_LDAPDelRequest(self, request, controls, reply):
self.checkControls(controls)
dn = distinguishedname.DistinguishedName(request.value)
root = interfaces.IConnectedLDAPEntry(self.factory)
d = root.lookup(dn)
def _gotEntry(entry):
d = entry.delete()
return d
d.addCallback(_gotEntry)
def _report(entry):
return pureldap.LDAPDelResponse(resultCode=0)
d.addCallback(_report)
return d
fail_LDAPAddRequest = pureldap.LDAPAddResponse
[docs] def handle_LDAPAddRequest(self, request, controls, reply):
self.checkControls(controls)
attributes = {}
for name, vals in request.attributes:
attributes.setdefault(name.value, set())
attributes[name.value].update([x.value for x in vals])
dn = distinguishedname.DistinguishedName(request.entry)
rdn = str(dn.split()[0])
parent = dn.up()
root = interfaces.IConnectedLDAPEntry(self.factory)
d = root.lookup(parent)
def _gotEntry(parent):
d = parent.addChild(rdn, attributes)
return d
d.addCallback(_gotEntry)
def _report(entry):
return pureldap.LDAPAddResponse(resultCode=0)
d.addCallback(_report)
return d
fail_LDAPModifyDNRequest = pureldap.LDAPModifyDNResponse
[docs] def handle_LDAPModifyDNRequest(self, request, controls, reply):
self.checkControls(controls)
dn = distinguishedname.DistinguishedName(request.entry)
newrdn = distinguishedname.RelativeDistinguishedName(request.newrdn)
deleteoldrdn = bool(request.deleteoldrdn)
if not deleteoldrdn:
#TODO support this
raise ldaperrors.LDAPUnwillingToPerform("Cannot handle preserving old RDN yet.")
newSuperior = request.newSuperior
if newSuperior is None:
newSuperior = dn.up()
else:
newSuperior = distinguishedname.DistinguishedName(newSuperior)
newdn = distinguishedname.DistinguishedName(
listOfRDNs=(newrdn,)+newSuperior.split())
#TODO make this more atomic
root = interfaces.IConnectedLDAPEntry(self.factory)
d = root.lookup(dn)
def _gotEntry(entry):
d = entry.move(newdn)
return d
d.addCallback(_gotEntry)
def _report(entry):
return pureldap.LDAPModifyDNResponse(resultCode=0)
d.addCallback(_report)
return d
fail_LDAPModifyRequest = pureldap.LDAPModifyResponse
[docs] def handle_LDAPModifyRequest(self, request, controls, reply):
self.checkControls(controls)
root = interfaces.IConnectedLDAPEntry(self.factory)
mod = delta.ModifyOp.fromLDAP(request)
d = mod.patch(root)
def _patched(entry):
return entry.commit()
d.addCallback(_patched)
def _report(entry):
return pureldap.LDAPModifyResponse(resultCode=0)
d.addCallback(_report)
return d
fail_LDAPExtendedRequest = pureldap.LDAPExtendedResponse
[docs] def handle_LDAPExtendedRequest(self, request, controls, reply):
self.checkControls(controls)
for handler in [getattr(self, attr)
for attr in dir(self)
if attr.startswith('extendedRequest_')]:
if getattr(handler, 'oid', None) == request.requestName:
berdecoder = getattr(handler, 'berdecoder', None)
if berdecoder is None:
values = [request.requestValue]
else:
values = pureber.berDecodeMultiple(request.requestValue, berdecoder)
d = defer.maybeDeferred(handler, *values, **{'reply': reply})
def eb(fail, oid):
fail.trap(ldaperrors.LDAPException)
return pureldap.LDAPExtendedResponse(
resultCode=fail.value.resultCode,
errorMessage=fail.value.message,
responseName=oid,
)
d.addErrback(eb, request.requestName)
return d
raise ldaperrors.LDAPProtocolError('Unknown extended request: %s' % request.requestName)
[docs] def extendedRequest_LDAPPasswordModifyRequest(self, data, reply):
if not isinstance(data, pureber.BERSequence):
raise ldaperrors.LDAPProtocolError('Extended request PasswordModify expected a BERSequence.')
userIdentity = None
oldPasswd = None
newPasswd = None
for value in data:
if isinstance(value, pureldap.LDAPPasswordModifyRequest_userIdentity):
if userIdentity is not None:
raise ldaperrors.LDAPProtocolError(
'Extended request PasswordModify received userIdentity twice.')
userIdentity = value.value
elif isinstance(value, pureldap.LDAPPasswordModifyRequest_oldPasswd):
if oldPasswd is not None:
raise ldaperrors.LDAPProtocolError('Extended request PasswordModify received oldPasswd twice.')
oldPasswd = value.value
elif isinstance(value, pureldap.LDAPPasswordModifyRequest_newPasswd):
if newPasswd is not None:
raise ldaperrors.LDAPProtocolError('Extended request PasswordModify received newPasswd twice.')
newPasswd = value.value
else:
raise ldaperrors.LDAPProtocolError('Extended request PasswordModify received unexpected item.')
if self.boundUser is None:
raise ldaperrors.LDAPStrongAuthRequired()
if (userIdentity is not None
and userIdentity != self.boundUser.dn):
#TODO this hardcodes ACL
log.msg('User %(actor)s tried to change password of %(target)s' % {
'actor': str(self.boundUser.dn),
'target': str(userIdentity),
})
raise ldaperrors.LDAPInsufficientAccessRights()
if (oldPasswd is not None
or newPasswd is None):
raise ldaperrors.LDAPOperationsError('Password does not support this case.')
self.boundUser.setPassword(newPasswd)
return pureldap.LDAPExtendedResponse(resultCode=ldaperrors.Success.resultCode,
responseName=self.extendedRequest_LDAPPasswordModifyRequest.oid)
# TODO
if userIdentity is None:
userIdentity = str(self.boundUser.dn)
raise NotImplementedError('VALUE %r' % value)
extendedRequest_LDAPPasswordModifyRequest.oid = pureldap.LDAPPasswordModifyRequest.oid
extendedRequest_LDAPPasswordModifyRequest.berdecoder = (
pureber.BERDecoderContext(
inherit=pureldap.LDAPBERDecoderContext_LDAPPasswordModifyRequest(inherit=pureber.BERDecoderContext())))
if __name__ == '__main__':
"""
Demonstration LDAP server; reads LDIF from stdin and
serves that over LDAP on port 10389.
"""
from twisted.internet import reactor
import sys
log.startLogging(sys.stderr)
from twisted.python import components
from ldaptor import inmemory
class LDAPServerFactory(protocol.ServerFactory):
def __init__(self, root):
self.root = root
components.registerAdapter(lambda x: x.root,
LDAPServerFactory,
interfaces.IConnectedLDAPEntry)
def start(db):
factory = LDAPServerFactory(db)
factory.protocol = LDAPServer
reactor.listenTCP(10389, factory)
d = inmemory.fromLDIFFile(sys.stdin)
d.addCallback(start)
d.addErrback(log.err)
reactor.run()