14
14
import ldap3
15
15
import urllib
16
16
17
+ from ldap3 .core .exceptions import LDAPException
18
+
17
19
logger = logging .getLogger (__name__ )
18
20
19
21
class LdapAttributeStoreException (Exception ):
@@ -36,29 +38,36 @@ class LdapAttributeStore(satosa.micro_services.base.ResponseMicroService):
36
38
# entityID for a per-SP configuration.
37
39
#
38
40
# The keys are the allowed configuration options. The values
39
- # are a list of [default value, if required or not]. Note that
40
- # required here means required as part of an effective
41
+ # are a list of [default value, if required or not, new connection],
42
+ # where 'new connection' means whether of not an override for an SP
43
+ # of that configuration option causes a separate connection to be
44
+ # created for that SP. Required here means required as part of an effective
41
45
# configuration during a particular flow, so it applies
42
46
# to the default configuration overridden with any per-SP
43
47
# configuration details.
44
48
config_options = {
45
- 'bind_dn' : {'default' : None , 'required' : True },
46
- 'bind_password' : {'default' : None , 'required' : True },
47
- 'clear_input_attributes' : {'default' : False , 'required' : False },
48
- 'ignore' : {'default' : False , 'required' : False },
49
- 'ldap_identifier_attribute' : {'default' : None , 'required' : True },
50
- 'ldap_url' : {'default' : None , 'required' : True },
51
- 'on_ldap_search_result_empty' : {'default' : None , 'required' : False },
52
- 'ordered_identifier_candidates' : {'default' : None , 'required' : True },
53
- 'search_base' : {'default' : None , 'required' : True },
54
- 'search_return_attributes' : {'default' : None , 'required' : True },
55
- 'user_id_from_attrs' : {'default' : [], 'required' : False }
49
+ 'bind_dn' : {'default' : None , 'required' : True , 'connection' : True },
50
+ 'bind_password' : {'default' : None , 'required' : True , 'connection' : True },
51
+ 'clear_input_attributes' : {'default' : False , 'required' : False , 'connection' : False },
52
+ 'ignore' : {'default' : False , 'required' : False , 'connection' : False },
53
+ 'ldap_identifier_attribute' : {'default' : None , 'required' : True , 'connection' : False },
54
+ 'ldap_url' : {'default' : None , 'required' : True , 'connection' : True },
55
+ 'on_ldap_search_result_empty' : {'default' : None , 'required' : False , 'connection' : False },
56
+ 'ordered_identifier_candidates' : {'default' : None , 'required' : True , 'connection' : False },
57
+ 'pool_size' : {'default' : 10 , 'required' : False , 'connection' : True },
58
+ 'pool_keepalive' : {'default' : 10 , 'required' : False , 'connection' : True },
59
+ 'search_base' : {'default' : None , 'required' : True , 'connection' : False },
60
+ 'search_return_attributes' : {'default' : None , 'required' : True , 'connection' : False },
61
+ 'user_id_from_attrs' : {'default' : [], 'required' : False , 'connection' : False }
56
62
}
57
63
58
64
def __init__ (self , config , * args , ** kwargs ):
59
65
super ().__init__ (* args , ** kwargs )
60
66
self .config = config
61
67
self .logprefix = LdapAttributeStore .logprefix
68
+ self ._createLdapConnectionPools ()
69
+
70
+ satosa_logging (logger , logging .INFO , "{} LDAP Attribute Store microservice initialized" .format (self .logprefix ), None )
62
71
63
72
def _constructFilterValue (self , candidate , data ):
64
73
"""
@@ -163,6 +172,107 @@ def _copyConfigOptions(self, source, target):
163
172
if option in source :
164
173
target [option ] = source [option ]
165
174
175
+ def _createLdapConnectionPools (self ):
176
+ """
177
+ Examine the configuration and create a LDAP connection pool
178
+ for each unique set of configured LDAP options. The connections
179
+ are stored as a dictionary with the SP entityID or 'default'
180
+ as the keys and ldap3 Connection instances as the values.
181
+ """
182
+ logprefix = self .logprefix
183
+
184
+ # Initialize dictionary that holds mappings between entityID and
185
+ # LDAP connection pools. The special key 'default' holds the default
186
+ # connection pool.
187
+ connections = {}
188
+
189
+ # List of connections to create.
190
+ connections_to_create = ['default' ]
191
+
192
+ # Find the entityID for SP overrides if any.
193
+ spEntityIds = self ._getSPConfigOverrideEntityIds ()
194
+
195
+ # Determine if the SP configuration requires a separate connection.
196
+ ldap_config_options = { key : value ['connection' ] for key , value in LdapAttributeStore .config_options .items ()}
197
+ for entityId in spEntityIds :
198
+ for option , new_connection in ldap_config_options .items ():
199
+ if new_connection and option in self .config [entityId ]:
200
+ if entityId not in connections_to_create :
201
+ connections_to_create .append (entityId )
202
+
203
+ # Create the connections.
204
+ for label in connections_to_create :
205
+ config = self ._getEffectiveConfig (label if label != 'default' else None )
206
+ if 'ldap_url' in config :
207
+ try :
208
+ connection = self ._ldapConnectionFactory (config )
209
+ except LdapAttributeStoreException as e :
210
+ msg = "{} Caught exception creating LDAP connection: {}" .format (logprefix , e )
211
+ satosa_logging (logger , logging .ERROR , msg , None )
212
+ raise
213
+
214
+ connections [label ] = connection
215
+ satosa_logging (logger , logging .DEBUG , "{} Created LDAP connection with label '{}'" .format (logprefix , label ), None )
216
+
217
+ satosa_logging (logger , logging .INFO , "{} Created {} LDAP connections" .format (logprefix ,len (connections .keys ())), None )
218
+
219
+ self .connections = connections
220
+
221
+ def _ldapConnectionFactory (self , config ):
222
+ """
223
+ Use the input configuration to instantiate and return
224
+ a ldap3 Connection object.
225
+ """
226
+ logprefix = self .logprefix
227
+
228
+ ldap_url = config ['ldap_url' ]
229
+ bind_dn = config ['bind_dn' ]
230
+ bind_password = config ['bind_password' ]
231
+ pool_size = config ['pool_size' ]
232
+ pool_keepalive = config ['pool_keepalive' ]
233
+
234
+ server = ldap3 .Server (config ['ldap_url' ])
235
+
236
+ satosa_logging (logger , logging .DEBUG , "{} Creating a new LDAP connection" .format (logprefix ), None )
237
+ satosa_logging (logger , logging .DEBUG , "{} Using LDAP URL {}" .format (logprefix , ldap_url ), None )
238
+ satosa_logging (logger , logging .DEBUG , "{} Using bind DN {}" .format (logprefix , bind_dn ), None )
239
+ satosa_logging (logger , logging .DEBUG , "{} Using pool size {}" .format (logprefix , pool_size ), None )
240
+ satosa_logging (logger , logging .DEBUG , "{} Using pool keep alive {}" .format (logprefix , pool_keepalive ), None )
241
+
242
+ try :
243
+ connection = ldap3 .Connection (
244
+ server ,
245
+ bind_dn ,
246
+ bind_password ,
247
+ auto_bind = True ,
248
+ client_strategy = ldap3 .REUSABLE ,
249
+ pool_size = pool_size ,
250
+ pool_keepalive = pool_keepalive
251
+ )
252
+
253
+ except LDAPException as e :
254
+ msg = "{} Caught exception when connecting to LDAP server: {}" .format (logprefix , e )
255
+ satosa_logging (logger , logging .ERROR , msg , None )
256
+ raise LdapAttributeStoreException (msg )
257
+
258
+ satosa_logging (logger , logging .DEBUG , "{} Successfully connected to LDAP server" .format (logprefix ), None )
259
+
260
+ return connection
261
+
262
+ def _getConnection (self , entityID = None ):
263
+ """
264
+ Return the ldap3 Connection instance for the input SP entityID
265
+ or the default if no entityID is input.
266
+ """
267
+ label = entityID if not entityID else 'default'
268
+ try :
269
+ connection = self .connections [label ]
270
+ except KeyError as e :
271
+ msg = "No LDAP connection for {}" .format (label )
272
+ raise LdapAttributeStoreException (msg )
273
+
274
+ return connection
275
+
166
276
def _getEffectiveConfig (self , entityID = None , state = None ):
167
277
"""
168
278
Get the effective configuration for the SP with entityID
@@ -194,6 +304,22 @@ def _getEffectiveConfig(self, entityID = None, state = None):
194
304
195
305
return effective_config
196
306
307
+ def _getSPConfigOverrideEntityIds (self ):
308
+ """
309
+ Get the list of SP entityIDs from the configuration that are
310
+ configured as overrides to the default configuration.
311
+ """
312
+ entityIds = []
313
+ known_config_options = LdapAttributeStore .config_options .keys ()
314
+
315
+ for key in self .config .keys ():
316
+ if key not in known_config_options :
317
+ entityIds .append (key )
318
+
319
+ entityIds .sort ()
320
+
321
+ return entityIds
322
+
197
323
def _hideConfigSecrets (self , config ):
198
324
"""
199
325
Make a deep copy of the input config dictionary and
@@ -255,14 +381,7 @@ def process(self, context, data):
255
381
record = None
256
382
257
383
try :
258
- ldap_url = config ['ldap_url' ]
259
- satosa_logging (logger , logging .DEBUG , "{} Using LDAP URL {}" .format (logprefix , ldap_url ), context .state )
260
- server = ldap3 .Server (ldap_url )
261
-
262
- bind_dn = config ['bind_dn' ]
263
- satosa_logging (logger , logging .DEBUG , "{} Using bind DN {}" .format (logprefix , bind_dn ), context .state )
264
- connection = ldap3 .Connection (server , bind_dn , config ['bind_password' ], auto_bind = True )
265
- satosa_logging (logger , logging .DEBUG , "{} Connected to LDAP server" .format (logprefix ), context .state )
384
+ connection = self ._getConnection (spEntityID )
266
385
267
386
for filterVal in filterValues :
268
387
if record :
@@ -272,27 +391,29 @@ def process(self, context, data):
272
391
satosa_logging (logger , logging .DEBUG , "{} Constructed search filter {}" .format (logprefix , search_filter ), context .state )
273
392
274
393
satosa_logging (logger , logging .DEBUG , "{} Querying LDAP server..." .format (logprefix ), context .state )
275
- connection .search (config ['search_base' ], search_filter , attributes = config ['search_return_attributes' ].keys ())
394
+ message_id = connection .search (config ['search_base' ], search_filter , attributes = config ['search_return_attributes' ].keys ())
395
+ responses = connection .get_response (message_id )[0 ]
276
396
satosa_logging (logger , logging .DEBUG , "{} Done querying LDAP server" .format (logprefix ), context .state )
277
-
278
- responses = connection .response
279
397
satosa_logging (logger , logging .DEBUG , "{} LDAP server returned {} records" .format (logprefix , len (responses )), context .state )
280
398
281
399
# for now consider only the first record found (if any)
282
400
if len (responses ) > 0 :
283
401
if len (responses ) > 1 :
284
- satosa_logging (logger , logging .WARN , "{} LDAP server returned {} records using search filter value {}" .format (logprefix , len (responses ), filterVar ), context .state )
402
+ satosa_logging (logger , logging .WARN , "{} LDAP server returned {} records using search filter value {}" .format (logprefix , len (responses ), filterVal ), context .state )
285
403
record = responses [0 ]
286
404
break
405
+ except LDAPException as err :
406
+ satosa_logging (logger , logging .ERROR , "{} Caught LDAP exception: {}" .format (logprefix , err ), context .state )
407
+ return super ().process (context , data )
408
+
409
+ except LdapAttributeStoreException as err :
410
+ satosa_logging (logger , logging .ERROR , "{} Caught LDAP Attribute Store exception: {}" .format (logprefix , err ), context .state )
411
+ return super ().process (context , data )
287
412
288
413
except Exception as err :
289
- satosa_logging (logger , logging .ERROR , "{} Caught exception: {}" .format (logprefix , err ), context .state )
414
+ satosa_logging (logger , logging .ERROR , "{} Caught unhandled exception: {}" .format (logprefix , err ), context .state )
290
415
return super ().process (context , data )
291
416
292
- else :
293
- satosa_logging (logger , logging .DEBUG , "{} Unbinding and closing connection to LDAP server" .format (logprefix ), context .state )
294
- connection .unbind ()
295
-
296
417
# Before using a found record, if any, to populate attributes
297
418
# clear any attributes incoming to this microservice if so configured.
298
419
if config ['clear_input_attributes' ]:
0 commit comments