From 2aee1896cc6a884e7ae141bb45cb0a9374bec8dc Mon Sep 17 00:00:00 2001 From: cannatag Date: Sun, 15 Nov 2015 23:26:00 +0100 Subject: [PATCH] v 0.9.9.3 --- CHANGES.txt | 10 +++-- _changelog.txt | 9 +++-- docs/manual/source/bind.rst | 22 ++++++++++- docs/manual/source/features.rst | 6 +++ ldap3/core/connection.py | 9 +++++ ldap3/core/server.py | 36 ++++++++++-------- ldap3/strategy/base.py | 65 ++++++++++++++++----------------- ldap3/version.py | 4 +- test/testBindOperation.py | 11 +++++- 9 files changed, 113 insertions(+), 59 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c1b3f1c33..36b34f9d3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,12 +1,16 @@ # changes file for ldap3 -# generated on 2015-11-11 19:35:51.384492 +# generated on 2015-11-15 23:24:19.725251 # version 0.9.9.3 -* 0.9.9.3 2015.11.08 +* 0.9.9.3 2015.11.15 + - Added LDAPI (LDAP over IPC) support for unix socket communication - Added mandatory_in and optional_in in server schema for attribute types. Now you can see in which classes attributes are used - Added last_transmitted_time and last_received_time to Usage object to track time of the last sent and received operation - Exception SessionTerminatedByServer renamed to SessionTerminatedByServerError and added to ldap3 namespace - - added get_config_parameter() in ldap3 to read the current value of parameters modified in ldap3 namespace + - Added get_config_parameter() in ldap3 to read the current value of parameters modified in the ldap3 namespace + - Added SASL mechanism name as constants in the ldap3 namespace + - Added escape_filter_chars in utils.conv (thanks Peter) + - reverted ALL_ATTRIBUTES behaviour in search to 0.9.9.1 (thanks Petros) * 0.9.9.2 2015.10.19 - Fixed hasattr() behaviour for Entry object in Python 3 diff --git a/_changelog.txt b/_changelog.txt index 369b9e541..0a3e22b88 100644 --- a/_changelog.txt +++ b/_changelog.txt @@ -1,9 +1,12 @@ -* 0.9.9.3 2015.11.08 +* 0.9.9.3 2015.11.15 + - Added LDAPI (LDAP over IPC) support for unix socket communication - Added mandatory_in and optional_in in server schema for attribute types. Now you can see in which classes attributes are used - Added last_transmitted_time and last_received_time to Usage object to track time of the last sent and received operation - Exception SessionTerminatedByServer renamed to SessionTerminatedByServerError and added to ldap3 namespace - - added get_config_parameter() in ldap3 to read the current value of parameters modified in ldap3 namespace - - added SASL mechanism name as constants in init + - Added get_config_parameter() in ldap3 to read the current value of parameters modified in the ldap3 namespace + - Added SASL mechanism name as constants in the ldap3 namespace + - Added escape_filter_chars in utils.conv (thanks Peter) + - reverted ALL_ATTRIBUTES behaviour in search to 0.9.9.1 (thanks Petros) * 0.9.9.2 2015.10.19 - Fixed hasattr() behaviour for Entry object in Python 3 diff --git a/docs/manual/source/bind.rst b/docs/manual/source/bind.rst index 4b14294e2..3e2052636 100644 --- a/docs/manual/source/bind.rst +++ b/docs/manual/source/bind.rst @@ -205,7 +205,7 @@ By default the library attempts to bind against the service principal for the do NTLM ---- -The ldap3 library support an additional method to bind to Active Directory servers via the NTLM method:: +The ldap3 library supports an additional method to bind to Active Directory servers via the NTLM method:: # import class and constants from ldap3 import Server, Connection, SIMPLE, SYNC, ALL, SASL, NTLM) @@ -221,6 +221,26 @@ This authentication method is specific for Active Directory and uses a proprieta that breaks the LDAP RFC but can be used to access AD. +LDAPI (LDAP over IPC) +--------------------- + +If your LDAP server provides a UNIX socket connection (*Interprocess Communication*) you can use the **ldapi:** schema to access it from the +same machine:: + + >>> # accessing OpenLDAP server in a root user session + >>> s = Server('ldapi:///var/run/slapd/ldapi') + >>> c = Connection(s, authentication=SASL, sasl_mechanism=EXTERNAL, sasl_credentials='') + >>> c.bind() + >>> True + >>> c.extend.standard.who_am_i() + >>> dn:cn=config + +Using the SASL *EXTERNAL* mechanism allows you to provide to the server the credentials of the logged user. + +While accessing your LDAP server via a UNIX socket you can perform any usual LDAP operation. This should be faster than using a TCP connection. +You don't need to use SSL when connecting via a socket because all the communication is in the server memory and is not exposed on the wire. + + Extended logging ---------------- To get an idea of what's happening when you perform a Simple Bind operation using the StartTLS security feature this is diff --git a/docs/manual/source/features.rst b/docs/manual/source/features.rst index a0c50ad6b..d3b2557ae 100644 --- a/docs/manual/source/features.rst +++ b/docs/manual/source/features.rst @@ -71,3 +71,9 @@ ldap3 Features 6. Simplified query construction language: * The library includes an optional **abstraction layer** for performing LDAP queries. + +7. Clear or secured access + + * ldap3 allows plaintext (**ldap:**), secure (**ldaps:**) and UNIX socket (**ldapi:**) access to the LDAP server. + + * The NTLM access method is available to connect to Active Directory servers diff --git a/ldap3/core/connection.py b/ldap3/core/connection.py index 632899778..78dd40c8a 100644 --- a/ldap3/core/connection.py +++ b/ldap3/core/connection.py @@ -77,6 +77,11 @@ def _format_socket_endpoint(endpoint): 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: @@ -595,8 +600,12 @@ def search(self, 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) + # real_search_control_value = RealSearchControlValue() + # real_search_control_value['size'] = Size(paged_size) + # real_search_control_value['cookie'] = Cookie(paged_cookie) if paged_cookie else Cookie('') if controls is None: controls = [] + # controls.append(('1.2.840.113556.1.4.319', paged_criticality if isinstance(paged_criticality, bool) else False, encoder.encode(real_search_control_value))) controls.append(paged_search_control(paged_criticality, paged_size, paged_cookie)) request = search_operation(search_base, search_filter, search_scope, dereference_aliases, attributes, size_limit, time_limit, types_only, self.server.schema if self.server else None) diff --git a/ldap3/core/server.py b/ldap3/core/server.py index 97e5ced46..b34d71802 100644 --- a/ldap3/core/server.py +++ b/ldap3/core/server.py @@ -36,13 +36,17 @@ from .tls import Tls from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL +try: + from urllib.parse import unquote +except ImportError: + from urllib import unquote + try: from socket import AF_UNIX unix_socket_available = True except ImportError: unix_socket_available = False - class Server(object): """ LDAP Server definition class @@ -74,7 +78,7 @@ def __init__(self, connect_timeout=None, mode=IP_V6_PREFERRED): - self.is_unix_socket = False + self.ipc = False url_given = False if host.lower().startswith('ldap://'): self.host = host[7:] @@ -85,8 +89,7 @@ def __init__(self, use_ssl = True url_given = True elif host.lower().startswith('ldapi://') and unix_socket_available: - self.host = host[8:] - self.is_unix_socket = True + self.ipc = True use_ssl = False url_given = True elif host.lower().startswith('ldapi://') and not unix_socket_available: @@ -94,8 +97,11 @@ def __init__(self, else: self.host = host - if self.is_unix_socket: - self.host = None + if self.ipc: + if str == bytes: # Python 2 + self.host = unquote(host[7:]).decode('utf-8') + else: + self.host = unquote(host[7:], encoding='utf-8') self.port = None elif ':' in self.host and self.host.count(':') == 1: hostname, _, hostport = self.host.partition(':') @@ -130,7 +136,7 @@ def __init__(self, log(ERROR, 'invalid server address for <%s>', self.host) raise LDAPInvalidServerError() - if not self.is_unix_socket: + if not self.ipc: self.host.rstrip('/') if not use_ssl and not port: port = 389 @@ -169,13 +175,13 @@ def __init__(self, self.tls = Tls() if self.ssl and not tls else tls - if not self.is_unix_socket: + if not self.ipc: if self._is_ipv6(self.host): self.name = ('ldaps' if self.ssl else 'ldap') + '://[' + self.host + ']:' + str(self.port) else: self.name = ('ldaps' if self.ssl else 'ldap') + '://' + self.host + ':' + str(self.port) else: - self.name = self.host + self.name = host self.get_info = get_info self._dsa_info = None @@ -201,7 +207,7 @@ def _is_ipv6(host): def __str__(self): if self.host: - s = self.name + (' - ssl' if self.ssl else ' - cleartext') + (' - unix socket' if self.is_unix_socket else '') + s = self.name + (' - ssl' if self.ssl else ' - cleartext') + (' - unix socket' if self.ipc else '') else: s = object.__str__(self) return s @@ -221,8 +227,8 @@ def address_info(self): # converts addresses tuple to list and adds a 6th parameter for availability (None = not checked, True = available, False=not available) and a 7th parameter for the checking time addresses = None try: - if self.is_unix_socket: - addresses = socket.getaddrinfo(self.host, self.port, socket.AF_UNIX, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED) + if self.ipc: + addresses = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, None, self.host, None)] else: addresses = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED) except (socket.gaierror, AttributeError): @@ -487,10 +493,10 @@ def from_definition(host, dsa_info, dsa_schema, port=None, use_ssl=False, format return dummy def candidate_addresses(self): - if self.is_unix_socket: - candidates = [socket.AF_UNIX, socket.SOCK_STREAM, socket.IPPROTO_TCP, self.host, None, None] + if self.ipc: + candidates = self.address_info if log_enabled(BASIC): - log(BASIC, 'candidate address for <%s>: <%s> with mode UNIX_SOCKET', self, self.host) + log(BASIC, 'candidate address for <%s>: <%s> with mode UNIX_SOCKET', self, self.name) else: # selects server address based on server mode and availability (in address[5]) addresses = self.address_info[:] # copy to avoid refreshing while searching candidates diff --git a/ldap3/strategy/base.py b/ldap3/strategy/base.py index b2b02b5b2..226702f1e 100644 --- a/ldap3/strategy/base.py +++ b/ldap3/strategy/base.py @@ -117,7 +117,7 @@ def open(self, reset_usage=True, read_server_info=True): try: if log_enabled(BASIC): log(BASIC, 'try to open candidate address %s', candidate_address[:-2]) - self._open_socket(candidate_address, self.connection.server.ssl, unix_socket=self.connection.server.is_unix_socket) + self._open_socket(candidate_address, self.connection.server.ssl, unix_socket=self.connection.server.ipc) self.connection.server.current_address = candidate_address self.connection.server.update_availability(candidate_address, True) break @@ -181,29 +181,40 @@ def _open_socket(self, address, use_ssl=False, unix_socket=False): raise LDAPExceptionError if unable to open or connect socket """ exc = None - if unix_socket: - raise NotImplementedError ('ldapi still not imolemented') - else: - try: - self.connection.socket = socket.socket(*address[:3]) - except Exception as e: - self.connection.last_error = 'socket creation error: ' + str(e) - exc = e + try: + self.connection.socket = socket.socket(*address[:3]) + except Exception as e: + self.connection.last_error = 'socket creation error: ' + str(e) + exc = e - if exc: - if log_enabled(ERROR): - log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) + if exc: + if log_enabled(ERROR): + log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) - raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error) + raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error) + try: + if self.connection.server.connect_timeout: + self.connection.socket.settimeout(self.connection.server.connect_timeout) + self.connection.socket.connect(address[4]) + if self.connection.server.connect_timeout: + self.connection.socket.settimeout(None) # disable socket timeout - socket is in blocking mode + except socket.error as e: + self.connection.last_error = 'socket connection error: ' + str(e) + exc = e + + if exc: + if log_enabled(ERROR): + log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) + raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error) + + if use_ssl: try: - if self.connection.server.connect_timeout: - self.connection.socket.settimeout(self.connection.server.connect_timeout) - self.connection.socket.connect(address[4]) - if self.connection.server.connect_timeout: - self.connection.socket.settimeout(None) # disable socket timeout - socket is in blocking mode - except socket.error as e: - self.connection.last_error = 'socket connection error: ' + str(e) + self.connection.server.tls.wrap_socket(self.connection, do_handshake=True) + if self.connection.usage: + self.connection._usage.wrapped_sockets += 1 + except Exception as e: + self.connection.last_error = 'socket ssl wrapping error: ' + str(e) exc = e if exc: @@ -211,20 +222,6 @@ def _open_socket(self, address, use_ssl=False, unix_socket=False): log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error) - if use_ssl: - try: - self.connection.server.tls.wrap_socket(self.connection, do_handshake=True) - if self.connection.usage: - self.connection._usage.wrapped_sockets += 1 - except Exception as e: - self.connection.last_error = 'socket ssl wrapping error: ' + str(e) - exc = e - - if exc: - if log_enabled(ERROR): - log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) - raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error) - if self.connection.usage: self.connection._usage.open_sockets += 1 diff --git a/ldap3/version.py b/ldap3/version.py index f411e20a1..80cd13c39 100644 --- a/ldap3/version.py +++ b/ldap3/version.py @@ -1,6 +1,6 @@ # THIS FILE IS AUTO-GENERATED. PLEASE DO NOT MODIFY# version file for ldap3 -# generated on 2015-11-11 19:35:51.384492 -# on system uname_result(system='Windows', node='GCW89227', release='post2012Server', version='6.3.9600', machine='AMD64', processor='Intel64 Family 6 Model 45 Stepping 7, GenuineIntel') +# generated on 2015-11-15 23:24:19.718253 +# on system uname_result(system='Windows', node='GCNBHPW8', release='post2012Server', version='6.3.9600', machine='AMD64', processor='Intel64 Family 6 Model 58 Stepping 9, GenuineIntel') # with Python 3.5.0 - ('v3.5.0:374f501f4567', 'Sep 13 2015 02:27:37') - MSC v.1900 64 bit (AMD64) # __version__ = '0.9.9.3' diff --git a/test/testBindOperation.py b/test/testBindOperation.py index d8e08fa2d..a76204f8b 100644 --- a/test/testBindOperation.py +++ b/test/testBindOperation.py @@ -75,6 +75,15 @@ def test_ntlm(self): def test_ldapi(self): if test_server_type == 'SLAPD': server = Server('ldapi:///var/run/slapd/ldapi') - connection = Connection(server, user=test_user, password=test_password, authentication=SASL, sasl_mechanism=EXTERNAL) + connection = Connection(server, authentication=SASL, sasl_mechanism=EXTERNAL, sasl_credentials='') connection.open() + connection.bind() + self.assertTrue(connection.bound) + + def test_ldapi_encoded_url(self): + if test_server_type == 'SLAPD': + server = Server('ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi') + connection = Connection(server, authentication=SASL, sasl_mechanism=EXTERNAL, sasl_credentials='') + connection.open() + connection.bind() self.assertTrue(connection.bound)