1""" 2""" 3 4# Created on 2014.05.31 5# 6# Author: Giovanni Cannata 7# 8# Copyright 2014 - 2020 Giovanni Cannata 9# 10# This file is part of ldap3. 11# 12# ldap3 is free software: you can redistribute it and/or modify 13# it under the terms of the GNU Lesser General Public License as published 14# by the Free Software Foundation, either version 3 of the License, or 15# (at your option) any later version. 16# 17# ldap3 is distributed in the hope that it will be useful, 18# but WITHOUT ANY WARRANTY; without even the implied warranty of 19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20# GNU Lesser General Public License for more details. 21# 22# You should have received a copy of the GNU Lesser General Public License 23# along with ldap3 in the COPYING and COPYING.LESSER files. 24# If not, see <http://www.gnu.org/licenses/>. 25from copy import deepcopy, copy 26from os import linesep 27from threading import RLock, Lock 28from functools import reduce 29import json 30 31from .. import ANONYMOUS, SIMPLE, SASL, MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, get_config_parameter, DEREF_ALWAYS, \ 32 SUBTREE, ASYNC, SYNC, NO_ATTRIBUTES, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, MODIFY_INCREMENT, LDIF, ASYNC_STREAM, \ 33 RESTARTABLE, ROUND_ROBIN, REUSABLE, AUTO_BIND_DEFAULT, AUTO_BIND_NONE, AUTO_BIND_TLS_BEFORE_BIND, SAFE_SYNC, \ 34 AUTO_BIND_TLS_AFTER_BIND, AUTO_BIND_NO_TLS, STRING_TYPES, SEQUENCE_TYPES, MOCK_SYNC, MOCK_ASYNC, NTLM, EXTERNAL,\ 35 DIGEST_MD5, GSSAPI, PLAIN, DSA, SCHEMA, ALL 36 37from .results import RESULT_SUCCESS, RESULT_COMPARE_TRUE, RESULT_COMPARE_FALSE 38from ..extend import ExtendedOperationsRoot 39from .pooling import ServerPool 40from .server import Server 41from ..operation.abandon import abandon_operation, abandon_request_to_dict 42from ..operation.add import add_operation, add_request_to_dict 43from ..operation.bind import bind_operation, bind_request_to_dict 44from ..operation.compare import compare_operation, compare_request_to_dict 45from ..operation.delete import delete_operation, delete_request_to_dict 46from ..operation.extended import extended_operation, extended_request_to_dict 47from ..operation.modify import modify_operation, modify_request_to_dict 48from ..operation.modifyDn import modify_dn_operation, modify_dn_request_to_dict 49from ..operation.search import search_operation, search_request_to_dict 50from ..protocol.rfc2849 import operation_to_ldif, add_ldif_header 51from ..protocol.sasl.digestMd5 import sasl_digest_md5 52from ..protocol.sasl.external import sasl_external 53from ..protocol.sasl.plain import sasl_plain 54from ..strategy.sync import SyncStrategy 55from ..strategy.safeSync import SafeSyncStrategy 56from ..strategy.mockAsync import MockAsyncStrategy 57from ..strategy.asynchronous import AsyncStrategy 58from ..strategy.reusable import ReusableStrategy 59from ..strategy.restartable import RestartableStrategy 60from ..strategy.ldifProducer import LdifProducerStrategy 61from ..strategy.mockSync import MockSyncStrategy 62from ..strategy.asyncStream import AsyncStreamStrategy 63from ..operation.unbind import unbind_operation 64from ..protocol.rfc2696 import paged_search_control 65from .usage import ConnectionUsage 66from .tls import Tls 67from .exceptions import LDAPUnknownStrategyError, LDAPBindError, LDAPUnknownAuthenticationMethodError, \ 68 LDAPSASLMechanismNotSupportedError, LDAPObjectClassError, LDAPConnectionIsReadOnlyError, LDAPChangeError, LDAPExceptionError, \ 69 LDAPObjectError, LDAPSocketReceiveError, LDAPAttributeError, LDAPInvalidValueError, LDAPInvalidPortError, LDAPStartTLSError 70 71from ..utils.conv import escape_bytes, prepare_for_stream, check_json_dict, format_json, to_unicode 72from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED, get_library_log_hide_sensitive_data 73from ..utils.dn import safe_dn 74from ..utils.port_validators import check_port_and_port_list 75 76 77SASL_AVAILABLE_MECHANISMS = [EXTERNAL, 78 DIGEST_MD5, 79 GSSAPI, 80 PLAIN] 81 82CLIENT_STRATEGIES = [SYNC, 83 SAFE_SYNC, 84 ASYNC, 85 LDIF, 86 RESTARTABLE, 87 REUSABLE, 88 MOCK_SYNC, 89 MOCK_ASYNC, 90 ASYNC_STREAM] 91 92 93def _format_socket_endpoint(endpoint): 94 if endpoint and len(endpoint) == 2: # IPv4 95 return str(endpoint[0]) + ':' + str(endpoint[1]) 96 elif endpoint and len(endpoint) == 4: # IPv6 97 return '[' + str(endpoint[0]) + ']:' + str(endpoint[1]) 98 99 try: 100 return str(endpoint) 101 except Exception: 102 return '?' 103 104 105def _format_socket_endpoints(sock): 106 if sock: 107 try: 108 local = sock.getsockname() 109 except Exception: 110 local = (None, None, None, None) 111 try: 112 remote = sock.getpeername() 113 except Exception: 114 remote = (None, None, None, None) 115 116 return '<local: ' + _format_socket_endpoint(local) + ' - remote: ' + _format_socket_endpoint(remote) + '>' 117 return '<no socket>' 118 119 120class Connection(object): 121 """Main ldap connection class. 122 123 Controls, if used, must be a list of tuples. Each tuple must have 3 124 elements, the control OID, a boolean meaning if the control is 125 critical, a value. 126 127 If the boolean is set to True the server must honor the control or 128 refuse the operation 129 130 Mixing controls must be defined in controls specification (as per 131 RFC 4511) 132 133 :param server: the Server object to connect to 134 :type server: Server, str 135 :param user: the user name for simple authentication 136 :type user: str 137 :param password: the password for simple authentication 138 :type password: str 139 :param auto_bind: specify if the bind will be performed automatically when defining the Connection object 140 :type auto_bind: int, can be one of AUTO_BIND_DEFAULT, AUTO_BIND_NONE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND, AUTO_BIND_TLS_AFTER_BIND as specified in ldap3 141 :param version: LDAP version, default to 3 142 :type version: int 143 :param authentication: type of authentication 144 :type authentication: int, can be one of AUTH_ANONYMOUS, AUTH_SIMPLE or AUTH_SASL, as specified in ldap3 145 :param client_strategy: communication strategy used in the Connection 146 :type client_strategy: can be one of STRATEGY_SYNC, STRATEGY_ASYNC_THREADED, STRATEGY_LDIF_PRODUCER, STRATEGY_SYNC_RESTARTABLE, STRATEGY_REUSABLE_THREADED as specified in ldap3 147 :param auto_referrals: specify if the connection object must automatically follow referrals 148 :type auto_referrals: bool 149 :param sasl_mechanism: mechanism for SASL authentication, can be one of 'EXTERNAL', 'DIGEST-MD5', 'GSSAPI', 'PLAIN' 150 :type sasl_mechanism: str 151 :param sasl_credentials: credentials for SASL mechanism 152 :type sasl_credentials: tuple 153 :param check_names: if True the library will check names of attributes and object classes against the schema. Also values found in entries will be formatted as indicated by the schema 154 :type check_names: bool 155 :param collect_usage: collect usage metrics in the usage attribute 156 :type collect_usage: bool 157 :param read_only: disable operations that modify data in the LDAP server 158 :type read_only: bool 159 :param lazy: open and bind the connection only when an actual operation is performed 160 :type lazy: bool 161 :param raise_exceptions: raise exceptions when operations are not successful, if False operations return False if not successful but not raise exceptions 162 :type raise_exceptions: bool 163 :param pool_name: pool name for pooled strategies 164 :type pool_name: str 165 :param pool_size: pool size for pooled strategies 166 :type pool_size: int 167 :param pool_lifetime: pool lifetime for pooled strategies 168 :type pool_lifetime: int 169 :param cred_store: credential store for gssapi 170 :type cred_store: dict 171 :param use_referral_cache: keep referral connections open and reuse them 172 :type use_referral_cache: bool 173 :param auto_escape: automatic escaping of filter values 174 :type auto_escape: bool 175 :param auto_encode: automatic encoding of attribute values 176 :type auto_encode: bool 177 :param source_address: the ip address or hostname to use as the source when opening the connection to the server 178 :type source_address: str 179 :param source_port: the source port to use when opening the connection to the server. Cannot be specified with source_port_list 180 :type source_port: int 181 :param source_port_list: a list of source ports to choose from when opening the connection to the server. Cannot be specified with source_port 182 :type source_port_list: list 183 """ 184 def __init__(self, 185 server, 186 user=None, 187 password=None, 188 auto_bind=AUTO_BIND_DEFAULT, 189 version=3, 190 authentication=None, 191 client_strategy=SYNC, 192 auto_referrals=True, 193 auto_range=True, 194 sasl_mechanism=None, 195 sasl_credentials=None, 196 check_names=True, 197 collect_usage=False, 198 read_only=False, 199 lazy=False, 200 raise_exceptions=False, 201 pool_name=None, 202 pool_size=None, 203 pool_lifetime=None, 204 cred_store=None, 205 fast_decoder=True, 206 receive_timeout=None, 207 return_empty_attributes=True, 208 use_referral_cache=False, 209 auto_escape=True, 210 auto_encode=True, 211 pool_keepalive=None, 212 source_address=None, 213 source_port=None, 214 source_port_list=None): 215 216 conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME') 217 self.connection_lock = RLock() # re-entrant lock to ensure that operations in the Connection object are executed atomically in the same thread 218 with self.connection_lock: 219 if client_strategy not in CLIENT_STRATEGIES: 220 self.last_error = 'unknown client connection strategy' 221 if log_enabled(ERROR): 222 log(ERROR, '%s for <%s>', self.last_error, self) 223 raise LDAPUnknownStrategyError(self.last_error) 224 225 self.strategy_type = client_strategy 226 self.user = user 227 self.password = password 228 229 if not authentication and self.user: 230 self.authentication = SIMPLE 231 elif not authentication: 232 self.authentication = ANONYMOUS 233 elif authentication in [SIMPLE, ANONYMOUS, SASL, NTLM]: 234 self.authentication = authentication 235 else: 236 self.last_error = 'unknown authentication method' 237 if log_enabled(ERROR): 238 log(ERROR, '%s for <%s>', self.last_error, self) 239 raise LDAPUnknownAuthenticationMethodError(self.last_error) 240 241 self.version = version 242 self.auto_referrals = True if auto_referrals else False 243 self.request = None 244 self.response = None 245 self.result = None 246 self.bound = False 247 self.listening = False 248 self.closed = True 249 self.last_error = None 250 if auto_bind is False: # compatibility with older version where auto_bind was a boolean 251 self.auto_bind = AUTO_BIND_DEFAULT 252 elif auto_bind is True: 253 self.auto_bind = AUTO_BIND_NO_TLS 254 else: 255 self.auto_bind = auto_bind 256 self.sasl_mechanism = sasl_mechanism 257 self.sasl_credentials = sasl_credentials 258 self._usage = ConnectionUsage() if collect_usage else None 259 self.socket = None 260 self.tls_started = False 261 self.sasl_in_progress = False 262 self.read_only = read_only 263 self._context_state = [] 264 self._deferred_open = False 265 self._deferred_bind = False 266 self._deferred_start_tls = False 267 self._bind_controls = None 268 self._executing_deferred = False 269 self.lazy = lazy 270 self.pool_name = pool_name if pool_name else conf_default_pool_name 271 self.pool_size = pool_size 272 self.cred_store = cred_store 273 self.pool_lifetime = pool_lifetime 274 self.pool_keepalive = pool_keepalive 275 self.starting_tls = False 276 self.check_names = check_names 277 self.raise_exceptions = raise_exceptions 278 self.auto_range = True if auto_range else False 279 self.extend = ExtendedOperationsRoot(self) 280 self._entries = [] 281 self.fast_decoder = fast_decoder 282 self.receive_timeout = receive_timeout 283 self.empty_attributes = return_empty_attributes 284 self.use_referral_cache = use_referral_cache 285 self.auto_escape = auto_escape 286 self.auto_encode = auto_encode 287 288 port_err = check_port_and_port_list(source_port, source_port_list) 289 if port_err: 290 if log_enabled(ERROR): 291 log(ERROR, port_err) 292 raise LDAPInvalidPortError(port_err) 293 # using an empty string to bind a socket means "use the default as if this wasn't provided" because socket 294 # binding requires that you pass something for the ip if you want to pass a specific port 295 self.source_address = source_address if source_address is not None else '' 296 # using 0 as the source port to bind a socket means "use the default behavior of picking a random port from 297 # all ports as if this wasn't provided" because socket binding requires that you pass something for the port 298 # if you want to pass a specific ip 299 self.source_port_list = [0] 300 if source_port is not None: 301 self.source_port_list = [source_port] 302 elif source_port_list is not None: 303 self.source_port_list = source_port_list[:] 304 305 if isinstance(server, STRING_TYPES): 306 server = Server(server) 307 if isinstance(server, SEQUENCE_TYPES): 308 server = ServerPool(server, ROUND_ROBIN, active=True, exhaust=True) 309 310 if isinstance(server, ServerPool): 311 self.server_pool = server 312 self.server_pool.initialize(self) 313 self.server = self.server_pool.get_current_server(self) 314 else: 315 self.server_pool = None 316 self.server = server 317 318 # if self.authentication == SIMPLE and self.user and self.check_names: 319 # self.user = safe_dn(self.user) 320 # if log_enabled(EXTENDED): 321 # log(EXTENDED, 'user name sanitized to <%s> for simple authentication via <%s>', self.user, self) 322 323 if self.strategy_type == SYNC: 324 self.strategy = SyncStrategy(self) 325 elif self.strategy_type == SAFE_SYNC: 326 self.strategy = SafeSyncStrategy(self) 327 elif self.strategy_type == ASYNC: 328 self.strategy = AsyncStrategy(self) 329 elif self.strategy_type == LDIF: 330 self.strategy = LdifProducerStrategy(self) 331 elif self.strategy_type == RESTARTABLE: 332 self.strategy = RestartableStrategy(self) 333 elif self.strategy_type == REUSABLE: 334 self.strategy = ReusableStrategy(self) 335 self.lazy = False 336 elif self.strategy_type == MOCK_SYNC: 337 self.strategy = MockSyncStrategy(self) 338 elif self.strategy_type == MOCK_ASYNC: 339 self.strategy = MockAsyncStrategy(self) 340 elif self.strategy_type == ASYNC_STREAM: 341 self.strategy = AsyncStreamStrategy(self) 342 else: 343 self.last_error = 'unknown strategy' 344 if log_enabled(ERROR): 345 log(ERROR, '%s for <%s>', self.last_error, self) 346 raise LDAPUnknownStrategyError(self.last_error) 347 348 # maps strategy functions to connection functions 349 self.send = self.strategy.send 350 self.open = self.strategy.open 351 self.get_response = self.strategy.get_response 352 self.post_send_single_response = self.strategy.post_send_single_response 353 self.post_send_search = self.strategy.post_send_search 354 355 if not self.strategy.no_real_dsa: 356 self._do_auto_bind() 357 # else: # for strategies with a fake server set get_info to NONE if server hasn't a schema 358 # if self.server and not self.server.schema: 359 # self.server.get_info = NONE 360 if log_enabled(BASIC): 361 if get_library_log_hide_sensitive_data(): 362 log(BASIC, 'instantiated Connection: <%s>', self.repr_with_sensitive_data_stripped()) 363 else: 364 log(BASIC, 'instantiated Connection: <%r>', self) 365 366 def _prepare_return_value(self, status, response=False): 367 if self.strategy.thread_safe: 368 temp_response = self.response 369 self.response = None 370 temp_request = self.request 371 self.request = None 372 return status, deepcopy(self.result), deepcopy(temp_response) if response else None, copy(temp_request) 373 return status 374 375 def _do_auto_bind(self): 376 if self.auto_bind and self.auto_bind not in [AUTO_BIND_NONE, AUTO_BIND_DEFAULT]: 377 if log_enabled(BASIC): 378 log(BASIC, 'performing automatic bind for <%s>', self) 379 if self.closed: 380 self.open(read_server_info=False) 381 if self.auto_bind == AUTO_BIND_NO_TLS: 382 self.bind(read_server_info=True) 383 elif self.auto_bind == AUTO_BIND_TLS_BEFORE_BIND: 384 if self.start_tls(read_server_info=False): 385 self.bind(read_server_info=True) 386 else: 387 error = 'automatic start_tls befored bind not successful' + (' - ' + self.last_error if self.last_error else '') 388 if log_enabled(ERROR): 389 log(ERROR, '%s for <%s>', error, self) 390 self.unbind() # unbind anyway to close connection 391 raise LDAPStartTLSError(error) 392 elif self.auto_bind == AUTO_BIND_TLS_AFTER_BIND: 393 self.bind(read_server_info=False) 394 if not self.start_tls(read_server_info=True): 395 error = 'automatic start_tls after bind not successful' + (' - ' + self.last_error if self.last_error else '') 396 if log_enabled(ERROR): 397 log(ERROR, '%s for <%s>', error, self) 398 self.unbind() 399 raise LDAPStartTLSError(error) 400 if not self.bound: 401 error = 'automatic bind not successful' + (' - ' + self.last_error if self.last_error else '') 402 if log_enabled(ERROR): 403 log(ERROR, '%s for <%s>', error, self) 404 self.unbind() 405 raise LDAPBindError(error) 406 407 def __str__(self): 408 s = [ 409 str(self.server) if self.server else 'None', 410 'user: ' + str(self.user), 411 'lazy' if self.lazy else 'not lazy', 412 'unbound' if not self.bound else ('deferred bind' if self._deferred_bind else 'bound'), 413 'closed' if self.closed else ('deferred open' if self._deferred_open else 'open'), 414 _format_socket_endpoints(self.socket), 415 'tls not started' if not self.tls_started else('deferred start_tls' if self._deferred_start_tls else 'tls started'), 416 'listening' if self.listening else 'not listening', 417 self.strategy.__class__.__name__ if hasattr(self, 'strategy') else 'No strategy', 418 'internal decoder' if self.fast_decoder else 'pyasn1 decoder' 419 ] 420 return ' - '.join(s) 421 422 def __repr__(self): 423 conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME') 424 if self.server_pool: 425 r = 'Connection(server={0.server_pool!r}'.format(self) 426 else: 427 r = 'Connection(server={0.server!r}'.format(self) 428 r += '' if self.user is None else ', user={0.user!r}'.format(self) 429 r += '' if self.password is None else ', password={0.password!r}'.format(self) 430 r += '' if self.auto_bind is None else ', auto_bind={0.auto_bind!r}'.format(self) 431 r += '' if self.version is None else ', version={0.version!r}'.format(self) 432 r += '' if self.authentication is None else ', authentication={0.authentication!r}'.format(self) 433 r += '' if self.strategy_type is None else ', client_strategy={0.strategy_type!r}'.format(self) 434 r += '' if self.auto_referrals is None else ', auto_referrals={0.auto_referrals!r}'.format(self) 435 r += '' if self.sasl_mechanism is None else ', sasl_mechanism={0.sasl_mechanism!r}'.format(self) 436 r += '' if self.sasl_credentials is None else ', sasl_credentials={0.sasl_credentials!r}'.format(self) 437 r += '' if self.check_names is None else ', check_names={0.check_names!r}'.format(self) 438 r += '' if self.usage is None else (', collect_usage=' + ('True' if self.usage else 'False')) 439 r += '' if self.read_only is None else ', read_only={0.read_only!r}'.format(self) 440 r += '' if self.lazy is None else ', lazy={0.lazy!r}'.format(self) 441 r += '' if self.raise_exceptions is None else ', raise_exceptions={0.raise_exceptions!r}'.format(self) 442 r += '' if (self.pool_name is None or self.pool_name == conf_default_pool_name) else ', pool_name={0.pool_name!r}'.format(self) 443 r += '' if self.pool_size is None else ', pool_size={0.pool_size!r}'.format(self) 444 r += '' if self.pool_lifetime is None else ', pool_lifetime={0.pool_lifetime!r}'.format(self) 445 r += '' if self.pool_keepalive is None else ', pool_keepalive={0.pool_keepalive!r}'.format(self) 446 r += '' if self.cred_store is None else (', cred_store=' + repr(self.cred_store)) 447 r += '' if self.fast_decoder is None else (', fast_decoder=' + ('True' if self.fast_decoder else 'False')) 448 r += '' if self.auto_range is None else (', auto_range=' + ('True' if self.auto_range else 'False')) 449 r += '' if self.receive_timeout is None else ', receive_timeout={0.receive_timeout!r}'.format(self) 450 r += '' if self.empty_attributes is None else (', return_empty_attributes=' + ('True' if self.empty_attributes else 'False')) 451 r += '' if self.auto_encode is None else (', auto_encode=' + ('True' if self.auto_encode else 'False')) 452 r += '' if self.auto_escape is None else (', auto_escape=' + ('True' if self.auto_escape else 'False')) 453 r += '' if self.use_referral_cache is None else (', use_referral_cache=' + ('True' if self.use_referral_cache else 'False')) 454 r += ')' 455 456 return r 457 458 def repr_with_sensitive_data_stripped(self): 459 conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME') 460 if self.server_pool: 461 r = 'Connection(server={0.server_pool!r}'.format(self) 462 else: 463 r = 'Connection(server={0.server!r}'.format(self) 464 r += '' if self.user is None else ', user={0.user!r}'.format(self) 465 r += '' if self.password is None else ", password='{0}'".format('<stripped %d characters of sensitive data>' % len(self.password)) 466 r += '' if self.auto_bind is None else ', auto_bind={0.auto_bind!r}'.format(self) 467 r += '' if self.version is None else ', version={0.version!r}'.format(self) 468 r += '' if self.authentication is None else ', authentication={0.authentication!r}'.format(self) 469 r += '' if self.strategy_type is None else ', client_strategy={0.strategy_type!r}'.format(self) 470 r += '' if self.auto_referrals is None else ', auto_referrals={0.auto_referrals!r}'.format(self) 471 r += '' if self.sasl_mechanism is None else ', sasl_mechanism={0.sasl_mechanism!r}'.format(self) 472 if self.sasl_mechanism == DIGEST_MD5: 473 r += '' if self.sasl_credentials is None else ", sasl_credentials=({0!r}, {1!r}, '{2}', {3!r})".format(self.sasl_credentials[0], self.sasl_credentials[1], '*' * len(self.sasl_credentials[2]), self.sasl_credentials[3]) 474 else: 475 r += '' if self.sasl_credentials is None else ', sasl_credentials={0.sasl_credentials!r}'.format(self) 476 r += '' if self.check_names is None else ', check_names={0.check_names!r}'.format(self) 477 r += '' if self.usage is None else (', collect_usage=' + 'True' if self.usage else 'False') 478 r += '' if self.read_only is None else ', read_only={0.read_only!r}'.format(self) 479 r += '' if self.lazy is None else ', lazy={0.lazy!r}'.format(self) 480 r += '' if self.raise_exceptions is None else ', raise_exceptions={0.raise_exceptions!r}'.format(self) 481 r += '' if (self.pool_name is None or self.pool_name == conf_default_pool_name) else ', pool_name={0.pool_name!r}'.format(self) 482 r += '' if self.pool_size is None else ', pool_size={0.pool_size!r}'.format(self) 483 r += '' if self.pool_lifetime is None else ', pool_lifetime={0.pool_lifetime!r}'.format(self) 484 r += '' if self.pool_keepalive is None else ', pool_keepalive={0.pool_keepalive!r}'.format(self) 485 r += '' if self.cred_store is None else (', cred_store=' + repr(self.cred_store)) 486 r += '' if self.fast_decoder is None else (', fast_decoder=' + 'True' if self.fast_decoder else 'False') 487 r += '' if self.auto_range is None else (', auto_range=' + ('True' if self.auto_range else 'False')) 488 r += '' if self.receive_timeout is None else ', receive_timeout={0.receive_timeout!r}'.format(self) 489 r += '' if self.empty_attributes is None else (', return_empty_attributes=' + 'True' if self.empty_attributes else 'False') 490 r += '' if self.auto_encode is None else (', auto_encode=' + ('True' if self.auto_encode else 'False')) 491 r += '' if self.auto_escape is None else (', auto_escape=' + ('True' if self.auto_escape else 'False')) 492 r += '' if self.use_referral_cache is None else (', use_referral_cache=' + ('True' if self.use_referral_cache else 'False')) 493 r += ')' 494 495 return r 496 497 @property 498 def stream(self): 499 """Used by the LDIFProducer strategy to accumulate the ldif-change operations with a single LDIF header 500 :return: reference to the response stream if defined in the strategy. 501 """ 502 return self.strategy.get_stream() if self.strategy.can_stream else None 503 504 @stream.setter 505 def stream(self, value): 506 with self.connection_lock: 507 if self.strategy.can_stream: 508 self.strategy.set_stream(value) 509 510 @property 511 def usage(self): 512 """Usage statistics for the connection. 513 :return: Usage object 514 """ 515 if not self._usage: 516 return None 517 if self.strategy.pooled: # update master connection usage from pooled connections 518 self._usage.reset() 519 for worker in self.strategy.pool.workers: 520 self._usage += worker.connection.usage 521 self._usage += self.strategy.pool.terminated_usage 522 return self._usage 523 524 def __enter__(self): 525 with self.connection_lock: 526 self._context_state.append((self.bound, self.closed)) # save status out of context as a tuple in a list 527 if self.auto_bind != AUTO_BIND_NONE: 528 if self.auto_bind == AUTO_BIND_DEFAULT: 529 self.auto_bind = AUTO_BIND_NO_TLS 530 if self.closed: 531 self.open() 532 if not self.bound: 533 if not self.bind(): 534 raise LDAPBindError('unable to bind') 535 536 return self 537 538 def __exit__(self, exc_type, exc_val, exc_tb): 539 with self.connection_lock: 540 context_bound, context_closed = self._context_state.pop() 541 if (not context_bound and self.bound) or self.stream: # restore status prior to entering context 542 try: 543 self.unbind() 544 except LDAPExceptionError: 545 pass 546 547 if not context_closed and self.closed: 548 self.open() 549 550 if exc_type is not None: 551 if log_enabled(ERROR): 552 log(ERROR, '%s for <%s>', exc_type, self) 553 return False # re-raise LDAPExceptionError 554 555 def bind(self, 556 read_server_info=True, 557 controls=None): 558 """Bind to ldap Server with the authentication method and the user defined in the connection 559 560 :param read_server_info: reads info from server 561 :param controls: LDAP controls to send along with the bind operation 562 :type controls: list of tuple 563 :return: bool 564 565 """ 566 if log_enabled(BASIC): 567 log(BASIC, 'start BIND operation via <%s>', self) 568 self.last_error = None 569 with self.connection_lock: 570 if self.lazy and not self._executing_deferred: 571 if self.strategy.pooled: 572 self.strategy.validate_bind(controls) 573 self._deferred_bind = True 574 self._bind_controls = controls 575 self.bound = True 576 if log_enabled(BASIC): 577 log(BASIC, 'deferring bind for <%s>', self) 578 else: 579 self._deferred_bind = False 580 self._bind_controls = None 581 if self.closed: # try to open connection if closed 582 self.open(read_server_info=False) 583 if self.authentication == ANONYMOUS: 584 if log_enabled(PROTOCOL): 585 log(PROTOCOL, 'performing anonymous BIND for <%s>', self) 586 if not self.strategy.pooled: 587 request = bind_operation(self.version, self.authentication, self.user, '', auto_encode=self.auto_encode) 588 if log_enabled(PROTOCOL): 589 log(PROTOCOL, 'anonymous BIND request <%s> sent via <%s>', bind_request_to_dict(request), self) 590 response = self.post_send_single_response(self.send('bindRequest', request, controls)) 591 else: 592 response = self.strategy.validate_bind(controls) # only for REUSABLE 593 elif self.authentication == SIMPLE: 594 if log_enabled(PROTOCOL): 595 log(PROTOCOL, 'performing simple BIND for <%s>', self) 596 if not self.strategy.pooled: 597 request = bind_operation(self.version, self.authentication, self.user, self.password, auto_encode=self.auto_encode) 598 if log_enabled(PROTOCOL): 599 log(PROTOCOL, 'simple BIND request <%s> sent via <%s>', bind_request_to_dict(request), self) 600 response = self.post_send_single_response(self.send('bindRequest', request, controls)) 601 else: 602 response = self.strategy.validate_bind(controls) # only for REUSABLE 603 elif self.authentication == SASL: 604 if self.sasl_mechanism in SASL_AVAILABLE_MECHANISMS: 605 if log_enabled(PROTOCOL): 606 log(PROTOCOL, 'performing SASL BIND for <%s>', self) 607 if not self.strategy.pooled: 608 response = self.do_sasl_bind(controls) 609 else: 610 response = self.strategy.validate_bind(controls) # only for REUSABLE 611 else: 612 self.last_error = 'requested SASL mechanism not supported' 613 if log_enabled(ERROR): 614 log(ERROR, '%s for <%s>', self.last_error, self) 615 raise LDAPSASLMechanismNotSupportedError(self.last_error) 616 elif self.authentication == NTLM: 617 if self.user and self.password and len(self.user.split('\\')) == 2: 618 if log_enabled(PROTOCOL): 619 log(PROTOCOL, 'performing NTLM BIND for <%s>', self) 620 if not self.strategy.pooled: 621 response = self.do_ntlm_bind(controls) 622 else: 623 response = self.strategy.validate_bind(controls) # only for REUSABLE 624 else: # user or password missing 625 self.last_error = 'NTLM needs domain\\username and a password' 626 if log_enabled(ERROR): 627 log(ERROR, '%s for <%s>', self.last_error, self) 628 raise LDAPUnknownAuthenticationMethodError(self.last_error) 629 else: 630 self.last_error = 'unknown authentication method' 631 if log_enabled(ERROR): 632 log(ERROR, '%s for <%s>', self.last_error, self) 633 raise LDAPUnknownAuthenticationMethodError(self.last_error) 634 635 if not self.strategy.sync and not self.strategy.pooled and self.authentication not in (SASL, NTLM): # get response if asynchronous except for SASL and NTLM that return the bind result even for asynchronous strategy 636 _, result = self.get_response(response) 637 if log_enabled(PROTOCOL): 638 log(PROTOCOL, 'async BIND response id <%s> received via <%s>', result, self) 639 elif self.strategy.sync: 640 result = self.result 641 if log_enabled(PROTOCOL): 642 log(PROTOCOL, 'BIND response <%s> received via <%s>', result, self) 643 elif self.strategy.pooled or self.authentication in (SASL, NTLM): # asynchronous SASL and NTLM or reusable strtegy get the bind result synchronously 644 result = response 645 else: 646 self.last_error = 'unknown authentication method' 647 if log_enabled(ERROR): 648 log(ERROR, '%s for <%s>', self.last_error, self) 649 raise LDAPUnknownAuthenticationMethodError(self.last_error) 650 651 if result is None: 652 # self.bound = True if self.strategy_type == REUSABLE else False 653 self.bound = False 654 elif result is True: 655 self.bound = True 656 elif result is False: 657 self.bound = False 658 else: 659 self.bound = True if result['result'] == RESULT_SUCCESS else False 660 if not self.bound and result and result['description'] and not self.last_error: 661 self.last_error = result['description'] 662 663 if read_server_info and self.bound: 664 self.refresh_server_info() 665 self._entries = [] 666 667 if log_enabled(BASIC): 668 log(BASIC, 'done BIND operation, result <%s>', self.bound) 669 670 return self._prepare_return_value(self.bound, self.result) 671 672 def rebind(self, 673 user=None, 674 password=None, 675 authentication=None, 676 sasl_mechanism=None, 677 sasl_credentials=None, 678 read_server_info=True, 679 controls=None 680 ): 681 682 if log_enabled(BASIC): 683 log(BASIC, 'start (RE)BIND operation via <%s>', self) 684 self.last_error = None 685 with self.connection_lock: 686 if user: 687 self.user = user 688 if password is not None: 689 self.password = password 690 if not authentication and user: 691 self.authentication = SIMPLE 692 if authentication in [SIMPLE, ANONYMOUS, SASL, NTLM]: 693 self.authentication = authentication 694 elif authentication is not None: 695 self.last_error = 'unknown authentication method' 696 if log_enabled(ERROR): 697 log(ERROR, '%s for <%s>', self.last_error, self) 698 raise LDAPUnknownAuthenticationMethodError(self.last_error) 699 if sasl_mechanism: 700 self.sasl_mechanism = sasl_mechanism 701 if sasl_credentials: 702 self.sasl_credentials = sasl_credentials 703 704 # if self.authentication == SIMPLE and self.user and self.check_names: 705 # self.user = safe_dn(self.user) 706 # if log_enabled(EXTENDED): 707 # log(EXTENDED, 'user name sanitized to <%s> for rebind via <%s>', self.user, self) 708 709 if not self.strategy.pooled: 710 try: 711 return self.bind(read_server_info, controls) 712 except LDAPSocketReceiveError: 713 self.last_error = 'Unable to rebind as a different user, furthermore the server abruptly closed the connection' 714 if log_enabled(ERROR): 715 log(ERROR, '%s for <%s>', self.last_error, self) 716 raise LDAPBindError(self.last_error) 717 else: 718 self.strategy.pool.rebind_pool() 719 return self._prepare_return_value(True, self.result) 720 721 def unbind(self, 722 controls=None): 723 """Unbind the connected user. Unbind implies closing session as per RFC4511 (4.3) 724 725 :param controls: LDAP controls to send along with the bind operation 726 727 """ 728 if log_enabled(BASIC): 729 log(BASIC, 'start UNBIND operation via <%s>', self) 730 731 if self.use_referral_cache: 732 self.strategy.unbind_referral_cache() 733 734 self.last_error = None 735 with self.connection_lock: 736 if self.lazy and not self._executing_deferred and (self._deferred_bind or self._deferred_open): # _clear deferred status 737 self.strategy.close() 738 self._deferred_open = False 739 self._deferred_bind = False 740 self._deferred_start_tls = False 741 elif not self.closed: 742 request = unbind_operation() 743 if log_enabled(PROTOCOL): 744 log(PROTOCOL, 'UNBIND request sent via <%s>', self) 745 self.send('unbindRequest', request, controls) 746 self.strategy.close() 747 748 if log_enabled(BASIC): 749 log(BASIC, 'done UNBIND operation, result <%s>', True) 750 751 return self._prepare_return_value(True) 752 753 def search(self, 754 search_base, 755 search_filter, 756 search_scope=SUBTREE, 757 dereference_aliases=DEREF_ALWAYS, 758 attributes=None, 759 size_limit=0, 760 time_limit=0, 761 types_only=False, 762 get_operational_attributes=False, 763 controls=None, 764 paged_size=None, 765 paged_criticality=False, 766 paged_cookie=None, 767 auto_escape=None): 768 """ 769 Perform an ldap search: 770 771 - If attributes is empty noRFC2696 with the specified size 772 - If paged is 0 and cookie is present the search is abandoned on 773 server attribute is returned 774 - If attributes is ALL_ATTRIBUTES all attributes are returned 775 - If paged_size is an int greater than 0 a simple paged search 776 is tried as described in 777 - Cookie is an opaque string received in the last paged search 778 and must be used on the next paged search response 779 - If lazy == True open and bind will be deferred until another 780 LDAP operation is performed 781 - If mssing_attributes == True then an attribute not returned by the server is set to None 782 - If auto_escape is set it overrides the Connection auto_escape 783 """ 784 conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')] 785 if log_enabled(BASIC): 786 log(BASIC, 'start SEARCH operation via <%s>', self) 787 788 if self.check_names and search_base: 789 search_base = safe_dn(search_base) 790 if log_enabled(EXTENDED): 791 log(EXTENDED, 'search base sanitized to <%s> for SEARCH operation via <%s>', search_base, self) 792 793 with self.connection_lock: 794 self._fire_deferred() 795 if not attributes: 796 attributes = [NO_ATTRIBUTES] 797 elif attributes == ALL_ATTRIBUTES: 798 attributes = [ALL_ATTRIBUTES] 799 800 if isinstance(attributes, STRING_TYPES): 801 attributes = [attributes] 802 803 if get_operational_attributes and isinstance(attributes, list): 804 attributes.append(ALL_OPERATIONAL_ATTRIBUTES) 805 elif get_operational_attributes and isinstance(attributes, tuple): 806 attributes += (ALL_OPERATIONAL_ATTRIBUTES, ) # concatenate tuple 807 808 if isinstance(paged_size, int): 809 if log_enabled(PROTOCOL): 810 log(PROTOCOL, 'performing paged search for %d items with cookie <%s> for <%s>', paged_size, escape_bytes(paged_cookie), self) 811 812 if controls is None: 813 controls = [] 814 else: 815 # Copy the controls to prevent modifying the original object 816 controls = list(controls) 817 controls.append(paged_search_control(paged_criticality, paged_size, paged_cookie)) 818 819 if self.server and self.server.schema and self.check_names: 820 for attribute_name in attributes: 821 if ';' in attribute_name: # remove tags 822 attribute_name_to_check = attribute_name.split(';')[0] 823 else: 824 attribute_name_to_check = attribute_name 825 if self.server.schema and attribute_name_to_check.lower() not in conf_attributes_excluded_from_check and attribute_name_to_check not in self.server.schema.attribute_types: 826 self.last_error = 'invalid attribute type ' + attribute_name_to_check 827 if log_enabled(ERROR): 828 log(ERROR, '%s for <%s>', self.last_error, self) 829 raise LDAPAttributeError(self.last_error) 830 831 request = search_operation(search_base, 832 search_filter, 833 search_scope, 834 dereference_aliases, 835 attributes, 836 size_limit, 837 time_limit, 838 types_only, 839 self.auto_escape if auto_escape is None else auto_escape, 840 self.auto_encode, 841 self.server.schema if self.server else None, 842 validator=self.server.custom_validator, 843 check_names=self.check_names) 844 if log_enabled(PROTOCOL): 845 log(PROTOCOL, 'SEARCH request <%s> sent via <%s>', search_request_to_dict(request), self) 846 response = self.post_send_search(self.send('searchRequest', request, controls)) 847 self._entries = [] 848 849 if isinstance(response, int): # asynchronous strategy 850 return_value = response 851 if log_enabled(PROTOCOL): 852 log(PROTOCOL, 'async SEARCH response id <%s> received via <%s>', return_value, self) 853 else: 854 return_value = True if self.result['type'] == 'searchResDone' and len(response) > 0 else False 855 if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error: 856 self.last_error = self.result['description'] 857 858 if log_enabled(PROTOCOL): 859 for entry in response: 860 if entry['type'] == 'searchResEntry': 861 log(PROTOCOL, 'SEARCH response entry <%s> received via <%s>', entry, self) 862 elif entry['type'] == 'searchResRef': 863 log(PROTOCOL, 'SEARCH response reference <%s> received via <%s>', entry, self) 864 865 if log_enabled(BASIC): 866 log(BASIC, 'done SEARCH operation, result <%s>', return_value) 867 868 return self._prepare_return_value(return_value, response=True) 869 870 def compare(self, 871 dn, 872 attribute, 873 value, 874 controls=None): 875 """ 876 Perform a compare operation 877 """ 878 conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')] 879 880 if log_enabled(BASIC): 881 log(BASIC, 'start COMPARE operation via <%s>', self) 882 self.last_error = None 883 if self.check_names: 884 dn = safe_dn(dn) 885 if log_enabled(EXTENDED): 886 log(EXTENDED, 'dn sanitized to <%s> for COMPARE operation via <%s>', dn, self) 887 888 if self.server and self.server.schema and self.check_names: 889 if ';' in attribute: # remove tags for checking 890 attribute_name_to_check = attribute.split(';')[0] 891 else: 892 attribute_name_to_check = attribute 893 894 if self.server.schema.attribute_types and attribute_name_to_check.lower() not in conf_attributes_excluded_from_check and attribute_name_to_check not in self.server.schema.attribute_types: 895 self.last_error = 'invalid attribute type ' + attribute_name_to_check 896 if log_enabled(ERROR): 897 log(ERROR, '%s for <%s>', self.last_error, self) 898 raise LDAPAttributeError(self.last_error) 899 900 if isinstance(value, SEQUENCE_TYPES): # value can't be a sequence 901 self.last_error = 'value cannot be a sequence' 902 if log_enabled(ERROR): 903 log(ERROR, '%s for <%s>', self.last_error, self) 904 raise LDAPInvalidValueError(self.last_error) 905 906 with self.connection_lock: 907 self._fire_deferred() 908 request = compare_operation(dn, attribute, value, self.auto_encode, self.server.schema if self.server else None, validator=self.server.custom_validator if self.server else None, check_names=self.check_names) 909 if log_enabled(PROTOCOL): 910 log(PROTOCOL, 'COMPARE request <%s> sent via <%s>', compare_request_to_dict(request), self) 911 response = self.post_send_single_response(self.send('compareRequest', request, controls)) 912 self._entries = [] 913 if isinstance(response, int): 914 return_value = response 915 if log_enabled(PROTOCOL): 916 log(PROTOCOL, 'async COMPARE response id <%s> received via <%s>', return_value, self) 917 else: 918 return_value = True if self.result['type'] == 'compareResponse' and self.result['result'] == RESULT_COMPARE_TRUE else False 919 if not return_value and self.result['result'] not in [RESULT_COMPARE_TRUE, RESULT_COMPARE_FALSE] and not self.last_error: 920 self.last_error = self.result['description'] 921 922 if log_enabled(PROTOCOL): 923 log(PROTOCOL, 'COMPARE response <%s> received via <%s>', response, self) 924 925 if log_enabled(BASIC): 926 log(BASIC, 'done COMPARE operation, result <%s>', return_value) 927 928 return self._prepare_return_value(return_value) 929 930 def add(self, 931 dn, 932 object_class=None, 933 attributes=None, 934 controls=None): 935 """ 936 Add dn to the DIT, object_class is None, a class name or a list 937 of class names. 938 939 Attributes is a dictionary in the form 'attr': 'val' or 'attr': 940 ['val1', 'val2', ...] for multivalued attributes 941 """ 942 conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')] 943 conf_classes_excluded_from_check = [v.lower() for v in get_config_parameter('CLASSES_EXCLUDED_FROM_CHECK')] 944 if log_enabled(BASIC): 945 log(BASIC, 'start ADD operation via <%s>', self) 946 self.last_error = None 947 _attributes = deepcopy(attributes) # dict could change when adding objectClass values 948 if self.check_names: 949 dn = safe_dn(dn) 950 if log_enabled(EXTENDED): 951 log(EXTENDED, 'dn sanitized to <%s> for ADD operation via <%s>', dn, self) 952 953 with self.connection_lock: 954 self._fire_deferred() 955 attr_object_class = [] 956 if object_class is None: 957 parm_object_class = [] 958 else: 959 parm_object_class = list(object_class) if isinstance(object_class, SEQUENCE_TYPES) else [object_class] 960 961 object_class_attr_name = '' 962 if _attributes: 963 for attr in _attributes: 964 if attr.lower() == 'objectclass': 965 object_class_attr_name = attr 966 attr_object_class = list(_attributes[object_class_attr_name]) if isinstance(_attributes[object_class_attr_name], SEQUENCE_TYPES) else [_attributes[object_class_attr_name]] 967 break 968 else: 969 _attributes = dict() 970 971 if not object_class_attr_name: 972 object_class_attr_name = 'objectClass' 973 974 attr_object_class = [to_unicode(object_class) for object_class in attr_object_class] # converts objectclass to unicode in case of bytes value 975 _attributes[object_class_attr_name] = reduce(lambda x, y: x + [y] if y not in x else x, parm_object_class + attr_object_class, []) # remove duplicate ObjectClasses 976 977 if not _attributes[object_class_attr_name]: 978 self.last_error = 'objectClass attribute is mandatory' 979 if log_enabled(ERROR): 980 log(ERROR, '%s for <%s>', self.last_error, self) 981 raise LDAPObjectClassError(self.last_error) 982 983 if self.server and self.server.schema and self.check_names: 984 for object_class_name in _attributes[object_class_attr_name]: 985 if object_class_name.lower() not in conf_classes_excluded_from_check and object_class_name not in self.server.schema.object_classes: 986 self.last_error = 'invalid object class ' + str(object_class_name) 987 if log_enabled(ERROR): 988 log(ERROR, '%s for <%s>', self.last_error, self) 989 raise LDAPObjectClassError(self.last_error) 990 991 for attribute_name in _attributes: 992 if ';' in attribute_name: # remove tags for checking 993 attribute_name_to_check = attribute_name.split(';')[0] 994 else: 995 attribute_name_to_check = attribute_name 996 997 if attribute_name_to_check.lower() not in conf_attributes_excluded_from_check and attribute_name_to_check not in self.server.schema.attribute_types: 998 self.last_error = 'invalid attribute type ' + attribute_name_to_check 999 if log_enabled(ERROR): 1000 log(ERROR, '%s for <%s>', self.last_error, self) 1001 raise LDAPAttributeError(self.last_error) 1002 1003 request = add_operation(dn, _attributes, self.auto_encode, self.server.schema if self.server else None, validator=self.server.custom_validator if self.server else None, check_names=self.check_names) 1004 if log_enabled(PROTOCOL): 1005 log(PROTOCOL, 'ADD request <%s> sent via <%s>', add_request_to_dict(request), self) 1006 response = self.post_send_single_response(self.send('addRequest', request, controls)) 1007 self._entries = [] 1008 1009 if isinstance(response, STRING_TYPES + (int, )): 1010 return_value = response 1011 if log_enabled(PROTOCOL): 1012 log(PROTOCOL, 'async ADD response id <%s> received via <%s>', return_value, self) 1013 else: 1014 if log_enabled(PROTOCOL): 1015 log(PROTOCOL, 'ADD response <%s> received via <%s>', response, self) 1016 return_value = True if self.result['type'] == 'addResponse' and self.result['result'] == RESULT_SUCCESS else False 1017 if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error: 1018 self.last_error = self.result['description'] 1019 1020 if log_enabled(BASIC): 1021 log(BASIC, 'done ADD operation, result <%s>', return_value) 1022 1023 return self._prepare_return_value(return_value) 1024 1025 def delete(self, 1026 dn, 1027 controls=None): 1028 """ 1029 Delete the entry identified by the DN from the DIB. 1030 """ 1031 if log_enabled(BASIC): 1032 log(BASIC, 'start DELETE operation via <%s>', self) 1033 self.last_error = None 1034 if self.check_names: 1035 dn = safe_dn(dn) 1036 if log_enabled(EXTENDED): 1037 log(EXTENDED, 'dn sanitized to <%s> for DELETE operation via <%s>', dn, self) 1038 1039 with self.connection_lock: 1040 self._fire_deferred() 1041 if self.read_only: 1042 self.last_error = 'connection is read-only' 1043 if log_enabled(ERROR): 1044 log(ERROR, '%s for <%s>', self.last_error, self) 1045 raise LDAPConnectionIsReadOnlyError(self.last_error) 1046 1047 request = delete_operation(dn) 1048 if log_enabled(PROTOCOL): 1049 log(PROTOCOL, 'DELETE request <%s> sent via <%s>', delete_request_to_dict(request), self) 1050 response = self.post_send_single_response(self.send('delRequest', request, controls)) 1051 self._entries = [] 1052 1053 if isinstance(response, STRING_TYPES + (int, )): 1054 return_value = response 1055 if log_enabled(PROTOCOL): 1056 log(PROTOCOL, 'async DELETE response id <%s> received via <%s>', return_value, self) 1057 else: 1058 if log_enabled(PROTOCOL): 1059 log(PROTOCOL, 'DELETE response <%s> received via <%s>', response, self) 1060 return_value = True if self.result['type'] == 'delResponse' and self.result['result'] == RESULT_SUCCESS else False 1061 if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error: 1062 self.last_error = self.result['description'] 1063 1064 if log_enabled(BASIC): 1065 log(BASIC, 'done DELETE operation, result <%s>', return_value) 1066 1067 return self._prepare_return_value(return_value) 1068 1069 def modify(self, 1070 dn, 1071 changes, 1072 controls=None): 1073 """ 1074 Modify attributes of entry 1075 1076 - changes is a dictionary in the form {'attribute1': change), 'attribute2': [change, change, ...], ...} 1077 - change is (operation, [value1, value2, ...]) 1078 - operation is 0 (MODIFY_ADD), 1 (MODIFY_DELETE), 2 (MODIFY_REPLACE), 3 (MODIFY_INCREMENT) 1079 """ 1080 conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')] 1081 1082 if log_enabled(BASIC): 1083 log(BASIC, 'start MODIFY operation via <%s>', self) 1084 self.last_error = None 1085 if self.check_names: 1086 dn = safe_dn(dn) 1087 if log_enabled(EXTENDED): 1088 log(EXTENDED, 'dn sanitized to <%s> for MODIFY operation via <%s>', dn, self) 1089 1090 with self.connection_lock: 1091 self._fire_deferred() 1092 if self.read_only: 1093 self.last_error = 'connection is read-only' 1094 if log_enabled(ERROR): 1095 log(ERROR, '%s for <%s>', self.last_error, self) 1096 raise LDAPConnectionIsReadOnlyError(self.last_error) 1097 1098 if not isinstance(changes, dict): 1099 self.last_error = 'changes must be a dictionary' 1100 if log_enabled(ERROR): 1101 log(ERROR, '%s for <%s>', self.last_error, self) 1102 raise LDAPChangeError(self.last_error) 1103 1104 if not changes: 1105 self.last_error = 'no changes in modify request' 1106 if log_enabled(ERROR): 1107 log(ERROR, '%s for <%s>', self.last_error, self) 1108 raise LDAPChangeError(self.last_error) 1109 1110 changelist = dict() 1111 for attribute_name in changes: 1112 if self.server and self.server.schema and self.check_names: 1113 if ';' in attribute_name: # remove tags for checking 1114 attribute_name_to_check = attribute_name.split(';')[0] 1115 else: 1116 attribute_name_to_check = attribute_name 1117 1118 if self.server.schema.attribute_types and attribute_name_to_check.lower() not in conf_attributes_excluded_from_check and attribute_name_to_check not in self.server.schema.attribute_types: 1119 self.last_error = 'invalid attribute type ' + attribute_name_to_check 1120 if log_enabled(ERROR): 1121 log(ERROR, '%s for <%s>', self.last_error, self) 1122 raise LDAPAttributeError(self.last_error) 1123 change = changes[attribute_name] 1124 if isinstance(change, SEQUENCE_TYPES) and change[0] in [MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, MODIFY_INCREMENT, 0, 1, 2, 3]: 1125 if len(change) != 2: 1126 self.last_error = 'malformed change' 1127 if log_enabled(ERROR): 1128 log(ERROR, '%s for <%s>', self.last_error, self) 1129 raise LDAPChangeError(self.last_error) 1130 1131 changelist[attribute_name] = [change] # insert change in a list 1132 else: 1133 for change_operation in change: 1134 if len(change_operation) != 2 or change_operation[0] not in [MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, MODIFY_INCREMENT, 0, 1, 2, 3]: 1135 self.last_error = 'invalid change list' 1136 if log_enabled(ERROR): 1137 log(ERROR, '%s for <%s>', self.last_error, self) 1138 raise LDAPChangeError(self.last_error) 1139 changelist[attribute_name] = change 1140 request = modify_operation(dn, changelist, self.auto_encode, self.server.schema if self.server else None, validator=self.server.custom_validator if self.server else None, check_names=self.check_names) 1141 if log_enabled(PROTOCOL): 1142 log(PROTOCOL, 'MODIFY request <%s> sent via <%s>', modify_request_to_dict(request), self) 1143 response = self.post_send_single_response(self.send('modifyRequest', request, controls)) 1144 self._entries = [] 1145 1146 if isinstance(response, STRING_TYPES + (int, )): 1147 return_value = response 1148 if log_enabled(PROTOCOL): 1149 log(PROTOCOL, 'async MODIFY response id <%s> received via <%s>', return_value, self) 1150 else: 1151 if log_enabled(PROTOCOL): 1152 log(PROTOCOL, 'MODIFY response <%s> received via <%s>', response, self) 1153 return_value = True if self.result['type'] == 'modifyResponse' and self.result['result'] == RESULT_SUCCESS else False 1154 if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error: 1155 self.last_error = self.result['description'] 1156 1157 if log_enabled(BASIC): 1158 log(BASIC, 'done MODIFY operation, result <%s>', return_value) 1159 1160 return self._prepare_return_value(return_value) 1161 1162 def modify_dn(self, 1163 dn, 1164 relative_dn, 1165 delete_old_dn=True, 1166 new_superior=None, 1167 controls=None): 1168 """ 1169 Modify DN of the entry or performs a move of the entry in the 1170 DIT. 1171 """ 1172 if log_enabled(BASIC): 1173 log(BASIC, 'start MODIFY DN operation via <%s>', self) 1174 self.last_error = None 1175 if self.check_names: 1176 dn = safe_dn(dn) 1177 if log_enabled(EXTENDED): 1178 log(EXTENDED, 'dn sanitized to <%s> for MODIFY DN operation via <%s>', dn, self) 1179 relative_dn = safe_dn(relative_dn) 1180 if log_enabled(EXTENDED): 1181 log(EXTENDED, 'relative dn sanitized to <%s> for MODIFY DN operation via <%s>', relative_dn, self) 1182 1183 with self.connection_lock: 1184 self._fire_deferred() 1185 if self.read_only: 1186 self.last_error = 'connection is read-only' 1187 if log_enabled(ERROR): 1188 log(ERROR, '%s for <%s>', self.last_error, self) 1189 raise LDAPConnectionIsReadOnlyError(self.last_error) 1190 1191 # if new_superior and not dn.startswith(relative_dn): # as per RFC4511 (4.9) 1192 # self.last_error = 'DN cannot change while performing moving' 1193 # if log_enabled(ERROR): 1194 # log(ERROR, '%s for <%s>', self.last_error, self) 1195 # raise LDAPChangeError(self.last_error) 1196 1197 request = modify_dn_operation(dn, relative_dn, delete_old_dn, new_superior) 1198 if log_enabled(PROTOCOL): 1199 log(PROTOCOL, 'MODIFY DN request <%s> sent via <%s>', modify_dn_request_to_dict(request), self) 1200 response = self.post_send_single_response(self.send('modDNRequest', request, controls)) 1201 self._entries = [] 1202 1203 if isinstance(response, STRING_TYPES + (int, )): 1204 return_value = response 1205 if log_enabled(PROTOCOL): 1206 log(PROTOCOL, 'async MODIFY DN response id <%s> received via <%s>', return_value, self) 1207 else: 1208 if log_enabled(PROTOCOL): 1209 log(PROTOCOL, 'MODIFY DN response <%s> received via <%s>', response, self) 1210 return_value = True if self.result['type'] == 'modDNResponse' and self.result['result'] == RESULT_SUCCESS else False 1211 if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error: 1212 self.last_error = self.result['description'] 1213 1214 if log_enabled(BASIC): 1215 log(BASIC, 'done MODIFY DN operation, result <%s>', return_value) 1216 1217 return self._prepare_return_value(return_value) 1218 1219 def abandon(self, 1220 message_id, 1221 controls=None): 1222 """ 1223 Abandon the operation indicated by message_id 1224 """ 1225 if log_enabled(BASIC): 1226 log(BASIC, 'start ABANDON operation via <%s>', self) 1227 self.last_error = None 1228 with self.connection_lock: 1229 self._fire_deferred() 1230 return_value = False 1231 if self.strategy._outstanding or message_id == 0: 1232 # only current operation should be abandoned, abandon, bind and unbind cannot ever be abandoned, 1233 # messagiId 0 is invalid and should be used as a "ping" to keep alive the connection 1234 if (self.strategy._outstanding and message_id in self.strategy._outstanding and self.strategy._outstanding[message_id]['type'] not in ['abandonRequest', 'bindRequest', 'unbindRequest']) or message_id == 0: 1235 request = abandon_operation(message_id) 1236 if log_enabled(PROTOCOL): 1237 log(PROTOCOL, 'ABANDON request: <%s> sent via <%s>', abandon_request_to_dict(request), self) 1238 self.send('abandonRequest', request, controls) 1239 self.result = None 1240 self.response = None 1241 self._entries = [] 1242 return_value = True 1243 else: 1244 if log_enabled(ERROR): 1245 log(ERROR, 'cannot abandon a Bind, an Unbind or an Abandon operation or message ID %s not found via <%s>', str(message_id), self) 1246 1247 if log_enabled(BASIC): 1248 log(BASIC, 'done ABANDON operation, result <%s>', return_value) 1249 1250 return self._prepare_return_value(return_value) 1251 1252 def extended(self, 1253 request_name, 1254 request_value=None, 1255 controls=None, 1256 no_encode=None): 1257 """ 1258 Performs an extended operation 1259 """ 1260 if log_enabled(BASIC): 1261 log(BASIC, 'start EXTENDED operation via <%s>', self) 1262 self.last_error = None 1263 with self.connection_lock: 1264 self._fire_deferred() 1265 request = extended_operation(request_name, request_value, no_encode=no_encode) 1266 if log_enabled(PROTOCOL): 1267 log(PROTOCOL, 'EXTENDED request <%s> sent via <%s>', extended_request_to_dict(request), self) 1268 response = self.post_send_single_response(self.send('extendedReq', request, controls)) 1269 self._entries = [] 1270 if isinstance(response, int): 1271 return_value = response 1272 if log_enabled(PROTOCOL): 1273 log(PROTOCOL, 'async EXTENDED response id <%s> received via <%s>', return_value, self) 1274 else: 1275 if log_enabled(PROTOCOL): 1276 log(PROTOCOL, 'EXTENDED response <%s> received via <%s>', response, self) 1277 return_value = True if self.result['type'] == 'extendedResp' and self.result['result'] == RESULT_SUCCESS else False 1278 if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error: 1279 self.last_error = self.result['description'] 1280 1281 if log_enabled(BASIC): 1282 log(BASIC, 'done EXTENDED operation, result <%s>', return_value) 1283 1284 return self._prepare_return_value(return_value, response=True) 1285 1286 def start_tls(self, read_server_info=True): # as per RFC4511. Removal of TLS is defined as MAY in RFC4511 so the client can't implement a generic stop_tls method0 1287 if log_enabled(BASIC): 1288 log(BASIC, 'start START TLS operation via <%s>', self) 1289 1290 with self.connection_lock: 1291 return_value = False 1292 self.result = None 1293 1294 if not self.server.tls: 1295 self.server.tls = Tls() 1296 1297 if self.lazy and not self._executing_deferred: 1298 self._deferred_start_tls = True 1299 self.tls_started = True 1300 return_value = True 1301 if log_enabled(BASIC): 1302 log(BASIC, 'deferring START TLS for <%s>', self) 1303 else: 1304 self._deferred_start_tls = False 1305 if self.closed: 1306 self.open() 1307 if self.server.tls.start_tls(self) and self.strategy.sync: # for asynchronous connections _start_tls is run by the strategy 1308 if read_server_info: 1309 self.refresh_server_info() # refresh server info as per RFC4515 (3.1.5) 1310 return_value = True 1311 elif not self.strategy.sync: 1312 return_value = True 1313 1314 if log_enabled(BASIC): 1315 log(BASIC, 'done START TLS operation, result <%s>', return_value) 1316 1317 return self._prepare_return_value(return_value) 1318 1319 def do_sasl_bind(self, 1320 controls): 1321 if log_enabled(BASIC): 1322 log(BASIC, 'start SASL BIND operation via <%s>', self) 1323 self.last_error = None 1324 with self.connection_lock: 1325 result = None 1326 1327 if not self.sasl_in_progress: 1328 self.sasl_in_progress = True 1329 try: 1330 if self.sasl_mechanism == EXTERNAL: 1331 result = sasl_external(self, controls) 1332 elif self.sasl_mechanism == DIGEST_MD5: 1333 result = sasl_digest_md5(self, controls) 1334 elif self.sasl_mechanism == GSSAPI: 1335 from ..protocol.sasl.kerberos import sasl_gssapi # needs the gssapi package 1336 result = sasl_gssapi(self, controls) 1337 elif self.sasl_mechanism == 'PLAIN': 1338 result = sasl_plain(self, controls) 1339 finally: 1340 self.sasl_in_progress = False 1341 1342 if log_enabled(BASIC): 1343 log(BASIC, 'done SASL BIND operation, result <%s>', result) 1344 1345 return result 1346 1347 def do_ntlm_bind(self, 1348 controls): 1349 if log_enabled(BASIC): 1350 log(BASIC, 'start NTLM BIND operation via <%s>', self) 1351 self.last_error = None 1352 with self.connection_lock: 1353 result = None 1354 if not self.sasl_in_progress: 1355 self.sasl_in_progress = True # ntlm is same of sasl authentication 1356 try: 1357 # additional import for NTLM 1358 from ..utils.ntlm import NtlmClient 1359 domain_name, user_name = self.user.split('\\', 1) 1360 ntlm_client = NtlmClient(user_name=user_name, domain=domain_name, password=self.password) 1361 1362 # as per https://msdn.microsoft.com/en-us/library/cc223501.aspx 1363 # send a sicilyPackageDiscovery request (in the bindRequest) 1364 request = bind_operation(self.version, 'SICILY_PACKAGE_DISCOVERY', ntlm_client) 1365 if log_enabled(PROTOCOL): 1366 log(PROTOCOL, 'NTLM SICILY PACKAGE DISCOVERY request sent via <%s>', self) 1367 response = self.post_send_single_response(self.send('bindRequest', request, controls)) 1368 if not self.strategy.sync: 1369 _, result = self.get_response(response) 1370 else: 1371 result = response[0] 1372 if 'server_creds' in result: 1373 sicily_packages = result['server_creds'].decode('ascii').split(';') 1374 if 'NTLM' in sicily_packages: # NTLM available on server 1375 request = bind_operation(self.version, 'SICILY_NEGOTIATE_NTLM', ntlm_client) 1376 if log_enabled(PROTOCOL): 1377 log(PROTOCOL, 'NTLM SICILY NEGOTIATE request sent via <%s>', self) 1378 response = self.post_send_single_response(self.send('bindRequest', request, controls)) 1379 if not self.strategy.sync: 1380 _, result = self.get_response(response) 1381 else: 1382 if log_enabled(PROTOCOL): 1383 log(PROTOCOL, 'NTLM SICILY NEGOTIATE response <%s> received via <%s>', response[0], 1384 self) 1385 result = response[0] 1386 1387 if result['result'] == RESULT_SUCCESS: 1388 request = bind_operation(self.version, 'SICILY_RESPONSE_NTLM', ntlm_client, 1389 result['server_creds']) 1390 if log_enabled(PROTOCOL): 1391 log(PROTOCOL, 'NTLM SICILY RESPONSE NTLM request sent via <%s>', self) 1392 response = self.post_send_single_response(self.send('bindRequest', request, controls)) 1393 if not self.strategy.sync: 1394 _, result = self.get_response(response) 1395 else: 1396 if log_enabled(PROTOCOL): 1397 log(PROTOCOL, 'NTLM BIND response <%s> received via <%s>', response[0], self) 1398 result = response[0] 1399 else: 1400 result = None 1401 finally: 1402 self.sasl_in_progress = False 1403 1404 if log_enabled(BASIC): 1405 log(BASIC, 'done SASL NTLM operation, result <%s>', result) 1406 1407 return result 1408 1409 def refresh_server_info(self): 1410 # if self.strategy.no_real_dsa: # do not refresh for mock strategies 1411 # return 1412 1413 if not self.strategy.pooled: 1414 with self.connection_lock: 1415 if not self.closed: 1416 if log_enabled(BASIC): 1417 log(BASIC, 'refreshing server info for <%s>', self) 1418 previous_response = self.response 1419 previous_result = self.result 1420 previous_entries = self._entries 1421 self.server.get_info_from_server(self) 1422 self.response = previous_response 1423 self.result = previous_result 1424 self._entries = previous_entries 1425 else: 1426 if log_enabled(BASIC): 1427 log(BASIC, 'refreshing server info from pool for <%s>', self) 1428 self.strategy.pool.get_info_from_server() 1429 1430 def response_to_ldif(self, 1431 search_result=None, 1432 all_base64=False, 1433 line_separator=None, 1434 sort_order=None, 1435 stream=None): 1436 with self.connection_lock: 1437 if search_result is None: 1438 search_result = self.response 1439 1440 if isinstance(search_result, SEQUENCE_TYPES): 1441 ldif_lines = operation_to_ldif('searchResponse', search_result, all_base64, sort_order=sort_order) 1442 ldif_lines = add_ldif_header(ldif_lines) 1443 line_separator = line_separator or linesep 1444 ldif_output = line_separator.join(ldif_lines) 1445 if stream: 1446 if stream.tell() == 0: 1447 header = add_ldif_header(['-'])[0] 1448 stream.write(prepare_for_stream(header + line_separator + line_separator)) 1449 stream.write(prepare_for_stream(ldif_output + line_separator + line_separator)) 1450 if log_enabled(BASIC): 1451 log(BASIC, 'building LDIF output <%s> for <%s>', ldif_output, self) 1452 return ldif_output 1453 1454 return None 1455 1456 def response_to_json(self, 1457 raw=False, 1458 search_result=None, 1459 indent=4, 1460 sort=True, 1461 stream=None, 1462 checked_attributes=True, 1463 include_empty=True): 1464 1465 with self.connection_lock: 1466 if search_result is None: 1467 search_result = self.response 1468 1469 if isinstance(search_result, SEQUENCE_TYPES): 1470 json_dict = dict() 1471 json_dict['entries'] = [] 1472 1473 for response in search_result: 1474 if response['type'] == 'searchResEntry': 1475 entry = dict() 1476 1477 entry['dn'] = response['dn'] 1478 if checked_attributes: 1479 if not include_empty: 1480 # needed for python 2.6 compatibility 1481 entry['attributes'] = dict((key, response['attributes'][key]) for key in response['attributes'] if response['attributes'][key]) 1482 else: 1483 entry['attributes'] = dict(response['attributes']) 1484 if raw: 1485 if not include_empty: 1486 # needed for python 2.6 compatibility 1487 entry['raw_attributes'] = dict((key, response['raw_attributes'][key]) for key in response['raw_attributes'] if response['raw:attributes'][key]) 1488 else: 1489 entry['raw'] = dict(response['raw_attributes']) 1490 json_dict['entries'].append(entry) 1491 1492 if str is bytes: # Python 2 1493 check_json_dict(json_dict) 1494 1495 json_output = json.dumps(json_dict, ensure_ascii=True, sort_keys=sort, indent=indent, check_circular=True, default=format_json, separators=(',', ': ')) 1496 1497 if log_enabled(BASIC): 1498 log(BASIC, 'building JSON output <%s> for <%s>', json_output, self) 1499 if stream: 1500 stream.write(json_output) 1501 1502 return json_output 1503 1504 def response_to_file(self, 1505 target, 1506 raw=False, 1507 indent=4, 1508 sort=True): 1509 with self.connection_lock: 1510 if self.response: 1511 if isinstance(target, STRING_TYPES): 1512 target = open(target, 'w+') 1513 1514 if log_enabled(BASIC): 1515 log(BASIC, 'writing response to file for <%s>', self) 1516 1517 target.writelines(self.response_to_json(raw=raw, indent=indent, sort=sort)) 1518 target.close() 1519 1520 def _fire_deferred(self, read_info=None): 1521 # if read_info is None reads the schema and server info if not present, if False doesn't read server info, if True reads always server info 1522 with self.connection_lock: 1523 if self.lazy and not self._executing_deferred: 1524 self._executing_deferred = True 1525 1526 if log_enabled(BASIC): 1527 log(BASIC, 'executing deferred (open: %s, start_tls: %s, bind: %s) for <%s>', self._deferred_open, self._deferred_start_tls, self._deferred_bind, self) 1528 try: 1529 if self._deferred_open: 1530 self.open(read_server_info=False) 1531 if self._deferred_start_tls: 1532 if not self.start_tls(read_server_info=False): 1533 error = 'deferred start_tls not successful' + (' - ' + self.last_error if self.last_error else '') 1534 if log_enabled(ERROR): 1535 log(ERROR, '%s for <%s>', error, self) 1536 self.unbind() 1537 raise LDAPStartTLSError(error) 1538 if self._deferred_bind: 1539 self.bind(read_server_info=False, controls=self._bind_controls) 1540 if (read_info is None and (not self.server.info and self.server.get_info in [DSA, ALL]) or (not self.server.schema and self.server.get_info in [SCHEMA, ALL])) or read_info: 1541 self.refresh_server_info() 1542 except LDAPExceptionError as e: 1543 if log_enabled(ERROR): 1544 log(ERROR, '%s for <%s>', e, self) 1545 raise # re-raise LDAPExceptionError 1546 finally: 1547 self._executing_deferred = False 1548 1549 @property 1550 def entries(self): 1551 if self.response: 1552 if not self._entries: 1553 self._entries = self._get_entries(self.response, self.request) 1554 return self._entries 1555 1556 def _get_entries(self, search_response, search_request): 1557 with self.connection_lock: 1558 from .. import ObjectDef, Reader 1559 1560 # build a table of ObjectDefs, grouping the entries found in search_response for their attributes set, subset will be included in superset 1561 attr_sets = [] 1562 for response in search_response: 1563 if response['type'] == 'searchResEntry': 1564 resp_attr_set = set(response['attributes'].keys()) 1565 if resp_attr_set not in attr_sets: 1566 attr_sets.append(resp_attr_set) 1567 attr_sets.sort(key=lambda x: -len(x)) # sorts the list in descending length order 1568 unique_attr_sets = [] 1569 for attr_set in attr_sets: 1570 for unique_set in unique_attr_sets: 1571 if unique_set >= attr_set: # checks if unique set is a superset of attr_set 1572 break 1573 else: # the attr_set is not a subset of any element in unique_attr_sets 1574 unique_attr_sets.append(attr_set) 1575 object_defs = [] 1576 for attr_set in unique_attr_sets: 1577 object_def = ObjectDef(schema=self.server.schema) 1578 object_def += list(attr_set) # converts the set in a list to be added to the object definition 1579 object_defs.append((attr_set, 1580 object_def, 1581 Reader(self, object_def, search_request['base'], search_request['filter'], attributes=attr_set) if self.strategy.sync else Reader(self, object_def, '', '', attributes=attr_set)) 1582 ) # objects_defs contains a tuple with the set, the ObjectDef and a cursor 1583 1584 entries = [] 1585 for response in search_response: 1586 if response['type'] == 'searchResEntry': 1587 resp_attr_set = set(response['attributes'].keys()) 1588 for object_def in object_defs: 1589 if resp_attr_set <= object_def[0]: # finds the ObjectDef for the attribute set of this entry 1590 entry = object_def[2]._create_entry(response) 1591 entries.append(entry) 1592 break 1593 else: 1594 self.last_error = 'attribute set not found for ' + str(resp_attr_set) 1595 if log_enabled(ERROR): 1596 log(ERROR, self.last_error, self) 1597 raise LDAPObjectError(self.last_error) 1598 1599 return entries 1600