├── multildap ├── __init__.py ├── satosa │ ├── multiple_ldap_attribute_store.yaml.example │ └── multiple_ldap_attribute_store.py ├── decorators.py ├── attr_rewrite.py ├── commands.py ├── client.py └── multildapd.py ├── requirements.txt ├── AUTHORS ├── .gitignore ├── examples ├── ldaptor │ ├── requirements │ └── ldap-merger.py ├── README.asyncio.md ├── ldap_gevent.py ├── ldap_gevent_server_echo.py ├── paged_resultset_example.py ├── ldap_aio.py └── settings.py.example ├── LICENSE ├── setup.py ├── tests └── run_test.py └── README.md /multildap/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ldap3>=2.9 2 | gevent>=21.1.2 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Giuseppe De Marco 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | settings.py 2 | *__pycache__/* 3 | *.pyc 4 | build/* 5 | dist/* 6 | *egg-info/* 7 | -------------------------------------------------------------------------------- /examples/ldaptor/requirements: -------------------------------------------------------------------------------- 1 | ldaptor 2 | passlib 3 | pyparsing 4 | twisted[tls] 5 | zope.interface 6 | -------------------------------------------------------------------------------- /examples/README.asyncio.md: -------------------------------------------------------------------------------- 1 | - https://github.com/hughrobb/aioldap3 2 | - https://github.com/cannatag/ldap3/issues/182 3 | -------------------------------------------------------------------------------- /multildap/satosa/multiple_ldap_attribute_store.yaml.example: -------------------------------------------------------------------------------- 1 | module: multildap.satosa.multiple_ldap_attribute_store.MultiLdapAttributeStore 2 | name: MultipleLdapAttributeStore 3 | config: 4 | settings_path: /opt/multildap/settings.py 5 | unique_attribute_to_match: schacpersonaluniqueid 6 | ldap_filter_operator: ':caseIgnoreMatch:=' 7 | -------------------------------------------------------------------------------- /examples/ldap_gevent.py: -------------------------------------------------------------------------------- 1 | import gevent 2 | from gevent import monkey; monkey.patch_all() 3 | 4 | from client import LdapClient 5 | from settings import LDAP_CONNECTIONS 6 | 7 | result_set = [] 8 | LDAP_SERVERS = [LdapClient(conf) for conf in LDAP_CONNECTIONS.values()] 9 | 10 | jobs = [gevent.spawn(conn.get) for conn in LDAP_SERVERS] 11 | gevent.joinall(jobs, timeout=2) 12 | for job in jobs: 13 | print(job.value) 14 | -------------------------------------------------------------------------------- /examples/ldaptor/ldap-merger.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from twisted.application import service, internet 4 | from twisted.internet import protocol 5 | from ldaptor.config import LDAPConfig 6 | from ldaptor.protocols.ldap.merger import MergedLDAPServer 7 | 8 | application = service.Application("LDAP Merger") 9 | 10 | configs = [LDAPConfig(serviceLocationOverrides={"": ('host1.unical.it', 389)}), 11 | LDAPConfig(serviceLocationOverrides={"": ('host2.unical.it', 636)}) 12 | ] 13 | use_tls = [True, True] 14 | factory = protocol.ServerFactory() 15 | factory.protocol = lambda: MergedLDAPServer(configs, use_tls) 16 | mergeService = internet.TCPServer(3899, factory) 17 | mergeService.setServiceParent(application) 18 | -------------------------------------------------------------------------------- /multildap/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import errno 3 | import os 4 | import signal 5 | 6 | 7 | class TimeoutError(Exception): 8 | pass 9 | 10 | def timeout(seconds=5, error_message=os.strerror(errno.ETIME)): 11 | def decorator(func): 12 | def _handle_timeout(signum, frame): 13 | raise TimeoutError(error_message) 14 | 15 | def wrapper(*args, **kwargs): 16 | signal.signal(signal.SIGALRM, _handle_timeout) 17 | signal.alarm(seconds) 18 | try: 19 | result = func(*args, **kwargs) 20 | finally: 21 | signal.alarm(0) 22 | return result 23 | 24 | return wraps(func)(wrapper) 25 | 26 | return decorator 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | 5 | Copyright (C) 2019 Giuseppe De Marco 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU Affero General Public License as 9 | published by the Free Software Foundation, either version 3 of the 10 | License, or (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU Affero General Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero General Public License 18 | along with this program. If not, see . 19 | -------------------------------------------------------------------------------- /examples/ldap_gevent_server_echo.py: -------------------------------------------------------------------------------- 1 | #import gevent 2 | 3 | #from gevent import monkey; monkey.patch_all() 4 | from gevent.server import StreamServer 5 | 6 | from client import LdapClient 7 | from settings import LDAP_CONNECTIONS 8 | 9 | 10 | def handle(socket, address): 11 | print('new connection from {}'.format(address)) 12 | rfileobj = socket.makefile(mode='rb') 13 | while True: 14 | line = rfileobj.readline() 15 | if not line: 16 | print("client {} disconnected".format(address)) 17 | break 18 | elif line.strip().lower() == b'quit': 19 | print("client {} quit".format(address)) 20 | break 21 | elif line == b'\n': continue 22 | else: 23 | socket.sendall(b'recv: '+line) 24 | print("< {}".format(line)) 25 | rfileobj.close() 26 | 27 | server = StreamServer(('127.0.0.1', 1234), handle) # creates a new server 28 | server.serve_forever() # start accepting new connections 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | _name = 'multildap' 4 | 5 | def readme(): 6 | with open('README.md') as f: 7 | return f.read() 8 | 9 | setup(name=_name, 10 | version='0.3.3', 11 | zip_safe = False, 12 | description="LDAP client or proxy to multiple LDAP server", 13 | long_description=readme(), 14 | long_description_content_type="text/markdown", 15 | classifiers=['Development Status :: 5 - Production/Stable', 16 | 'License :: OSI Approved :: BSD License', 17 | 'Programming Language :: Python :: 2', 18 | 'Programming Language :: Python :: 3'], 19 | url='https://github.com/peppelinux/pyMultiLDAP', 20 | author='Giuseppe De Marco', 21 | author_email='giuseppe.demarco@unical.it', 22 | license='BSD', 23 | scripts=['{}/multildapd.py'.format(_name)], 24 | packages=[_name, 'multildap/satosa'], 25 | install_requires=['ldap3', 'gevent'], 26 | ) 27 | -------------------------------------------------------------------------------- /examples/paged_resultset_example.py: -------------------------------------------------------------------------------- 1 | from ldap3 import Server, Connection, SUBTREE 2 | from settings import SAMVICE 3 | 4 | total_entries = 0 5 | server = ldap3.Server(SAMVICE['url']) 6 | c = ldap3.Connection(server, **SAMVICE['connection']) 7 | 8 | c.search(search_base = 'o=test', 9 | search_filter = '(objectClass=inetOrgPerson)', 10 | search_scope = SUBTREE, 11 | attributes = ['cn', 'givenName'], 12 | paged_size = 5) 13 | total_entries += len(c.response) 14 | for entry in c.response: 15 | print(entry['dn'], entry['attributes']) 16 | cookie = c.result['controls']['1.2.840.113556.1.4.319']['value']['cookie'] 17 | while cookie: 18 | c.search(search_base = 'o=test', 19 | search_filter = '(object_class=inetOrgPerson)', 20 | search_scope = SUBTREE, 21 | attributes = ['cn', 'givenName'], 22 | paged_size = 5, 23 | paged_cookie = cookie) 24 | total_entries += len(c.response) 25 | cookie = c.result['controls']['1.2.840.113556.1.4.319']['value']['cookie'] 26 | for entry in c.response: 27 | print(entry['dn'], entry['attributes']) 28 | print('Total entries retrieved:', total_entries) 29 | -------------------------------------------------------------------------------- /examples/ldap_aio.py: -------------------------------------------------------------------------------- 1 | # https://www.pythonsheets.com/notes/python-asyncio.html 2 | import asyncio 3 | 4 | from client import LdapClient 5 | from settings import LDAP_CONNECTIONS 6 | 7 | result_set = [] 8 | LDAP_SERVERS = [] 9 | 10 | async def get_result(lc): 11 | await asyncio.sleep(0.001) 12 | return lc.get(format='dict') 13 | 14 | async def get_server(CONF): 15 | return LdapClient(CONF) 16 | 17 | async def ensure_connection(lc): 18 | await asyncio.sleep(0.001) 19 | lc.ensure_connection() 20 | 21 | async def connect(lc_id: int): 22 | CONF = list(LDAP_CONNECTIONS.keys())[lc_id] 23 | lc = await get_server(LDAP_CONNECTIONS[CONF]) 24 | # LDAP_SERVERS.append(lc) 25 | print('Connectign and gathering {} [{}]'.format(lc, CONF)) 26 | # await ensure_connection(lc) 27 | try: 28 | result = await get_result(lc) 29 | result_set.extend(result) 30 | print('Get {} results from {}'.format(len(result), lc)) 31 | except Exception as e: 32 | print('-- Fail to connect to {} [{}]'.format(lc, CONF)) 33 | print(e) 34 | 35 | async def main(): 36 | await asyncio.gather(*(connect(n) for n in range(len(LDAP_CONNECTIONS.keys())))) 37 | print('Done') 38 | 39 | if __name__ == '__main__': 40 | asyncio.run(main()) 41 | print(len(result_set)) 42 | -------------------------------------------------------------------------------- /tests/run_test.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import logging 3 | import sys 4 | 5 | from importlib import util as importlib_util 6 | from multildap.client import LdapClient 7 | 8 | spec = importlib_util.spec_from_file_location("settings", "settings.py") 9 | settings = importlib_util.module_from_spec(spec) 10 | spec.loader.exec_module(settings) 11 | 12 | 13 | for i in settings.LDAP_CONNECTIONS.values(): 14 | lc = LdapClient(i) 15 | # print('# Results from: {} ...'.format(lc)) 16 | # kwargs = copy.copy(lc.conf) 17 | # r = lc.get(search="(&(sn=aie*)(givenName=isa*))") 18 | # print(r) 19 | 20 | # like wildcard 21 | # r = lc.get(search="(&(sn=de marco)(schacPersonalUniqueId=*DMRGPP83*))") 22 | # print(r) 23 | 24 | # using search method with overload of configuration 25 | #kwargs['search']['search_filter'] = "(&(sn=de marco))" 26 | #r = lc.search(**kwargs['search']) 27 | 28 | lc.set_strategy('RESTARTABLE') 29 | print('# ', lc.strategy) 30 | print('# as DICT') 31 | r = lc.get(format='dict') 32 | print(r) if r else '' 33 | print() 34 | 35 | r = lc.get(format='json') 36 | print('# as JSON') 37 | print(r) if r else '' 38 | print() 39 | 40 | lc.set_strategy('REUSABLE') 41 | print('# ', lc.strategy) 42 | print('# as DICT') 43 | r = lc.get(format='dict') 44 | print(r) if r else '' 45 | print() 46 | 47 | r = lc.get(format='json') 48 | print('# as JSON') 49 | print(r) if r else '' 50 | print() 51 | 52 | # get result in original format 53 | # this won't apply rewrite rules 54 | print('# as ORIGNAL') 55 | r = lc.get() 56 | print(r) if r else '' 57 | print() 58 | 59 | print('# as LDIF') 60 | r = lc.get(format='ldif') 61 | print(r) if r else '' 62 | print() 63 | 64 | print('# End') 65 | -------------------------------------------------------------------------------- /multildap/attr_rewrite.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import re 3 | 4 | 5 | def decode_iterables(ite, encoding='utf-8'): 6 | return [e.decode(encoding) if isinstance(e, bytes) else e for e in ite] 7 | 8 | 9 | def regexp_replace(attrs, regexp='', sub='', encoding='utf-8'): 10 | d = dict() 11 | for k,v in attrs.items(): 12 | items = decode_iterables(v, encoding) 13 | d[k] = [re.sub(regexp, sub, e, re.I) for e in items] 14 | return d 15 | 16 | 17 | def lowercase(attrs, fields=[], encoding='utf-8'): 18 | """ 19 | fields -> limits only to specified attributes 20 | 21 | rewrite_rules = 22 | [ 23 | 24 | {'package': 'multildap.attr_rewrite', 25 | 'name': 'lowercase', 26 | 'kwargs': {'fields': ['email', 'mail']} 27 | }, 28 | 29 | ... 30 | """ 31 | d = attrs.copy() 32 | for k,v in attrs.items(): 33 | if k in fields: 34 | items = decode_iterables(v, encoding) 35 | d[k] = [e.lower() for e in items] 36 | return d 37 | 38 | 39 | def replace(attrs, from_str='', to_str='', to_attrs=[], encoding='utf-8'): 40 | """ 41 | to_attrs -> limits only to specified attributes 42 | """ 43 | d = dict() 44 | for k,v in attrs.items(): 45 | items = decode_iterables(v, encoding) 46 | d[k] = [e.replace(from_str, to_str) for e in items] 47 | return d 48 | 49 | def append(attrs, value='', to_attrs=[], encoding='utf-8'): 50 | """ 51 | to_attrs -> limits only to specified attributes 52 | """ 53 | d = dict() 54 | for k,v in attrs.items(): 55 | items = decode_iterables(v, encoding) 56 | if value not in d[k]: 57 | d[k] = v + value 58 | return d 59 | 60 | 61 | def add_static_attribute(attrs, name='email', value='', **kwargs): 62 | if name in attrs and isinstance(attrs[name], list): 63 | attrs[name].append(value) 64 | else: 65 | attrs[name] = [value] 66 | return attrs 67 | 68 | 69 | def copy_attribute_value(attrs, from_attr='email', to_attr='', 70 | suffix='', prefix='', **kwargs): 71 | if not from_attr in attrs: return attrs 72 | v = attrs[from_attr][0] if isinstance(attrs[from_attr], list) else attrs[from_attr] 73 | value = '{}{}{}'.format(prefix, v, suffix) 74 | if to_attr in attrs and isinstance(attrs[from_attr], list): 75 | attrs[to_attr].append(value) 76 | else: 77 | attrs[to_attr] = [value] 78 | return attrs 79 | 80 | 81 | def map_from_dict(attrs, from_attr='email', to_attr='', 82 | suffix='', prefix='', dict_map={}, **kwargs): 83 | """map from_attr to an external dict_map, if match 84 | create a new attr or add to an existent one (to_attr). 85 | """ 86 | # TODO 87 | pass 88 | -------------------------------------------------------------------------------- /multildap/satosa/multiple_ldap_attribute_store.py: -------------------------------------------------------------------------------- 1 | """ 2 | SATOSA microservice that uses an identifier asserted by 3 | the home organization SAML IdP as a key to search an LDAP 4 | directory for a record and then consume attributes from 5 | the record and assert them to the receiving SP. 6 | """ 7 | 8 | from ldap3.core.exceptions import LDAPException 9 | from multildap.client import LdapClient 10 | 11 | from satosa.micro_services.base import ResponseMicroService 12 | from satosa.response import Redirect 13 | from satosa.exception import SATOSAError 14 | 15 | import copy 16 | import importlib.util as importlib_util 17 | import logging 18 | 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class MultiLdapAttributeStore(ResponseMicroService): 24 | 25 | def __init__(self, config, *args, **kwargs): 26 | super().__init__(*args, **kwargs) 27 | self.config = config 28 | 29 | # cache 30 | # use mongodb, using sp+user id as key 31 | #self.attributes = {} 32 | 33 | # import settings and loads connections 34 | spec = importlib_util.spec_from_file_location("settings", 35 | config['settings_path']) 36 | settings = importlib_util.module_from_spec(spec) 37 | spec.loader.exec_module(settings) 38 | 39 | self.config['connections'] = settings.LDAP_CONNECTIONS 40 | self.connections = {name: LdapClient(conf) 41 | for name,conf in 42 | settings.LDAP_CONNECTIONS.items()} 43 | 44 | msg = "MultiLDAP Attribute Store microservice initialized" 45 | logger.info(msg) 46 | for conn in self.connections: 47 | msg = "MultiLDAP connection to: {}".format(conn) 48 | logger.info(msg) 49 | 50 | 51 | def process(self, context, data): 52 | 53 | # Use cache in mongoDB with cache duration of 5min 54 | #if self.attributes and self.config.get('attributes_cache', None): 55 | #logger.debug('Attr processor id: {}'.format(id(self))) 56 | #msg = "MultiLdapAttributeStore found previously fetched attributes" 57 | #logger.info(msg) 58 | #return ResponseMicroService.process(self, context, data) 59 | 60 | for name,lc in self.connections.items(): 61 | search_attr = self.config['unique_attribute_to_match'] 62 | 63 | # prevent exception on missing attr 64 | attr_value = data.attributes.get(search_attr, False) 65 | if not attr_value: 66 | msg = '{} not found in {}'.format(search_attr, lc) 67 | logger.debug(msg) 68 | continue 69 | if isinstance(attr_value, str): 70 | attr_value = [attr_value] 71 | 72 | msg = ("MultiLdapAttributeStore searches for {} in {}".format(search_attr, lc)) 73 | logger.info(msg) 74 | 75 | ldapfilter = '({}{}{})'.format(search_attr, 76 | self.config['ldap_filter_operator'], 77 | attr_value[0]) 78 | identity = lc.get(search=ldapfilter, format='dict') 79 | if not identity: continue 80 | 81 | msg = "MultiLdapAttributeStore matches on {}".format(search_attr) 82 | logger.info(msg) 83 | 84 | attributes = {} 85 | for k,v in identity.items(): 86 | k = k.lower() 87 | if k not in attributes: 88 | attributes[k] = v 89 | msg = "MultiLdapAttributeStore created: {}".format([e for e in v.keys()]) 90 | logger.debug(msg) 91 | # TODO: check this update 92 | elif k in attributes: 93 | attributes[k].update(v) 94 | 95 | data.attributes.update(copy.copy(attributes[k])) 96 | logger.debug( ''.format(data.attributes)) 97 | 98 | return ResponseMicroService.process(self, context, data) 99 | -------------------------------------------------------------------------------- /examples/settings.py.example: -------------------------------------------------------------------------------- 1 | import ldap3 2 | 3 | # GLOBALS 4 | 5 | # encoding 6 | ldap3.set_config_parameter('DEFAULT_SERVER_ENCODING', 7 | 'UTF-8') 8 | # some broken LDAP implementation may have different encoding 9 | # than those expected by RFCs 10 | # ldap3.set_config_paramenter('ADDITIONAL_ENCODINGS', ...) 11 | 12 | # timeouts 13 | ldap3.set_config_parameter('RESTARTABLE_TRIES', 1) 14 | ldap3.set_config_parameter('POOLING_LOOP_TIMEOUT', 1) 15 | ldap3.set_config_parameter('RESET_AVAILABILITY_TIMEOUT', 1) 16 | ldap3.set_config_parameter('RESTARTABLE_SLEEPTIME', 1) 17 | 18 | 19 | _REWRITE_DN_TO = 'dc=proxy,dc=testunical,dc=it' 20 | 21 | DEFAULT = dict(server = 22 | dict(host = 'ldaps://thathost.unical.it', 23 | connect_timeout = 5, 24 | # TLS... 25 | ), 26 | connection = 27 | dict(user = 'cn=thatusername,dc=unical,dc=it', 28 | password = 'thatpassword', 29 | read_only = True, 30 | version = 3, 31 | # see ldap3 client_strategies 32 | client_strategy = ldap3.RESTARTABLE, 33 | auto_bind = True, 34 | pool_size = 10, 35 | pool_keepalive = 10), 36 | search = 37 | dict(search_base = 'ou=people,dc=unical,dc=it', 38 | search_filter = '(objectclass=person)', 39 | search_scope = ldap3.SUBTREE, 40 | 41 | # general purpose for huge resultsets 42 | # TODO: implement paged resultset, see: examples/paged_resultset.py 43 | # size_limit = 500, 44 | # paged_size = 1000, # up to 500000 results 45 | # paged_criticality = True, # check if the server supports paged results 46 | # paged_cookie = True, # must be sent back while requesting subsequent entries 47 | 48 | # to get all = # '*' 49 | attributes = ['eduPersonPrincipalName', 50 | 'schacHomeOrganization', 51 | 'mail', 52 | 'uid', 53 | 'givenName', 54 | 'sn', 55 | 'eduPersonScopedAffiliation', 56 | 'schacPersonalUniqueId', 57 | 'schacPersonalUniqueCode' 58 | ] 59 | 60 | ), 61 | encoding = 'utf-8', 62 | rewrite_rules = 63 | [{'package': 'multildap.attr_rewrite', 64 | 'name': 'replace', 65 | 'kwargs': {'from_str': 'unical', 'to_str': 'lacinu',}}, 66 | 67 | {'package': 'multildap.attr_rewrite', 68 | 'name': 'regexp_replace', 69 | 'kwargs': {'regexp': 'unical', 'sub': 'gnocc',}}, 70 | 71 | {'package': 'multildap.attr_rewrite', 72 | 'name': 'add_static_attribute', 73 | 'kwargs': {'name': 'eduPersonOrcid', 'value': 'ingoalla',}}, 74 | 75 | {'package': 'multildap.attr_rewrite', 76 | 'name': 'copy_attribute_value', 77 | 'kwargs': {'from_attr': 'uid', 78 | 'to_attr': 'schacPersonalUniqueID', 79 | 'suffix': '', 80 | 'prefix': 'urn:schac:personalUniqueID:IT:CF:', 81 | }}, 82 | ], 83 | 84 | # Authentication settings 85 | rewrite_dn_to = _REWRITE_DN_TO, 86 | allow_authentication = True, 87 | ) 88 | 89 | LDAPTEST = dict(server = 90 | dict(host = 'ldap://ldap.testunical.it:389', 91 | connect_timeout = 5, 92 | # TLS... 93 | ), 94 | connection = 95 | dict(user = 'cn=idp1,ou=idp,dc=testunical,dc=it', 96 | password = 'idp1', 97 | read_only = True, 98 | version = 3, 99 | # see ldap3 client_strategies 100 | client_strategy = ldap3.RESTARTABLE, 101 | auto_bind = True, 102 | pool_size = 10, 103 | pool_keepalive = 10), 104 | search = 105 | dict(search_base = 'ou=people,dc=testunical,dc=it', 106 | search_filter = '(objectclass=person)', 107 | search_scope = ldap3.SUBTREE, 108 | 109 | # general purpose for huge resultsets 110 | # TODO: implement paged resultset, see: examples/paged_resultset.py 111 | # size_limit = 500, 112 | # paged_size = 1000, # up to 500000 results 113 | # paged_criticality = True, # check if the server supports paged results 114 | # paged_cookie = True, # must be sent back while requesting subsequent entries 115 | 116 | # to get all = # '*' 117 | attributes = ['eduPersonPrincipalName', 118 | 'schacHomeOrganization', 119 | 'mail', 120 | 'uid', 121 | 'givenName', 122 | 'sn', 123 | 'eduPersonScopedAffiliation', 124 | 'schacPersonalUniqueId', 125 | 'schacPersonalUniqueCode' 126 | ] 127 | 128 | ), 129 | encoding = 'utf-8', 130 | rewrite_rules = 131 | [{'package': 'multildap.attr_rewrite', 132 | 'name': 'replace', 133 | 'kwargs': {'from_str': 'testunical', 'to_str': 'unical',}}, 134 | 135 | {'package': 'multildap.attr_rewrite', 136 | 'name': 'regexp_replace', 137 | 'kwargs': {'regexp': '', 'sub': '',}}, 138 | 139 | ], 140 | rewrite_dn_to = _REWRITE_DN_TO, 141 | ) 142 | 143 | # put multiple connections here 144 | LDAP_CONNECTIONS = {'DEFAULT' : DEFAULT, 145 | 'LDAPTEST' : LDAPTEST} 146 | -------------------------------------------------------------------------------- /multildap/commands.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import logging 3 | 4 | from multildap.client import LdapClient 5 | 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | # https://docs.oracle.com/javase/jndi/tutorial/ldap/models/exceptions.html 11 | _CODE_NOSUCHOBJECTEXISTS = 32 12 | _CODE_NOTIMPLEMENTED = 53 13 | _CODE_TIMEOUT = 3 14 | _CODE_OPERATIONERROR = 1 15 | _CODE_INVALIDCREDENTIAL = 49 16 | 17 | # RESPONSE TEMPLATES 18 | _ENTRY_TMPL = """{ldifs}""" 19 | _RESULT_TMPL = """ 20 | 21 | RESULT 22 | code: {code} 23 | info: {text} 24 | """ 25 | 26 | 27 | class LDAPUnrecognizesCommandAttributes(Exception): 28 | pass 29 | 30 | 31 | class LdapCommand(object): 32 | 33 | def __init__(self, ldap_command): 34 | """ 35 | ldap_command = ['SEARCH', {**attr_kwargs}] 36 | """ 37 | self.type = ldap_command[0].lower() 38 | for k,v in ldap_command[1].items(): 39 | setattr(self, k, int(v) if v.isdigit() else v) 40 | self.command = 'process_{}'.format(self.type) 41 | logging.debug('LDAPCommand: {}'.format(ldap_command)) 42 | 43 | def process(self, ldapclients=[]): 44 | """ 45 | The commands - except unbind - should output: 46 | RESULT 47 | code: 48 | matched: 49 | info: 50 | where only RESULT is mandatory, and then close the socket. The search 51 | RESULT should be preceded by the entries in LDIF format, each entry 52 | followed by a blank line. Lines starting with `#' or `DEBUG:' are 53 | ignored. 54 | """ 55 | response = '' 56 | if self.type == 'unbind': 57 | return '' 58 | if hasattr(self, self.command): 59 | try: 60 | response = getattr(self, self.command.lower())(ldapclients) 61 | except Exception as excp: 62 | return _RESULT_TMPL.format(**{'code':_CODE_OPERATIONERROR, 63 | 'text': excp}) 64 | else: 65 | code = _CODE_NOTIMPLEMENTED 66 | text = 'LDAP command [{}] non implemented yet'.format(self.command, 67 | self.type) 68 | return response 69 | 70 | 71 | def process_search(self, ldapclients): 72 | """ 73 | SEARCH 74 | msgid: 75 | }> 76 | base: 77 | scope: <0-2, see ldap.h> 78 | deref: <0-3, see ldap.h> 79 | sizelimit: 80 | timelimit: