1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501 |
- """
- """
-
- # Created on 2014.05.31
- #
- # Author: Giovanni Cannata
- #
- # Copyright 2014 - 2018 Giovanni Cannata
- #
- # This file is part of ldap3.
- #
- # ldap3 is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Lesser General Public License as published
- # by the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # ldap3 is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU Lesser General Public License for more details.
- #
- # You should have received a copy of the GNU Lesser General Public License
- # along with ldap3 in the COPYING and COPYING.LESSER files.
- # If not, see <http://www.gnu.org/licenses/>.
- from copy import deepcopy
- from os import linesep
- from threading import RLock, Lock
- from functools import reduce
- import json
-
- from .. import ANONYMOUS, SIMPLE, SASL, MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, get_config_parameter, DEREF_ALWAYS, \
- SUBTREE, ASYNC, SYNC, NO_ATTRIBUTES, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, MODIFY_INCREMENT, LDIF, ASYNC_STREAM, \
- RESTARTABLE, ROUND_ROBIN, REUSABLE, AUTO_BIND_NONE, AUTO_BIND_TLS_BEFORE_BIND, AUTO_BIND_TLS_AFTER_BIND, AUTO_BIND_NO_TLS, \
- STRING_TYPES, SEQUENCE_TYPES, MOCK_SYNC, MOCK_ASYNC, NTLM, EXTERNAL, DIGEST_MD5, GSSAPI, PLAIN
-
- from .results import RESULT_SUCCESS, RESULT_COMPARE_TRUE, RESULT_COMPARE_FALSE
- from ..extend import ExtendedOperationsRoot
- from .pooling import ServerPool
- from .server import Server
- from ..operation.abandon import abandon_operation, abandon_request_to_dict
- from ..operation.add import add_operation, add_request_to_dict
- from ..operation.bind import bind_operation, bind_request_to_dict
- from ..operation.compare import compare_operation, compare_request_to_dict
- from ..operation.delete import delete_operation, delete_request_to_dict
- from ..operation.extended import extended_operation, extended_request_to_dict
- from ..operation.modify import modify_operation, modify_request_to_dict
- from ..operation.modifyDn import modify_dn_operation, modify_dn_request_to_dict
- from ..operation.search import search_operation, search_request_to_dict
- from ..protocol.rfc2849 import operation_to_ldif, add_ldif_header
- from ..protocol.sasl.digestMd5 import sasl_digest_md5
- from ..protocol.sasl.external import sasl_external
- from ..protocol.sasl.plain import sasl_plain
- from ..strategy.sync import SyncStrategy
- from ..strategy.mockAsync import MockAsyncStrategy
- from ..strategy.asynchronous import AsyncStrategy
- from ..strategy.reusable import ReusableStrategy
- from ..strategy.restartable import RestartableStrategy
- from ..strategy.ldifProducer import LdifProducerStrategy
- from ..strategy.mockSync import MockSyncStrategy
- from ..strategy.asyncStream import AsyncStreamStrategy
- from ..operation.unbind import unbind_operation
- from ..protocol.rfc2696 import paged_search_control
- from .usage import ConnectionUsage
- from .tls import Tls
- from .exceptions import LDAPUnknownStrategyError, LDAPBindError, LDAPUnknownAuthenticationMethodError, \
- LDAPSASLMechanismNotSupportedError, LDAPObjectClassError, LDAPConnectionIsReadOnlyError, LDAPChangeError, LDAPExceptionError, \
- LDAPObjectError, LDAPSocketReceiveError, LDAPAttributeError, LDAPInvalidValueError, LDAPConfigurationError
-
- from ..utils.conv import escape_bytes, prepare_for_stream, check_json_dict, format_json, to_unicode
- from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED, get_library_log_hide_sensitive_data
- from ..utils.dn import safe_dn
-
-
- SASL_AVAILABLE_MECHANISMS = [EXTERNAL,
- DIGEST_MD5,
- GSSAPI,
- PLAIN]
-
- CLIENT_STRATEGIES = [SYNC,
- ASYNC,
- LDIF,
- RESTARTABLE,
- REUSABLE,
- MOCK_SYNC,
- MOCK_ASYNC,
- ASYNC_STREAM]
-
-
- def _format_socket_endpoint(endpoint):
- if endpoint and len(endpoint) == 2: # IPv4
- return str(endpoint[0]) + ':' + str(endpoint[1])
- elif endpoint and len(endpoint) == 4: # IPv6
- return '[' + str(endpoint[0]) + ']:' + str(endpoint[1])
-
- try:
- return str(endpoint)
- except Exception:
- return '?'
-
-
- def _format_socket_endpoints(sock):
- if sock:
- try:
- local = sock.getsockname()
- except Exception:
- local = (None, None, None, None)
- try:
- remote = sock.getpeername()
- except Exception:
- remote = (None, None, None, None)
-
- return '<local: ' + _format_socket_endpoint(local) + ' - remote: ' + _format_socket_endpoint(remote) + '>'
- return '<no socket>'
-
-
- # noinspection PyProtectedMember
- class Connection(object):
- """Main ldap connection class.
-
- Controls, if used, must be a list of tuples. Each tuple must have 3
- elements, the control OID, a boolean meaning if the control is
- critical, a value.
-
- If the boolean is set to True the server must honor the control or
- refuse the operation
-
- Mixing controls must be defined in controls specification (as per
- RFC 4511)
-
- :param server: the Server object to connect to
- :type server: Server, str
- :param user: the user name for simple authentication
- :type user: str
- :param password: the password for simple authentication
- :type password: str
- :param auto_bind: specify if the bind will be performed automatically when defining the Connection object
- :type auto_bind: int, can be one of AUTO_BIND_NONE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND, AUTO_BIND_TLS_AFTER_BIND as specified in ldap3
- :param version: LDAP version, default to 3
- :type version: int
- :param authentication: type of authentication
- :type authentication: int, can be one of AUTH_ANONYMOUS, AUTH_SIMPLE or AUTH_SASL, as specified in ldap3
- :param client_strategy: communication strategy used in the Connection
- :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
- :param auto_referrals: specify if the connection object must automatically follow referrals
- :type auto_referrals: bool
- :param sasl_mechanism: mechanism for SASL authentication, can be one of 'EXTERNAL', 'DIGEST-MD5', 'GSSAPI', 'PLAIN'
- :type sasl_mechanism: str
- :param sasl_credentials: credentials for SASL mechanism
- :type sasl_credentials: tuple
- :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
- :type check_names: bool
- :param collect_usage: collect usage metrics in the usage attribute
- :type collect_usage: bool
- :param read_only: disable operations that modify data in the LDAP server
- :type read_only: bool
- :param lazy: open and bind the connection only when an actual operation is performed
- :type lazy: bool
- :param raise_exceptions: raise exceptions when operations are not successful, if False operations return False if not successful but not raise exceptions
- :type raise_exceptions: bool
- :param pool_name: pool name for pooled strategies
- :type pool_name: str
- :param pool_size: pool size for pooled strategies
- :type pool_size: int
- :param pool_lifetime: pool lifetime for pooled strategies
- :type pool_lifetime: int
- :param use_referral_cache: keep referral connections open and reuse them
- :type use_referral_cache: bool
- :param auto_escape: automatic escaping of filter values
- :param auto_encode: automatic encoding of attribute values
- :type use_referral_cache: bool
- """
-
- def __init__(self,
- server,
- user=None,
- password=None,
- auto_bind=AUTO_BIND_NONE,
- version=3,
- authentication=None,
- client_strategy=SYNC,
- auto_referrals=True,
- auto_range=True,
- sasl_mechanism=None,
- sasl_credentials=None,
- check_names=True,
- collect_usage=False,
- read_only=False,
- lazy=False,
- raise_exceptions=False,
- pool_name=None,
- pool_size=None,
- pool_lifetime=None,
- fast_decoder=True,
- receive_timeout=None,
- return_empty_attributes=True,
- use_referral_cache=False,
- auto_escape=True,
- auto_encode=True,
- pool_keepalive=None):
-
- conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME')
- self.connection_lock = RLock() # re-entrant lock to ensure that operations in the Connection object are executed atomically in the same thread
- with self.connection_lock:
- if client_strategy not in CLIENT_STRATEGIES:
- self.last_error = 'unknown client connection strategy'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPUnknownStrategyError(self.last_error)
-
- self.strategy_type = client_strategy
- self.user = user
- self.password = password
-
- if not authentication and self.user:
- self.authentication = SIMPLE
- elif not authentication:
- self.authentication = ANONYMOUS
- elif authentication in [SIMPLE, ANONYMOUS, SASL, NTLM]:
- self.authentication = authentication
- else:
- self.last_error = 'unknown authentication method'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPUnknownAuthenticationMethodError(self.last_error)
-
- self.version = version
- self.auto_referrals = True if auto_referrals else False
- self.request = None
- self.response = None
- self.result = None
- self.bound = False
- self.listening = False
- self.closed = True
- self.last_error = None
- if auto_bind is False: # compatibility with older version where auto_bind was a boolean
- self.auto_bind = AUTO_BIND_NONE
- elif auto_bind is True:
- self.auto_bind = AUTO_BIND_NO_TLS
- else:
- self.auto_bind = auto_bind
- self.sasl_mechanism = sasl_mechanism
- self.sasl_credentials = sasl_credentials
- self._usage = ConnectionUsage() if collect_usage else None
- self.socket = None
- self.tls_started = False
- self.sasl_in_progress = False
- self.read_only = read_only
- self._context_state = []
- self._deferred_open = False
- self._deferred_bind = False
- self._deferred_start_tls = False
- self._bind_controls = None
- self._executing_deferred = False
- self.lazy = lazy
- self.pool_name = pool_name if pool_name else conf_default_pool_name
- self.pool_size = pool_size
- self.pool_lifetime = pool_lifetime
- self.pool_keepalive = pool_keepalive
- self.starting_tls = False
- self.check_names = check_names
- self.raise_exceptions = raise_exceptions
- self.auto_range = True if auto_range else False
- self.extend = ExtendedOperationsRoot(self)
- self._entries = []
- self.fast_decoder = fast_decoder
- self.receive_timeout = receive_timeout
- self.empty_attributes = return_empty_attributes
- self.use_referral_cache = use_referral_cache
- self.auto_escape = auto_escape
- self.auto_encode = auto_encode
-
- if isinstance(server, STRING_TYPES):
- server = Server(server)
- if isinstance(server, SEQUENCE_TYPES):
- server = ServerPool(server, ROUND_ROBIN, active=True, exhaust=True)
-
- if isinstance(server, ServerPool):
- self.server_pool = server
- self.server_pool.initialize(self)
- self.server = self.server_pool.get_current_server(self)
- else:
- self.server_pool = None
- self.server = server
-
- # if self.authentication == SIMPLE and self.user and self.check_names:
- # self.user = safe_dn(self.user)
- # if log_enabled(EXTENDED):
- # log(EXTENDED, 'user name sanitized to <%s> for simple authentication via <%s>', self.user, self)
-
- if self.strategy_type == SYNC:
- self.strategy = SyncStrategy(self)
- elif self.strategy_type == ASYNC:
- self.strategy = AsyncStrategy(self)
- elif self.strategy_type == LDIF:
- self.strategy = LdifProducerStrategy(self)
- elif self.strategy_type == RESTARTABLE:
- self.strategy = RestartableStrategy(self)
- elif self.strategy_type == REUSABLE:
- self.strategy = ReusableStrategy(self)
- self.lazy = False
- elif self.strategy_type == MOCK_SYNC:
- self.strategy = MockSyncStrategy(self)
- elif self.strategy_type == MOCK_ASYNC:
- self.strategy = MockAsyncStrategy(self)
- elif self.strategy_type == ASYNC_STREAM:
- self.strategy = AsyncStreamStrategy(self)
- else:
- self.last_error = 'unknown strategy'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPUnknownStrategyError(self.last_error)
-
- # maps strategy functions to connection functions
- self.send = self.strategy.send
- self.open = self.strategy.open
- self.get_response = self.strategy.get_response
- self.post_send_single_response = self.strategy.post_send_single_response
- self.post_send_search = self.strategy.post_send_search
-
- if not self.strategy.no_real_dsa:
- self.do_auto_bind()
- # else: # for strategies with a fake server set get_info to NONE if server hasn't a schema
- # if self.server and not self.server.schema:
- # self.server.get_info = NONE
- if log_enabled(BASIC):
- if get_library_log_hide_sensitive_data():
- log(BASIC, 'instantiated Connection: <%s>', self.repr_with_sensitive_data_stripped())
- else:
- log(BASIC, 'instantiated Connection: <%r>', self)
-
- def do_auto_bind(self):
- if self.auto_bind and self.auto_bind != AUTO_BIND_NONE:
- if log_enabled(BASIC):
- log(BASIC, 'performing automatic bind for <%s>', self)
- if self.closed:
- self.open(read_server_info=False)
- if self.auto_bind == AUTO_BIND_NO_TLS:
- self.bind(read_server_info=True)
- elif self.auto_bind == AUTO_BIND_TLS_BEFORE_BIND:
- self.start_tls(read_server_info=False)
- self.bind(read_server_info=True)
- elif self.auto_bind == AUTO_BIND_TLS_AFTER_BIND:
- self.bind(read_server_info=False)
- self.start_tls(read_server_info=True)
- if not self.bound:
- self.last_error = 'automatic bind not successful' + (' - ' + self.last_error if self.last_error else '')
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPBindError(self.last_error)
-
- def __str__(self):
- s = [
- str(self.server) if self.server else 'None',
- 'user: ' + str(self.user),
- 'lazy' if self.lazy else 'not lazy',
- 'unbound' if not self.bound else ('deferred bind' if self._deferred_bind else 'bound'),
- 'closed' if self.closed else ('deferred open' if self._deferred_open else 'open'),
- _format_socket_endpoints(self.socket),
- 'tls not started' if not self.tls_started else('deferred start_tls' if self._deferred_start_tls else 'tls started'),
- 'listening' if self.listening else 'not listening',
- self.strategy.__class__.__name__ if hasattr(self, 'strategy') else 'No strategy',
- 'internal decoder' if self.fast_decoder else 'pyasn1 decoder'
- ]
- return ' - '.join(s)
-
- def __repr__(self):
- conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME')
- if self.server_pool:
- r = 'Connection(server={0.server_pool!r}'.format(self)
- else:
- r = 'Connection(server={0.server!r}'.format(self)
- r += '' if self.user is None else ', user={0.user!r}'.format(self)
- r += '' if self.password is None else ', password={0.password!r}'.format(self)
- r += '' if self.auto_bind is None else ', auto_bind={0.auto_bind!r}'.format(self)
- r += '' if self.version is None else ', version={0.version!r}'.format(self)
- r += '' if self.authentication is None else ', authentication={0.authentication!r}'.format(self)
- r += '' if self.strategy_type is None else ', client_strategy={0.strategy_type!r}'.format(self)
- r += '' if self.auto_referrals is None else ', auto_referrals={0.auto_referrals!r}'.format(self)
- r += '' if self.sasl_mechanism is None else ', sasl_mechanism={0.sasl_mechanism!r}'.format(self)
- r += '' if self.sasl_credentials is None else ', sasl_credentials={0.sasl_credentials!r}'.format(self)
- r += '' if self.check_names is None else ', check_names={0.check_names!r}'.format(self)
- r += '' if self.usage is None else (', collect_usage=' + ('True' if self.usage else 'False'))
- r += '' if self.read_only is None else ', read_only={0.read_only!r}'.format(self)
- r += '' if self.lazy is None else ', lazy={0.lazy!r}'.format(self)
- r += '' if self.raise_exceptions is None else ', raise_exceptions={0.raise_exceptions!r}'.format(self)
- r += '' if (self.pool_name is None or self.pool_name == conf_default_pool_name) else ', pool_name={0.pool_name!r}'.format(self)
- r += '' if self.pool_size is None else ', pool_size={0.pool_size!r}'.format(self)
- r += '' if self.pool_lifetime is None else ', pool_lifetime={0.pool_lifetime!r}'.format(self)
- r += '' if self.pool_keepalive is None else ', pool_keepalive={0.pool_keepalive!r}'.format(self)
- r += '' if self.fast_decoder is None else (', fast_decoder=' + ('True' if self.fast_decoder else 'False'))
- r += '' if self.auto_range is None else (', auto_range=' + ('True' if self.auto_range else 'False'))
- r += '' if self.receive_timeout is None else ', receive_timeout={0.receive_timeout!r}'.format(self)
- r += '' if self.empty_attributes is None else (', return_empty_attributes=' + ('True' if self.empty_attributes else 'False'))
- r += '' if self.auto_encode is None else (', auto_encode=' + ('True' if self.auto_encode else 'False'))
- r += '' if self.auto_escape is None else (', auto_escape=' + ('True' if self.auto_escape else 'False'))
- r += '' if self.use_referral_cache is None else (', use_referral_cache=' + ('True' if self.use_referral_cache else 'False'))
- r += ')'
-
- return r
-
- def repr_with_sensitive_data_stripped(self):
- conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME')
- if self.server_pool:
- r = 'Connection(server={0.server_pool!r}'.format(self)
- else:
- r = 'Connection(server={0.server!r}'.format(self)
- r += '' if self.user is None else ', user={0.user!r}'.format(self)
- r += '' if self.password is None else ", password='{0}'".format('<stripped %d characters of sensitive data>' % len(self.password))
- r += '' if self.auto_bind is None else ', auto_bind={0.auto_bind!r}'.format(self)
- r += '' if self.version is None else ', version={0.version!r}'.format(self)
- r += '' if self.authentication is None else ', authentication={0.authentication!r}'.format(self)
- r += '' if self.strategy_type is None else ', client_strategy={0.strategy_type!r}'.format(self)
- r += '' if self.auto_referrals is None else ', auto_referrals={0.auto_referrals!r}'.format(self)
- r += '' if self.sasl_mechanism is None else ', sasl_mechanism={0.sasl_mechanism!r}'.format(self)
- if self.sasl_mechanism == DIGEST_MD5:
- 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])
- else:
- r += '' if self.sasl_credentials is None else ', sasl_credentials={0.sasl_credentials!r}'.format(self)
- r += '' if self.check_names is None else ', check_names={0.check_names!r}'.format(self)
- r += '' if self.usage is None else (', collect_usage=' + 'True' if self.usage else 'False')
- r += '' if self.read_only is None else ', read_only={0.read_only!r}'.format(self)
- r += '' if self.lazy is None else ', lazy={0.lazy!r}'.format(self)
- r += '' if self.raise_exceptions is None else ', raise_exceptions={0.raise_exceptions!r}'.format(self)
- r += '' if (self.pool_name is None or self.pool_name == conf_default_pool_name) else ', pool_name={0.pool_name!r}'.format(self)
- r += '' if self.pool_size is None else ', pool_size={0.pool_size!r}'.format(self)
- r += '' if self.pool_lifetime is None else ', pool_lifetime={0.pool_lifetime!r}'.format(self)
- r += '' if self.pool_keepalive is None else ', pool_keepalive={0.pool_keepalive!r}'.format(self)
- r += '' if self.fast_decoder is None else (', fast_decoder=' + 'True' if self.fast_decoder else 'False')
- r += '' if self.auto_range is None else (', auto_range=' + ('True' if self.auto_range else 'False'))
- r += '' if self.receive_timeout is None else ', receive_timeout={0.receive_timeout!r}'.format(self)
- r += '' if self.empty_attributes is None else (', return_empty_attributes=' + 'True' if self.empty_attributes else 'False')
- r += '' if self.auto_encode is None else (', auto_encode=' + ('True' if self.auto_encode else 'False'))
- r += '' if self.auto_escape is None else (', auto_escape=' + ('True' if self.auto_escape else 'False'))
- r += '' if self.use_referral_cache is None else (', use_referral_cache=' + ('True' if self.use_referral_cache else 'False'))
- r += ')'
-
- return r
-
- @property
- def stream(self):
- """Used by the LDIFProducer strategy to accumulate the ldif-change operations with a single LDIF header
- :return: reference to the response stream if defined in the strategy.
- """
- return self.strategy.get_stream() if self.strategy.can_stream else None
-
- @stream.setter
- def stream(self, value):
- with self.connection_lock:
- if self.strategy.can_stream:
- self.strategy.set_stream(value)
-
- @property
- def usage(self):
- """Usage statistics for the connection.
- :return: Usage object
- """
- if not self._usage:
- return None
- if self.strategy.pooled: # update master connection usage from pooled connections
- self._usage.reset()
- for worker in self.strategy.pool.workers:
- self._usage += worker.connection.usage
- self._usage += self.strategy.pool.terminated_usage
- return self._usage
-
- def __enter__(self):
- with self.connection_lock:
- self._context_state.append((self.bound, self.closed)) # save status out of context as a tuple in a list
- if self.closed:
- self.open()
- if not self.bound:
- self.bind()
-
- return self
-
- # noinspection PyUnusedLocal
- def __exit__(self, exc_type, exc_val, exc_tb):
- with self.connection_lock:
- context_bound, context_closed = self._context_state.pop()
- if (not context_bound and self.bound) or self.stream: # restore status prior to entering context
- try:
- self.unbind()
- except LDAPExceptionError:
- pass
-
- if not context_closed and self.closed:
- self.open()
-
- if exc_type is not None:
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', exc_type, self)
- return False # re-raise LDAPExceptionError
-
- def bind(self,
- read_server_info=True,
- controls=None):
- """Bind to ldap Server with the authentication method and the user defined in the connection
-
- :param read_server_info: reads info from server
- :param controls: LDAP controls to send along with the bind operation
- :type controls: list of tuple
- :return: bool
-
- """
- if log_enabled(BASIC):
- log(BASIC, 'start BIND operation via <%s>', self)
- self.last_error = None
- with self.connection_lock:
- if self.lazy and not self._executing_deferred:
- if self.strategy.pooled:
- self.strategy.validate_bind(controls)
- self._deferred_bind = True
- self._bind_controls = controls
- self.bound = True
- if log_enabled(BASIC):
- log(BASIC, 'deferring bind for <%s>', self)
- else:
- self._deferred_bind = False
- self._bind_controls = None
- if self.closed: # try to open connection if closed
- self.open(read_server_info=False)
- if self.authentication == ANONYMOUS:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'performing anonymous BIND for <%s>', self)
- if not self.strategy.pooled:
- request = bind_operation(self.version, self.authentication, self.user, '', auto_encode=self.auto_encode)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'anonymous BIND request <%s> sent via <%s>', bind_request_to_dict(request), self)
- response = self.post_send_single_response(self.send('bindRequest', request, controls))
- else:
- response = self.strategy.validate_bind(controls) # only for REUSABLE
- elif self.authentication == SIMPLE:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'performing simple BIND for <%s>', self)
- if not self.strategy.pooled:
- request = bind_operation(self.version, self.authentication, self.user, self.password, auto_encode=self.auto_encode)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'simple BIND request <%s> sent via <%s>', bind_request_to_dict(request), self)
- response = self.post_send_single_response(self.send('bindRequest', request, controls))
- else:
- response = self.strategy.validate_bind(controls) # only for REUSABLE
- elif self.authentication == SASL:
- if self.sasl_mechanism in SASL_AVAILABLE_MECHANISMS:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'performing SASL BIND for <%s>', self)
- if not self.strategy.pooled:
- response = self.do_sasl_bind(controls)
- else:
- response = self.strategy.validate_bind(controls) # only for REUSABLE
- else:
- self.last_error = 'requested SASL mechanism not supported'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPSASLMechanismNotSupportedError(self.last_error)
- elif self.authentication == NTLM:
- if self.user and self.password and len(self.user.split('\\')) == 2:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'performing NTLM BIND for <%s>', self)
- if not self.strategy.pooled:
- response = self.do_ntlm_bind(controls)
- else:
- response = self.strategy.validate_bind(controls) # only for REUSABLE
- else: # user or password missing
- self.last_error = 'NTLM needs domain\\username and a password'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPUnknownAuthenticationMethodError(self.last_error)
- else:
- self.last_error = 'unknown authentication method'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPUnknownAuthenticationMethodError(self.last_error)
-
- 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
- _, result = self.get_response(response)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'async BIND response id <%s> received via <%s>', result, self)
- elif self.strategy.sync:
- result = self.result
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'BIND response <%s> received via <%s>', result, self)
- elif self.strategy.pooled or self.authentication in (SASL, NTLM): # asynchronous SASL and NTLM or reusable strtegy get the bind result synchronously
- result = response
- else:
- self.last_error = 'unknown authentication method'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPUnknownAuthenticationMethodError(self.last_error)
-
- if result is None:
- # self.bound = True if self.strategy_type == REUSABLE else False
- self.bound = False
- elif result is True:
- self.bound = True
- elif result is False:
- self.bound = False
- else:
- self.bound = True if result['result'] == RESULT_SUCCESS else False
- if not self.bound and result and result['description'] and not self.last_error:
- self.last_error = result['description']
-
- if read_server_info and self.bound:
- self.refresh_server_info()
- self._entries = []
-
- if log_enabled(BASIC):
- log(BASIC, 'done BIND operation, result <%s>', self.bound)
-
- return self.bound
-
- def rebind(self,
- user=None,
- password=None,
- authentication=None,
- sasl_mechanism=None,
- sasl_credentials=None,
- read_server_info=True,
- controls=None
- ):
-
- if log_enabled(BASIC):
- log(BASIC, 'start (RE)BIND operation via <%s>', self)
- self.last_error = None
- with self.connection_lock:
- if user:
- self.user = user
- if password is not None:
- self.password = password
- if not authentication and user:
- self.authentication = SIMPLE
- if authentication in [SIMPLE, ANONYMOUS, SASL, NTLM]:
- self.authentication = authentication
- elif authentication is not None:
- self.last_error = 'unknown authentication method'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPUnknownAuthenticationMethodError(self.last_error)
- if sasl_mechanism:
- self.sasl_mechanism = sasl_mechanism
- if sasl_credentials:
- self.sasl_credentials = sasl_credentials
-
- # if self.authentication == SIMPLE and self.user and self.check_names:
- # self.user = safe_dn(self.user)
- # if log_enabled(EXTENDED):
- # log(EXTENDED, 'user name sanitized to <%s> for rebind via <%s>', self.user, self)
-
- if not self.strategy.pooled:
- try:
- return self.bind(read_server_info, controls)
- except LDAPSocketReceiveError:
- raise LDAPBindError('Unable to rebind as a different user, furthermore the server abruptly closed the connection')
- else:
- self.strategy.pool.rebind_pool()
- return True
-
- def unbind(self,
- controls=None):
- """Unbind the connected user. Unbind implies closing session as per RFC4511 (4.3)
-
- :param controls: LDAP controls to send along with the bind operation
-
- """
- if log_enabled(BASIC):
- log(BASIC, 'start UNBIND operation via <%s>', self)
-
- if self.use_referral_cache:
- self.strategy.unbind_referral_cache()
-
- self.last_error = None
- with self.connection_lock:
- if self.lazy and not self._executing_deferred and (self._deferred_bind or self._deferred_open): # _clear deferred status
- self.strategy.close()
- self._deferred_open = False
- self._deferred_bind = False
- self._deferred_start_tls = False
- elif not self.closed:
- request = unbind_operation()
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'UNBIND request sent via <%s>', self)
- self.send('unbindRequest', request, controls)
- self.strategy.close()
-
- if log_enabled(BASIC):
- log(BASIC, 'done UNBIND operation, result <%s>', True)
-
- return True
-
- def search(self,
- search_base,
- search_filter,
- search_scope=SUBTREE,
- dereference_aliases=DEREF_ALWAYS,
- attributes=None,
- size_limit=0,
- time_limit=0,
- types_only=False,
- get_operational_attributes=False,
- controls=None,
- paged_size=None,
- paged_criticality=False,
- paged_cookie=None,
- auto_escape=None):
- """
- Perform an ldap search:
-
- - If attributes is empty noRFC2696 with the specified size
- - If paged is 0 and cookie is present the search is abandoned on
- server attribute is returned
- - If attributes is ALL_ATTRIBUTES all attributes are returned
- - If paged_size is an int greater than 0 a simple paged search
- is tried as described in
- - Cookie is an opaque string received in the last paged search
- and must be used on the next paged search response
- - If lazy == True open and bind will be deferred until another
- LDAP operation is performed
- - If mssing_attributes == True then an attribute not returned by the server is set to None
- - If auto_escape is set it overrides the Connection auto_escape
- """
- conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')]
- if log_enabled(BASIC):
- log(BASIC, 'start SEARCH operation via <%s>', self)
-
- if self.check_names and search_base:
- search_base = safe_dn(search_base)
- if log_enabled(EXTENDED):
- log(EXTENDED, 'search base sanitized to <%s> for SEARCH operation via <%s>', search_base, self)
-
- with self.connection_lock:
- self._fire_deferred()
- if not attributes:
- attributes = [NO_ATTRIBUTES]
- elif attributes == ALL_ATTRIBUTES:
- attributes = [ALL_ATTRIBUTES]
-
- if isinstance(attributes, STRING_TYPES):
- attributes = [attributes]
-
- if get_operational_attributes and isinstance(attributes, list):
- attributes.append(ALL_OPERATIONAL_ATTRIBUTES)
- elif get_operational_attributes and isinstance(attributes, tuple):
- attributes += (ALL_OPERATIONAL_ATTRIBUTES, ) # concatenate tuple
-
- if isinstance(paged_size, int):
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'performing paged search for %d items with cookie <%s> for <%s>', paged_size, escape_bytes(paged_cookie), self)
-
- if controls is None:
- controls = []
- else:
- # Copy the controls to prevent modifying the original object
- controls = list(controls)
- controls.append(paged_search_control(paged_criticality, paged_size, paged_cookie))
-
- if self.server and self.server.schema and self.check_names:
- for attribute_name in attributes:
- if ';' in attribute_name: # remove tags
- attribute_name_to_check = attribute_name.split(';')[0]
- else:
- attribute_name_to_check = attribute_name
- 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:
- raise LDAPAttributeError('invalid attribute type ' + attribute_name_to_check)
-
- request = search_operation(search_base,
- search_filter,
- search_scope,
- dereference_aliases,
- attributes,
- size_limit,
- time_limit,
- types_only,
- self.auto_escape if auto_escape is None else auto_escape,
- self.auto_encode,
- self.server.schema if self.server else None,
- validator=self.server.custom_validator,
- check_names=self.check_names)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'SEARCH request <%s> sent via <%s>', search_request_to_dict(request), self)
- response = self.post_send_search(self.send('searchRequest', request, controls))
- self._entries = []
-
- if isinstance(response, int): # asynchronous strategy
- return_value = response
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'async SEARCH response id <%s> received via <%s>', return_value, self)
- else:
- return_value = True if self.result['type'] == 'searchResDone' and len(response) > 0 else False
- if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
- self.last_error = self.result['description']
-
- if log_enabled(PROTOCOL):
- for entry in response:
- if entry['type'] == 'searchResEntry':
- log(PROTOCOL, 'SEARCH response entry <%s> received via <%s>', entry, self)
- elif entry['type'] == 'searchResRef':
- log(PROTOCOL, 'SEARCH response reference <%s> received via <%s>', entry, self)
-
- if log_enabled(BASIC):
- log(BASIC, 'done SEARCH operation, result <%s>', return_value)
-
- return return_value
-
- def compare(self,
- dn,
- attribute,
- value,
- controls=None):
- """
- Perform a compare operation
- """
- conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')]
-
- if log_enabled(BASIC):
- log(BASIC, 'start COMPARE operation via <%s>', self)
- self.last_error = None
- if self.check_names:
- dn = safe_dn(dn)
- if log_enabled(EXTENDED):
- log(EXTENDED, 'dn sanitized to <%s> for COMPARE operation via <%s>', dn, self)
-
- if self.server and self.server.schema and self.check_names:
- if ';' in attribute: # remove tags for checking
- attribute_name_to_check = attribute.split(';')[0]
- else:
- attribute_name_to_check = attribute
-
- 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:
- raise LDAPAttributeError('invalid attribute type ' + attribute_name_to_check)
-
- if isinstance(value, SEQUENCE_TYPES): # value can't be a sequence
- raise LDAPInvalidValueError('value cannot be a sequence')
-
- with self.connection_lock:
- self._fire_deferred()
- 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)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'COMPARE request <%s> sent via <%s>', compare_request_to_dict(request), self)
- response = self.post_send_single_response(self.send('compareRequest', request, controls))
- self._entries = []
- if isinstance(response, int):
- return_value = response
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'async COMPARE response id <%s> received via <%s>', return_value, self)
- else:
- return_value = True if self.result['type'] == 'compareResponse' and self.result['result'] == RESULT_COMPARE_TRUE else False
- if not return_value and self.result['result'] not in [RESULT_COMPARE_TRUE, RESULT_COMPARE_FALSE] and not self.last_error:
- self.last_error = self.result['description']
-
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'COMPARE response <%s> received via <%s>', response, self)
-
- if log_enabled(BASIC):
- log(BASIC, 'done COMPARE operation, result <%s>', return_value)
-
- return return_value
-
- def add(self,
- dn,
- object_class=None,
- attributes=None,
- controls=None):
- """
- Add dn to the DIT, object_class is None, a class name or a list
- of class names.
-
- Attributes is a dictionary in the form 'attr': 'val' or 'attr':
- ['val1', 'val2', ...] for multivalued attributes
- """
- conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')]
- conf_classes_excluded_from_check = [v.lower() for v in get_config_parameter('CLASSES_EXCLUDED_FROM_CHECK')]
- if log_enabled(BASIC):
- log(BASIC, 'start ADD operation via <%s>', self)
- self.last_error = None
- _attributes = deepcopy(attributes) # dict could change when adding objectClass values
- if self.check_names:
- dn = safe_dn(dn)
- if log_enabled(EXTENDED):
- log(EXTENDED, 'dn sanitized to <%s> for ADD operation via <%s>', dn, self)
-
- with self.connection_lock:
- self._fire_deferred()
- attr_object_class = []
- if object_class is None:
- parm_object_class = []
- else:
- parm_object_class = list(object_class) if isinstance(object_class, SEQUENCE_TYPES) else [object_class]
-
- object_class_attr_name = ''
- if _attributes:
- for attr in _attributes:
- if attr.lower() == 'objectclass':
- object_class_attr_name = attr
- attr_object_class = list(_attributes[object_class_attr_name]) if isinstance(_attributes[object_class_attr_name], SEQUENCE_TYPES) else [_attributes[object_class_attr_name]]
- break
- else:
- _attributes = dict()
-
- if not object_class_attr_name:
- object_class_attr_name = 'objectClass'
-
- attr_object_class = [to_unicode(object_class) for object_class in attr_object_class] # converts objectclass to unicode in case of bytes value
- _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
-
- if not _attributes[object_class_attr_name]:
- self.last_error = 'objectClass attribute is mandatory'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPObjectClassError(self.last_error)
-
- if self.server and self.server.schema and self.check_names:
- for object_class_name in _attributes[object_class_attr_name]:
- if object_class_name.lower() not in conf_classes_excluded_from_check and object_class_name not in self.server.schema.object_classes:
- raise LDAPObjectClassError('invalid object class ' + str(object_class_name))
-
- for attribute_name in _attributes:
- if ';' in attribute_name: # remove tags for checking
- attribute_name_to_check = attribute_name.split(';')[0]
- else:
- attribute_name_to_check = attribute_name
-
- 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:
- raise LDAPAttributeError('invalid attribute type ' + attribute_name_to_check)
-
- 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)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'ADD request <%s> sent via <%s>', add_request_to_dict(request), self)
- response = self.post_send_single_response(self.send('addRequest', request, controls))
- self._entries = []
-
- if isinstance(response, STRING_TYPES + (int, )):
- return_value = response
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'async ADD response id <%s> received via <%s>', return_value, self)
- else:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'ADD response <%s> received via <%s>', response, self)
- return_value = True if self.result['type'] == 'addResponse' and self.result['result'] == RESULT_SUCCESS else False
- if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
- self.last_error = self.result['description']
-
- if log_enabled(BASIC):
- log(BASIC, 'done ADD operation, result <%s>', return_value)
-
- return return_value
-
- def delete(self,
- dn,
- controls=None):
- """
- Delete the entry identified by the DN from the DIB.
- """
- if log_enabled(BASIC):
- log(BASIC, 'start DELETE operation via <%s>', self)
- self.last_error = None
- if self.check_names:
- dn = safe_dn(dn)
- if log_enabled(EXTENDED):
- log(EXTENDED, 'dn sanitized to <%s> for DELETE operation via <%s>', dn, self)
-
- with self.connection_lock:
- self._fire_deferred()
- if self.read_only:
- self.last_error = 'connection is read-only'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPConnectionIsReadOnlyError(self.last_error)
-
- request = delete_operation(dn)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'DELETE request <%s> sent via <%s>', delete_request_to_dict(request), self)
- response = self.post_send_single_response(self.send('delRequest', request, controls))
- self._entries = []
-
- if isinstance(response, STRING_TYPES + (int, )):
- return_value = response
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'async DELETE response id <%s> received via <%s>', return_value, self)
- else:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'DELETE response <%s> received via <%s>', response, self)
- return_value = True if self.result['type'] == 'delResponse' and self.result['result'] == RESULT_SUCCESS else False
- if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
- self.last_error = self.result['description']
-
- if log_enabled(BASIC):
- log(BASIC, 'done DELETE operation, result <%s>', return_value)
-
- return return_value
-
- def modify(self,
- dn,
- changes,
- controls=None):
- """
- Modify attributes of entry
-
- - changes is a dictionary in the form {'attribute1': change), 'attribute2': [change, change, ...], ...}
- - change is (operation, [value1, value2, ...])
- - operation is 0 (MODIFY_ADD), 1 (MODIFY_DELETE), 2 (MODIFY_REPLACE), 3 (MODIFY_INCREMENT)
- """
- conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')]
-
- if log_enabled(BASIC):
- log(BASIC, 'start MODIFY operation via <%s>', self)
- self.last_error = None
- if self.check_names:
- dn = safe_dn(dn)
- if log_enabled(EXTENDED):
- log(EXTENDED, 'dn sanitized to <%s> for MODIFY operation via <%s>', dn, self)
-
- with self.connection_lock:
- self._fire_deferred()
- if self.read_only:
- self.last_error = 'connection is read-only'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPConnectionIsReadOnlyError(self.last_error)
-
- if not isinstance(changes, dict):
- self.last_error = 'changes must be a dictionary'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPChangeError(self.last_error)
-
- if not changes:
- self.last_error = 'no changes in modify request'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPChangeError(self.last_error)
-
- for attribute_name in changes:
- if self.server and self.server.schema and self.check_names:
- if ';' in attribute_name: # remove tags for checking
- attribute_name_to_check = attribute_name.split(';')[0]
- else:
- attribute_name_to_check = attribute_name
-
- 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:
- raise LDAPAttributeError('invalid attribute type ' + attribute_name_to_check)
- change = changes[attribute_name]
- if isinstance(change, SEQUENCE_TYPES) and change[0] in [MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, MODIFY_INCREMENT, 0, 1, 2, 3]:
- if len(change) != 2:
- self.last_error = 'malformed change'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPChangeError(self.last_error)
-
- changes[attribute_name] = [change] # insert change in a tuple
- else:
- for change_operation in change:
- if len(change_operation) != 2 or change_operation[0] not in [MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, MODIFY_INCREMENT, 0, 1, 2, 3]:
- self.last_error = 'invalid change list'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPChangeError(self.last_error)
- request = modify_operation(dn, changes, 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)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'MODIFY request <%s> sent via <%s>', modify_request_to_dict(request), self)
- response = self.post_send_single_response(self.send('modifyRequest', request, controls))
- self._entries = []
-
- if isinstance(response, STRING_TYPES + (int, )):
- return_value = response
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'async MODIFY response id <%s> received via <%s>', return_value, self)
- else:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'MODIFY response <%s> received via <%s>', response, self)
- return_value = True if self.result['type'] == 'modifyResponse' and self.result['result'] == RESULT_SUCCESS else False
- if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
- self.last_error = self.result['description']
-
- if log_enabled(BASIC):
- log(BASIC, 'done MODIFY operation, result <%s>', return_value)
-
- return return_value
-
- def modify_dn(self,
- dn,
- relative_dn,
- delete_old_dn=True,
- new_superior=None,
- controls=None):
- """
- Modify DN of the entry or performs a move of the entry in the
- DIT.
- """
- if log_enabled(BASIC):
- log(BASIC, 'start MODIFY DN operation via <%s>', self)
- self.last_error = None
- if self.check_names:
- dn = safe_dn(dn)
- if log_enabled(EXTENDED):
- log(EXTENDED, 'dn sanitized to <%s> for MODIFY DN operation via <%s>', dn, self)
- relative_dn = safe_dn(relative_dn)
- if log_enabled(EXTENDED):
- log(EXTENDED, 'relative dn sanitized to <%s> for MODIFY DN operation via <%s>', relative_dn, self)
-
- with self.connection_lock:
- self._fire_deferred()
- if self.read_only:
- self.last_error = 'connection is read-only'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPConnectionIsReadOnlyError(self.last_error)
-
- if new_superior and not dn.startswith(relative_dn): # as per RFC4511 (4.9)
- self.last_error = 'DN cannot change while performing moving'
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
- raise LDAPChangeError(self.last_error)
-
- request = modify_dn_operation(dn, relative_dn, delete_old_dn, new_superior)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'MODIFY DN request <%s> sent via <%s>', modify_dn_request_to_dict(request), self)
- response = self.post_send_single_response(self.send('modDNRequest', request, controls))
- self._entries = []
-
- if isinstance(response, STRING_TYPES + (int, )):
- return_value = response
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'async MODIFY DN response id <%s> received via <%s>', return_value, self)
- else:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'MODIFY DN response <%s> received via <%s>', response, self)
- return_value = True if self.result['type'] == 'modDNResponse' and self.result['result'] == RESULT_SUCCESS else False
- if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
- self.last_error = self.result['description']
-
- if log_enabled(BASIC):
- log(BASIC, 'done MODIFY DN operation, result <%s>', return_value)
-
- return return_value
-
- def abandon(self,
- message_id,
- controls=None):
- """
- Abandon the operation indicated by message_id
- """
- if log_enabled(BASIC):
- log(BASIC, 'start ABANDON operation via <%s>', self)
- self.last_error = None
- with self.connection_lock:
- self._fire_deferred()
- return_value = False
- if self.strategy._outstanding or message_id == 0:
- # only current operation should be abandoned, abandon, bind and unbind cannot ever be abandoned,
- # messagiId 0 is invalid and should be used as a "ping" to keep alive the connection
- 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:
- request = abandon_operation(message_id)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'ABANDON request: <%s> sent via <%s>', abandon_request_to_dict(request), self)
- self.send('abandonRequest', request, controls)
- self.result = None
- self.response = None
- self._entries = []
- return_value = True
- else:
- if log_enabled(ERROR):
- log(ERROR, 'cannot abandon a Bind, an Unbind or an Abandon operation or message ID %s not found via <%s>', str(message_id), self)
-
- if log_enabled(BASIC):
- log(BASIC, 'done ABANDON operation, result <%s>', return_value)
-
- return return_value
-
- def extended(self,
- request_name,
- request_value=None,
- controls=None,
- no_encode=None):
- """
- Performs an extended operation
- """
- if log_enabled(BASIC):
- log(BASIC, 'start EXTENDED operation via <%s>', self)
- self.last_error = None
- with self.connection_lock:
- self._fire_deferred()
- request = extended_operation(request_name, request_value, no_encode=no_encode)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'EXTENDED request <%s> sent via <%s>', extended_request_to_dict(request), self)
- response = self.post_send_single_response(self.send('extendedReq', request, controls))
- self._entries = []
- if isinstance(response, int):
- return_value = response
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'async EXTENDED response id <%s> received via <%s>', return_value, self)
- else:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'EXTENDED response <%s> received via <%s>', response, self)
- return_value = True if self.result['type'] == 'extendedResp' and self.result['result'] == RESULT_SUCCESS else False
- if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
- self.last_error = self.result['description']
-
- if log_enabled(BASIC):
- log(BASIC, 'done EXTENDED operation, result <%s>', return_value)
-
- return return_value
-
- 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
-
- if log_enabled(BASIC):
- log(BASIC, 'start START TLS operation via <%s>', self)
-
- with self.connection_lock:
- return_value = False
- if not self.server.tls:
- self.server.tls = Tls()
-
- if self.lazy and not self._executing_deferred:
- self._deferred_start_tls = True
- self.tls_started = True
- return_value = True
- if log_enabled(BASIC):
- log(BASIC, 'deferring START TLS for <%s>', self)
- else:
- self._deferred_start_tls = False
- if self.server.tls.start_tls(self) and self.strategy.sync: # for asynchronous connections _start_tls is run by the strategy
- if read_server_info:
- self.refresh_server_info() # refresh server info as per RFC4515 (3.1.5)
- return_value = True
- elif not self.strategy.sync:
- return_value = True
-
- if log_enabled(BASIC):
- log(BASIC, 'done START TLS operation, result <%s>', return_value)
-
- return return_value
-
- def do_sasl_bind(self,
- controls):
- if log_enabled(BASIC):
- log(BASIC, 'start SASL BIND operation via <%s>', self)
- self.last_error = None
- with self.connection_lock:
- result = None
-
- if not self.sasl_in_progress:
- self.sasl_in_progress = True
- try:
- if self.sasl_mechanism == EXTERNAL:
- result = sasl_external(self, controls)
- elif self.sasl_mechanism == DIGEST_MD5:
- result = sasl_digest_md5(self, controls)
- elif self.sasl_mechanism == GSSAPI:
- from ..protocol.sasl.kerberos import sasl_gssapi # needs the gssapi package
- result = sasl_gssapi(self, controls)
- elif self.sasl_mechanism == 'PLAIN':
- result = sasl_plain(self, controls)
- finally:
- self.sasl_in_progress = False
-
- if log_enabled(BASIC):
- log(BASIC, 'done SASL BIND operation, result <%s>', result)
-
- return result
-
- def do_ntlm_bind(self,
- controls):
- if log_enabled(BASIC):
- log(BASIC, 'start NTLM BIND operation via <%s>', self)
- self.last_error = None
- with self.connection_lock:
- result = None
- if not self.sasl_in_progress:
- self.sasl_in_progress = True # ntlm is same of sasl authentication
- # additional import for NTLM
- from ..utils.ntlm import NtlmClient
- domain_name, user_name = self.user.split('\\', 1)
- ntlm_client = NtlmClient(user_name=user_name, domain=domain_name, password=self.password)
-
- # as per https://msdn.microsoft.com/en-us/library/cc223501.aspx
- # send a sicilyPackageDiscovery request (in the bindRequest)
- request = bind_operation(self.version, 'SICILY_PACKAGE_DISCOVERY', ntlm_client)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'NTLM SICILY PACKAGE DISCOVERY request sent via <%s>', self)
- response = self.post_send_single_response(self.send('bindRequest', request, controls))
- if not self.strategy.sync:
- _, result = self.get_response(response)
- else:
- result = response[0]
- if 'server_creds' in result:
- sicily_packages = result['server_creds'].decode('ascii').split(';')
- if 'NTLM' in sicily_packages: # NTLM available on server
- request = bind_operation(self.version, 'SICILY_NEGOTIATE_NTLM', ntlm_client)
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'NTLM SICILY NEGOTIATE request sent via <%s>', self)
- response = self.post_send_single_response(self.send('bindRequest', request, controls))
- if not self.strategy.sync:
- _, result = self.get_response(response)
- else:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'NTLM SICILY NEGOTIATE response <%s> received via <%s>', response[0], self)
- result = response[0]
-
- if result['result'] == RESULT_SUCCESS:
- request = bind_operation(self.version, 'SICILY_RESPONSE_NTLM', ntlm_client, result['server_creds'])
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'NTLM SICILY RESPONSE NTLM request sent via <%s>', self)
- response = self.post_send_single_response(self.send('bindRequest', request, controls))
- if not self.strategy.sync:
- _, result = self.get_response(response)
- else:
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'NTLM BIND response <%s> received via <%s>', response[0], self)
- result = response[0]
- else:
- result = None
- self.sasl_in_progress = False
-
- if log_enabled(BASIC):
- log(BASIC, 'done SASL NTLM operation, result <%s>', result)
-
- return result
-
- def refresh_server_info(self):
- # if self.strategy.no_real_dsa: # do not refresh for mock strategies
- # return
-
- if not self.strategy.pooled:
- with self.connection_lock:
- if not self.closed:
- if log_enabled(BASIC):
- log(BASIC, 'refreshing server info for <%s>', self)
- previous_response = self.response
- previous_result = self.result
- previous_entries = self._entries
- self.server.get_info_from_server(self)
- self.response = previous_response
- self.result = previous_result
- self._entries = previous_entries
- else:
- if log_enabled(BASIC):
- log(BASIC, 'refreshing server info from pool for <%s>', self)
- self.strategy.pool.get_info_from_server()
-
- def response_to_ldif(self,
- search_result=None,
- all_base64=False,
- line_separator=None,
- sort_order=None,
- stream=None):
- with self.connection_lock:
- if search_result is None:
- search_result = self.response
-
- if isinstance(search_result, SEQUENCE_TYPES):
- ldif_lines = operation_to_ldif('searchResponse', search_result, all_base64, sort_order=sort_order)
- ldif_lines = add_ldif_header(ldif_lines)
- line_separator = line_separator or linesep
- ldif_output = line_separator.join(ldif_lines)
- if stream:
- if stream.tell() == 0:
- header = add_ldif_header(['-'])[0]
- stream.write(prepare_for_stream(header + line_separator + line_separator))
- stream.write(prepare_for_stream(ldif_output + line_separator + line_separator))
- if log_enabled(BASIC):
- log(BASIC, 'building LDIF output <%s> for <%s>', ldif_output, self)
- return ldif_output
-
- return None
-
- def response_to_json(self,
- raw=False,
- search_result=None,
- indent=4,
- sort=True,
- stream=None,
- checked_attributes=True,
- include_empty=True):
-
- with self.connection_lock:
- if search_result is None:
- search_result = self.response
-
- if isinstance(search_result, SEQUENCE_TYPES):
- json_dict = dict()
- json_dict['entries'] = []
-
- for response in search_result:
- if response['type'] == 'searchResEntry':
- entry = dict()
-
- entry['dn'] = response['dn']
- if checked_attributes:
- if not include_empty:
- # needed for python 2.6 compatibility
- entry['attributes'] = dict((key, response['attributes'][key]) for key in response['attributes'] if response['attributes'][key])
- else:
- entry['attributes'] = dict(response['attributes'])
- if raw:
- if not include_empty:
- # needed for python 2.6 compatibility
- entry['raw_attributes'] = dict((key, response['raw_attributes'][key]) for key in response['raw_attributes'] if response['raw:attributes'][key])
- else:
- entry['raw'] = dict(response['raw_attributes'])
- json_dict['entries'].append(entry)
-
- if str is bytes: # Python 2
- check_json_dict(json_dict)
-
- json_output = json.dumps(json_dict, ensure_ascii=True, sort_keys=sort, indent=indent, check_circular=True, default=format_json, separators=(',', ': '))
-
- if log_enabled(BASIC):
- log(BASIC, 'building JSON output <%s> for <%s>', json_output, self)
- if stream:
- stream.write(json_output)
-
- return json_output
-
- def response_to_file(self,
- target,
- raw=False,
- indent=4,
- sort=True):
- with self.connection_lock:
- if self.response:
- if isinstance(target, STRING_TYPES):
- target = open(target, 'w+')
-
- if log_enabled(BASIC):
- log(BASIC, 'writing response to file for <%s>', self)
-
- target.writelines(self.response_to_json(raw=raw, indent=indent, sort=sort))
- target.close()
-
- def _fire_deferred(self, read_info=True):
- with self.connection_lock:
- if self.lazy and not self._executing_deferred:
- self._executing_deferred = True
-
- if log_enabled(BASIC):
- log(BASIC, 'executing deferred (open: %s, start_tls: %s, bind: %s) for <%s>', self._deferred_open, self._deferred_start_tls, self._deferred_bind, self)
- try:
- if self._deferred_open:
- self.open(read_server_info=False)
- if self._deferred_start_tls:
- self.start_tls(read_server_info=False)
- if self._deferred_bind:
- self.bind(read_server_info=False, controls=self._bind_controls)
- if read_info:
- self.refresh_server_info()
- except LDAPExceptionError as e:
- if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', e, self)
- raise # re-raise LDAPExceptionError
- finally:
- self._executing_deferred = False
-
- @property
- def entries(self):
- if self.response:
- if not self._entries:
- self._entries = self._get_entries(self.response)
- return self._entries
-
- def _get_entries(self, search_response):
- with self.connection_lock:
- from .. import ObjectDef, Reader
-
- # build a table of ObjectDefs, grouping the entries found in search_response for their attributes set, subset will be included in superset
- attr_sets = []
- for response in search_response:
- if response['type'] == 'searchResEntry':
- resp_attr_set = set(response['attributes'].keys())
- if resp_attr_set not in attr_sets:
- attr_sets.append(resp_attr_set)
- attr_sets.sort(key=lambda x: -len(x)) # sorts the list in descending length order
- unique_attr_sets = []
- for attr_set in attr_sets:
- for unique_set in unique_attr_sets:
- if unique_set >= attr_set: # checks if unique set is a superset of attr_set
- break
- else: # the attr_set is not a subset of any element in unique_attr_sets
- unique_attr_sets.append(attr_set)
- object_defs = []
- for attr_set in unique_attr_sets:
- object_def = ObjectDef(schema=self.server.schema)
- object_def += list(attr_set) # converts the set in a list to be added to the object definition
- object_defs.append((attr_set,
- object_def,
- Reader(self, object_def, self.request['base'], self.request['filter'], attributes=attr_set) if self.strategy.sync else Reader(self, object_def, '', '', attributes=attr_set))
- ) # objects_defs contains a tuple with the set, the ObjectDef and a cursor
-
- entries = []
- for response in search_response:
- if response['type'] == 'searchResEntry':
- resp_attr_set = set(response['attributes'].keys())
- for object_def in object_defs:
- if resp_attr_set <= object_def[0]: # finds the ObjectDef for the attribute set of this entry
- entry = object_def[2]._create_entry(response)
- entries.append(entry)
- break
- else:
- if log_enabled(ERROR):
- log(ERROR, 'attribute set not found for %s in <%s>', resp_attr_set, self)
- raise LDAPObjectError('attribute set not found for ' + str(resp_attr_set))
-
- return entries
|