Coverage for /root/GitHubProjects/impacket/impacket/ldap/ldap.py : 52%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
2#
3# This software is provided under under a slightly modified version
4# of the Apache Software License. See the accompanying LICENSE file
5# for more information.
6#
7# Authors: Alberto Solino (@agsolino)
8# Kacper Nowak (@kacpern)
9#
10# Description:
11# RFC 4511 Minimalistic implementation. We don't need much functionality yet
12# If we need more complex use cases we might opt to use a third party implementation
13# Keep in mind the APIs are still unstable, might require to re-write your scripts
14# as we change them.
15# Adding [MS-ADTS] specific functionality
16#
17# ToDo:
18# [x] Implement Paging Search, especially important for big requests
19#
21import os
22import re
23import socket
24from binascii import unhexlify
25import random
27from pyasn1.codec.ber import encoder, decoder
28from pyasn1.error import SubstrateUnderrunError
29from pyasn1.type.univ import noValue
31from impacket import LOG
32from impacket.ldap.ldapasn1 import Filter, Control, SimplePagedResultsControl, ResultCode, Scope, DerefAliases, Operation, \
33 KNOWN_CONTROLS, CONTROL_PAGEDRESULTS, NOTIFICATION_DISCONNECT, KNOWN_NOTIFICATIONS, BindRequest, SearchRequest, \
34 SearchResultDone, LDAPMessage
35from impacket.ntlm import getNTLMSSPType1, getNTLMSSPType3
36from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
38try:
39 import OpenSSL
40 from OpenSSL import SSL, crypto
41except:
42 LOG.critical("pyOpenSSL is not installed, can't continue")
43 raise
45__all__ = [
46 'LDAPConnection', 'LDAPFilterSyntaxError', 'LDAPFilterInvalidException', 'LDAPSessionError', 'LDAPSearchError',
47 'Control', 'SimplePagedResultsControl', 'ResultCode', 'Scope', 'DerefAliases', 'Operation',
48 'CONTROL_PAGEDRESULTS', 'KNOWN_CONTROLS', 'NOTIFICATION_DISCONNECT', 'KNOWN_NOTIFICATIONS',
49]
51# https://tools.ietf.org/search/rfc4515#section-3
52DESCRIPTION = r'(?:[a-z][a-z0-9\-]*)'
53NUMERIC_OID = r'(?:(?:\d|[1-9]\d+)(?:\.(?:\d|[1-9]\d+))*)'
54OID = r'(?:%s|%s)' % (DESCRIPTION, NUMERIC_OID)
55OPTIONS = r'(?:(?:;[a-z0-9\-]+)*)'
56ATTRIBUTE = r'(%s%s)' % (OID, OPTIONS)
57DN = r'(:dn)'
58MATCHING_RULE = r'(?::(%s))' % OID
60RE_OPERATOR = re.compile(r'([:<>~]?=)')
61RE_ATTRIBUTE = re.compile(r'^%s$' % ATTRIBUTE, re.I)
62RE_EX_ATTRIBUTE_1 = re.compile(r'^%s%s?%s?$' % (ATTRIBUTE, DN, MATCHING_RULE), re.I)
63RE_EX_ATTRIBUTE_2 = re.compile(r'^(){0}%s?%s$' % (DN, MATCHING_RULE), re.I)
66class LDAPConnection:
67 def __init__(self, url, baseDN='', dstIp=None):
68 """
69 LDAPConnection class
71 :param string url:
72 :param string baseDN:
73 :param string dstIp:
75 :return: a LDAP instance, if not raises a LDAPSessionError exception
76 """
77 self._SSL = False
78 self._dstPort = 0
79 self._dstHost = 0
80 self._socket = None
81 self._baseDN = baseDN
82 self._dstIp = dstIp
84 if url.startswith('ldap://'): 84 ↛ 88line 84 didn't jump to line 88, because the condition on line 84 was never false
85 self._dstPort = 389
86 self._SSL = False
87 self._dstHost = url[7:]
88 elif url.startswith('ldaps://'):
89 self._dstPort = 636
90 self._SSL = True
91 self._dstHost = url[8:]
92 elif url.startswith('gc://'):
93 self._dstPort = 3268
94 self._SSL = False
95 self._dstHost = url[5:]
96 else:
97 raise LDAPSessionError(errorString="Unknown URL prefix: '%s'" % url)
99 # Try to connect
100 if self._dstIp is not None: 100 ↛ 101line 100 didn't jump to line 101, because the condition on line 100 was never true
101 targetHost = self._dstIp
102 else:
103 targetHost = self._dstHost
105 LOG.debug('Connecting to %s, port %d, SSL %s' % (targetHost, self._dstPort, self._SSL))
106 try:
107 af, socktype, proto, _, sa = socket.getaddrinfo(targetHost, self._dstPort, 0, socket.SOCK_STREAM)[0]
108 self._socket = socket.socket(af, socktype, proto)
109 except socket.error as e:
110 raise socket.error('Connection error (%s:%d)' % (targetHost, 88), e)
112 if self._SSL is False: 112 ↛ 116line 112 didn't jump to line 116, because the condition on line 112 was never false
113 self._socket.connect(sa)
114 else:
115 # Switching to TLS now
116 ctx = SSL.Context(SSL.TLSv1_METHOD)
117 # ctx.set_cipher_list('RC4')
118 self._socket = SSL.Connection(ctx, self._socket)
119 self._socket.connect(sa)
120 self._socket.do_handshake()
122 def kerberosLogin(self, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None,
123 TGS=None, useCache=True):
124 """
125 logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.
127 :param string user: username
128 :param string password: password for the user
129 :param string domain: domain where the account is valid for (required)
130 :param string lmhash: LMHASH used to authenticate using hashes (password is not used)
131 :param string nthash: NTHASH used to authenticate using hashes (password is not used)
132 :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication
133 :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)
134 :param struct TGT: If there's a TGT available, send the structure here and it will be used
135 :param struct TGS: same for TGS. See smb3.py for the format
136 :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False
138 :return: True, raises a LDAPSessionError if error.
139 """
141 if lmhash != '' or nthash != '':
142 if len(lmhash) % 2: 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true
143 lmhash = '0' + lmhash
144 if len(nthash) % 2: 144 ↛ 145line 144 didn't jump to line 145, because the condition on line 144 was never true
145 nthash = '0' + nthash
146 try: # just in case they were converted already
147 lmhash = unhexlify(lmhash)
148 nthash = unhexlify(nthash)
149 except TypeError:
150 pass
152 # Importing down here so pyasn1 is not required if kerberos is not used.
153 from impacket.krb5.ccache import CCache
154 from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set
155 from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
156 from impacket.krb5 import constants
157 from impacket.krb5.types import Principal, KerberosTime, Ticket
158 import datetime
160 if TGT is not None or TGS is not None: 160 ↛ 161line 160 didn't jump to line 161, because the condition on line 160 was never true
161 useCache = False
163 if useCache: 163 ↛ 200line 163 didn't jump to line 200, because the condition on line 163 was never false
164 try:
165 ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
166 except:
167 # No cache present
168 pass
169 else:
170 # retrieve domain information from CCache file if needed
171 if domain == '':
172 domain = ccache.principal.realm['data'].decode('utf-8')
173 LOG.debug('Domain retrieved from CCache: %s' % domain)
175 LOG.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
176 principal = 'ldap/%s@%s' % (self._dstHost.upper(), domain.upper())
177 creds = ccache.getCredential(principal)
178 if creds is None:
179 # Let's try for the TGT and go from there
180 principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
181 creds = ccache.getCredential(principal)
182 if creds is not None:
183 TGT = creds.toTGT()
184 LOG.debug('Using TGT from cache')
185 else:
186 LOG.debug('No valid credentials found in cache')
187 else:
188 TGS = creds.toTGS(principal)
189 LOG.debug('Using TGS from cache')
191 # retrieve user information from CCache file if needed
192 if user == '' and creds is not None:
193 user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
194 LOG.debug('Username retrieved from CCache: %s' % user)
195 elif user == '' and len(ccache.principal.components) > 0:
196 user = ccache.principal.components[0]['data'].decode('utf-8')
197 LOG.debug('Username retrieved from CCache: %s' % user)
199 # First of all, we need to get a TGT for the user
200 userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
201 if TGT is None: 201 ↛ 206line 201 didn't jump to line 206, because the condition on line 201 was never false
202 if TGS is None: 202 ↛ 210line 202 didn't jump to line 210, because the condition on line 202 was never false
203 tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash,
204 aesKey, kdcHost)
205 else:
206 tgt = TGT['KDC_REP']
207 cipher = TGT['cipher']
208 sessionKey = TGT['sessionKey']
210 if TGS is None: 210 ↛ 215line 210 didn't jump to line 215, because the condition on line 210 was never false
211 serverName = Principal('ldap/%s' % self._dstHost, type=constants.PrincipalNameType.NT_SRV_INST.value)
212 tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher,
213 sessionKey)
214 else:
215 tgs = TGS['KDC_REP']
216 cipher = TGS['cipher']
217 sessionKey = TGS['sessionKey']
219 # Let's build a NegTokenInit with a Kerberos REQ_AP
221 blob = SPNEGO_NegTokenInit()
223 # Kerberos
224 blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]
226 # Let's extract the ticket from the TGS
227 tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
228 ticket = Ticket()
229 ticket.from_asn1(tgs['ticket'])
231 # Now let's build the AP_REQ
232 apReq = AP_REQ()
233 apReq['pvno'] = 5
234 apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
236 opts = []
237 apReq['ap-options'] = constants.encodeFlags(opts)
238 seq_set(apReq, 'ticket', ticket.to_asn1)
240 authenticator = Authenticator()
241 authenticator['authenticator-vno'] = 5
242 authenticator['crealm'] = domain
243 seq_set(authenticator, 'cname', userName.components_to_asn1)
244 now = datetime.datetime.utcnow()
246 authenticator['cusec'] = now.microsecond
247 authenticator['ctime'] = KerberosTime.to_asn1(now)
249 encodedAuthenticator = encoder.encode(authenticator)
251 # Key Usage 11
252 # AP-REQ Authenticator (includes application authenticator
253 # subkey), encrypted with the application session key
254 # (Section 5.5.1)
255 encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)
257 apReq['authenticator'] = noValue
258 apReq['authenticator']['etype'] = cipher.enctype
259 apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
261 blob['MechToken'] = encoder.encode(apReq)
263 # Done with the Kerberos saga, now let's get into LDAP
265 bindRequest = BindRequest()
266 bindRequest['version'] = 3
267 bindRequest['name'] = user
268 bindRequest['authentication']['sasl']['mechanism'] = 'GSS-SPNEGO'
269 bindRequest['authentication']['sasl']['credentials'] = blob.getData()
271 response = self.sendReceive(bindRequest)[0]['protocolOp']
273 if response['bindResponse']['resultCode'] != ResultCode('success'): 273 ↛ 274line 273 didn't jump to line 274, because the condition on line 273 was never true
274 raise LDAPSessionError(
275 errorString='Error in bindRequest -> %s: %s' % (response['bindResponse']['resultCode'].prettyPrint(),
276 response['bindResponse']['diagnosticMessage'])
277 )
279 return True
281 def login(self, user='', password='', domain='', lmhash='', nthash='', authenticationChoice='sicilyNegotiate'):
282 """
283 logins into the target system
285 :param string user: username
286 :param string password: password for the user
287 :param string domain: domain where the account is valid for
288 :param string lmhash: LMHASH used to authenticate using hashes (password is not used)
289 :param string nthash: NTHASH used to authenticate using hashes (password is not used)
290 :param string authenticationChoice: type of authentication protocol to use (default NTLM)
292 :return: True, raises a LDAPSessionError if error.
293 """
294 bindRequest = BindRequest()
295 bindRequest['version'] = 3
297 if authenticationChoice == 'simple': 297 ↛ 298line 297 didn't jump to line 298, because the condition on line 297 was never true
298 if '.' in domain:
299 bindRequest['name'] = user + '@' + domain
300 elif domain:
301 bindRequest['name'] = domain + '\\' + user
302 else:
303 bindRequest['name'] = user
304 bindRequest['authentication']['simple'] = password
305 response = self.sendReceive(bindRequest)[0]['protocolOp']
306 elif authenticationChoice == 'sicilyPackageDiscovery':
307 bindRequest['name'] = user
308 bindRequest['authentication']['sicilyPackageDiscovery'] = ''
309 response = self.sendReceive(bindRequest)[0]['protocolOp']
310 elif authenticationChoice == 'sicilyNegotiate': 310 ↛ 338line 310 didn't jump to line 338, because the condition on line 310 was never false
311 # Deal with NTLM Authentication
312 if lmhash != '' or nthash != '':
313 if len(lmhash) % 2: 313 ↛ 314line 313 didn't jump to line 314, because the condition on line 313 was never true
314 lmhash = '0' + lmhash
315 if len(nthash) % 2: 315 ↛ 316line 315 didn't jump to line 316, because the condition on line 315 was never true
316 nthash = '0' + nthash
317 try: # just in case they were converted already
318 lmhash = unhexlify(lmhash)
319 nthash = unhexlify(nthash)
320 except TypeError:
321 pass
323 bindRequest['name'] = user
325 # NTLM Negotiate
326 negotiate = getNTLMSSPType1('', domain)
327 bindRequest['authentication']['sicilyNegotiate'] = negotiate.getData()
328 response = self.sendReceive(bindRequest)[0]['protocolOp']
330 # NTLM Challenge
331 type2 = response['bindResponse']['matchedDN']
333 # NTLM Auth
334 type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash)
335 bindRequest['authentication']['sicilyResponse'] = type3.getData()
336 response = self.sendReceive(bindRequest)[0]['protocolOp']
337 else:
338 raise LDAPSessionError(errorString="Unknown authenticationChoice: '%s'" % authenticationChoice)
340 if response['bindResponse']['resultCode'] != ResultCode('success'): 340 ↛ 341line 340 didn't jump to line 341, because the condition on line 340 was never true
341 raise LDAPSessionError(
342 errorString='Error in bindRequest -> %s: %s' % (response['bindResponse']['resultCode'].prettyPrint(),
343 response['bindResponse']['diagnosticMessage'])
344 )
346 return True
348 def search(self, searchBase=None, scope=None, derefAliases=None, sizeLimit=0, timeLimit=0, typesOnly=False,
349 searchFilter='(objectClass=*)', attributes=None, searchControls=None, perRecordCallback=None):
350 if searchBase is None: 350 ↛ 352line 350 didn't jump to line 352, because the condition on line 350 was never false
351 searchBase = self._baseDN
352 if scope is None: 352 ↛ 354line 352 didn't jump to line 354, because the condition on line 352 was never false
353 scope = Scope('wholeSubtree')
354 if derefAliases is None: 354 ↛ 356line 354 didn't jump to line 356, because the condition on line 354 was never false
355 derefAliases = DerefAliases('neverDerefAliases')
356 if attributes is None: 356 ↛ 357line 356 didn't jump to line 357, because the condition on line 356 was never true
357 attributes = []
359 searchRequest = SearchRequest()
360 searchRequest['baseObject'] = searchBase
361 searchRequest['scope'] = scope
362 searchRequest['derefAliases'] = derefAliases
363 searchRequest['sizeLimit'] = sizeLimit
364 searchRequest['timeLimit'] = timeLimit
365 searchRequest['typesOnly'] = typesOnly
366 searchRequest['filter'] = self._parseFilter(searchFilter)
367 searchRequest['attributes'].setComponents(*attributes)
369 done = False
370 answers = []
371 # We keep asking records until we get a SearchResultDone packet and all controls are handled
372 while not done:
373 response = self.sendReceive(searchRequest, searchControls)
374 for message in response:
375 searchResult = message['protocolOp'].getComponent()
376 if searchResult.isSameTypeWith(SearchResultDone()):
377 if searchResult['resultCode'] == ResultCode('success'): 377 ↛ 380line 377 didn't jump to line 380, because the condition on line 377 was never false
378 done = self._handleControls(searchControls, message['controls'])
379 else:
380 raise LDAPSearchError(
381 error=int(searchResult['resultCode']),
382 errorString='Error in searchRequest -> %s: %s' % (searchResult['resultCode'].prettyPrint(),
383 searchResult['diagnosticMessage']),
384 answers=answers
385 )
386 else:
387 if perRecordCallback is None: 387 ↛ 390line 387 didn't jump to line 390, because the condition on line 387 was never false
388 answers.append(searchResult)
389 else:
390 perRecordCallback(searchResult)
392 return answers
394 def _handleControls(self, requestControls, responseControls):
395 done = True
396 if requestControls is not None: 396 ↛ 397line 396 didn't jump to line 397, because the condition on line 396 was never true
397 for requestControl in requestControls:
398 if responseControls is not None:
399 for responseControl in responseControls:
400 if str(requestControl['controlType']) == CONTROL_PAGEDRESULTS:
401 if str(responseControl['controlType']) == CONTROL_PAGEDRESULTS:
402 if hasattr(responseControl, 'getCookie') is not True:
403 responseControl = decoder.decode(encoder.encode(responseControl),
404 asn1Spec=KNOWN_CONTROLS[CONTROL_PAGEDRESULTS]())[0]
405 if responseControl.getCookie():
406 done = False
407 requestControl.setCookie(responseControl.getCookie())
408 break
409 else:
410 # handle different controls here
411 pass
412 return done
414 def close(self):
415 if self._socket is not None:
416 self._socket.close()
418 def send(self, request, controls=None):
419 message = LDAPMessage()
420 message['messageID'] = random.randrange(1, 2147483647)
421 message['protocolOp'].setComponentByType(request.getTagSet(), request)
422 if controls is not None: 422 ↛ 423line 422 didn't jump to line 423, because the condition on line 422 was never true
423 message['controls'].setComponents(*controls)
425 data = encoder.encode(message)
427 return self._socket.sendall(data)
429 def recv(self):
430 REQUEST_SIZE = 8192
431 data = b''
432 done = False
433 while not done:
434 recvData = self._socket.recv(REQUEST_SIZE)
435 if len(recvData) < REQUEST_SIZE: 435 ↛ 437line 435 didn't jump to line 437, because the condition on line 435 was never false
436 done = True
437 data += recvData
439 response = []
440 while len(data) > 0:
441 try:
442 message, remaining = decoder.decode(data, asn1Spec=LDAPMessage())
443 except SubstrateUnderrunError:
444 # We need more data
445 remaining = data + self._socket.recv(REQUEST_SIZE)
446 else:
447 if message['messageID'] == 0: # unsolicited notification 447 ↛ 448line 447 didn't jump to line 448, because the condition on line 447 was never true
448 name = message['protocolOp']['extendedResp']['responseName'] or message['responseName']
449 notification = KNOWN_NOTIFICATIONS.get(name, "Unsolicited Notification '%s'" % name)
450 if name == NOTIFICATION_DISCONNECT: # Server has disconnected
451 self.close()
452 raise LDAPSessionError(
453 error=int(message['protocolOp']['extendedResp']['resultCode']),
454 errorString='%s -> %s: %s' % (notification,
455 message['protocolOp']['extendedResp']['resultCode'].prettyPrint(),
456 message['protocolOp']['extendedResp']['diagnosticMessage'])
457 )
458 response.append(message)
459 data = remaining
461 return response
463 def sendReceive(self, request, controls=None):
464 self.send(request, controls)
465 return self.recv()
467 def _parseFilter(self, filterStr):
468 try:
469 filterStr = filterStr.decode()
470 except AttributeError:
471 pass
472 filterList = list(reversed(filterStr))
473 searchFilter = self._consumeCompositeFilter(filterList)
474 if filterList: # we have not consumed the whole filter string 474 ↛ 475line 474 didn't jump to line 475, because the condition on line 474 was never true
475 raise LDAPFilterSyntaxError("unexpected token: '%s'" % filterList[-1])
476 return searchFilter
478 def _consumeCompositeFilter(self, filterList):
479 try:
480 c = filterList.pop()
481 except IndexError:
482 raise LDAPFilterSyntaxError('EOL while parsing search filter')
483 if c != '(': # filter must start with a '(' 483 ↛ 484line 483 didn't jump to line 484, because the condition on line 483 was never true
484 filterList.append(c)
485 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)
487 try:
488 operator = filterList.pop()
489 except IndexError:
490 raise LDAPFilterSyntaxError('EOL while parsing search filter')
491 if operator not in ['!', '&', '|']: # must be simple filter in this case 491 ↛ 495line 491 didn't jump to line 495, because the condition on line 491 was never false
492 filterList.extend([operator, c])
493 return self._consumeSimpleFilter(filterList)
495 filters = []
496 while True:
497 try:
498 filters.append(self._consumeCompositeFilter(filterList))
499 except LDAPFilterSyntaxError:
500 break
502 try:
503 c = filterList.pop()
504 except IndexError:
505 raise LDAPFilterSyntaxError('EOL while parsing search filter')
506 if c != ')': # filter must end with a ')'
507 filterList.append(c)
508 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)
510 return self._compileCompositeFilter(operator, filters)
512 def _consumeSimpleFilter(self, filterList):
513 try:
514 c = filterList.pop()
515 except IndexError:
516 raise LDAPFilterSyntaxError('EOL while parsing search filter')
517 if c != '(': # filter must start with a '(' 517 ↛ 518line 517 didn't jump to line 518, because the condition on line 517 was never true
518 filterList.append(c)
519 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)
521 filter = []
522 while True:
523 try:
524 c = filterList.pop()
525 except IndexError:
526 raise LDAPFilterSyntaxError('EOL while parsing search filter')
527 if c == ')': # we pop till we find a ')'
528 break
529 elif c == '(': # should be no unencoded parenthesis 529 ↛ 530line 529 didn't jump to line 530, because the condition on line 529 was never true
530 filterList.append(c)
531 raise LDAPFilterSyntaxError("unexpected token: '('")
532 else:
533 filter.append(c)
535 filterStr = ''.join(filter)
536 try:
537 # https://tools.ietf.org/search/rfc4515#section-3
538 attribute, operator, value = RE_OPERATOR.split(filterStr, 1)
539 except ValueError:
540 raise LDAPFilterInvalidException("invalid filter: '(%s)'" % filterStr)
542 return self._compileSimpleFilter(attribute, operator, value)
544 @staticmethod
545 def _compileCompositeFilter(operator, filters):
546 searchFilter = Filter()
547 if operator == '!':
548 if len(filters) != 1:
549 raise LDAPFilterInvalidException("'not' filter must have exactly one element")
550 searchFilter['not'].setComponents(*filters)
551 elif operator == '&':
552 if len(filters) == 0:
553 raise LDAPFilterInvalidException("'and' filter must have at least one element")
554 searchFilter['and'].setComponents(*filters)
555 elif operator == '|':
556 if len(filters) == 0:
557 raise LDAPFilterInvalidException("'or' filter must have at least one element")
558 searchFilter['or'].setComponents(*filters)
560 return searchFilter
562 @staticmethod
563 def _compileSimpleFilter(attribute, operator, value):
564 searchFilter = Filter()
565 if operator == ':=': # extensibleMatch 565 ↛ 566line 565 didn't jump to line 566, because the condition on line 565 was never true
566 match = RE_EX_ATTRIBUTE_1.match(attribute) or RE_EX_ATTRIBUTE_2.match(attribute)
567 if not match:
568 raise LDAPFilterInvalidException("invalid filter attribute: '%s'" % attribute)
569 attribute, dn, matchingRule = match.groups()
570 if attribute:
571 searchFilter['extensibleMatch']['type'] = attribute
572 if dn:
573 searchFilter['extensibleMatch']['dnAttributes'] = bool(dn)
574 if matchingRule:
575 searchFilter['extensibleMatch']['matchingRule'] = matchingRule
576 searchFilter['extensibleMatch']['matchValue'] = value
577 else:
578 if not RE_ATTRIBUTE.match(attribute): 578 ↛ 579line 578 didn't jump to line 579, because the condition on line 578 was never true
579 raise LDAPFilterInvalidException("invalid filter attribute: '%s'" % attribute)
580 if value == '*' and operator == '=': # present
581 searchFilter['present'] = attribute
582 elif '*' in value and operator == '=': # substring 582 ↛ 583line 582 didn't jump to line 583, because the condition on line 582 was never true
583 assertions = value.split('*')
584 choice = searchFilter['substrings']['substrings'].getComponentType()
585 substrings = []
586 if assertions[0]:
587 substrings.append(choice.clone().setComponentByName('initial', assertions[0]))
588 for assertion in assertions[1:-1]:
589 substrings.append(choice.clone().setComponentByName('any', assertion))
590 if assertions[-1]:
591 substrings.append(choice.clone().setComponentByName('final', assertions[-1]))
592 searchFilter['substrings']['type'] = attribute
593 searchFilter['substrings']['substrings'].setComponents(*substrings)
594 elif '*' not in value: # simple 594 ↛ 604line 594 didn't jump to line 604, because the condition on line 594 was never false
595 if operator == '=': 595 ↛ 597line 595 didn't jump to line 597, because the condition on line 595 was never false
596 searchFilter['equalityMatch'].setComponents(attribute, value)
597 elif operator == '~=':
598 searchFilter['approxMatch'].setComponents(attribute, value)
599 elif operator == '>=':
600 searchFilter['greaterOrEqual'].setComponents(attribute, value)
601 elif operator == '<=':
602 searchFilter['lessOrEqual'].setComponents(attribute, value)
603 else:
604 raise LDAPFilterInvalidException("invalid filter '(%s%s%s)'" % (attribute, operator, value))
606 return searchFilter
609class LDAPFilterSyntaxError(SyntaxError):
610 pass
613class LDAPFilterInvalidException(Exception):
614 pass
617class LDAPSessionError(Exception):
618 """
619 This is the exception every client should catch
620 """
622 def __init__(self, error=0, packet=0, errorString=''):
623 Exception.__init__(self)
624 self.error = error
625 self.packet = packet
626 self.errorString = errorString
628 def getErrorCode(self):
629 return self.error
631 def getErrorPacket(self):
632 return self.packet
634 def getErrorString(self):
635 return self.errorString
637 def __str__(self):
638 return self.errorString
641class LDAPSearchError(LDAPSessionError):
642 def __init__(self, error=0, packet=0, errorString='', answers=None):
643 LDAPSessionError.__init__(self, error, packet, errorString)
644 if answers is None:
645 answers = []
646 self.answers = answers
648 def getAnswers(self):
649 return self.answers