@@ -70,14 +70,23 @@ type PoolStats struct {
7070 ConnectionsClosed int64
7171}
7272
73+ // ConnectionCredentials stores authentication information for a pooled connection.
74+ // This enables per-user connection tracking and credential-aware connection reuse
75+ // in multi-user scenarios (e.g., web applications, multi-tenant systems).
76+ type ConnectionCredentials struct {
77+ DN string // Distinguished Name for LDAP bind
78+ Password string // Password for LDAP bind
79+ }
80+
7381// pooledConnection wraps an LDAP connection with metadata for pool management
7482type pooledConnection struct {
75- conn * ldap.Conn
76- createdAt time.Time
77- lastUsed time.Time
78- usageCount int64
79- isHealthy bool
80- inUse bool
83+ conn * ldap.Conn
84+ createdAt time.Time
85+ lastUsed time.Time
86+ usageCount int64
87+ isHealthy bool
88+ inUse bool
89+ credentials * ConnectionCredentials // Tracks connection credentials for multi-user pooling
8190}
8291
8392// ConnectionPool manages a pool of LDAP connections with health monitoring and lifecycle management
@@ -284,6 +293,265 @@ func (p *ConnectionPool) Put(conn *ldap.Conn) error {
284293 }
285294}
286295
296+ // GetWithCredentials returns a connection from the pool authenticated with specific credentials.
297+ // This method enables per-user connection pooling in multi-user scenarios where different
298+ // users need separate authenticated connections.
299+ //
300+ // The pool will:
301+ // - Reuse existing connections with matching credentials
302+ // - Create new connections when no matching credentials found
303+ // - Prevent credential mixing for security
304+ //
305+ // Use cases:
306+ // - Web applications with per-user LDAP operations
307+ // - Multi-tenant systems
308+ // - Delegated operations on behalf of authenticated users
309+ //
310+ // Example:
311+ //
312+ // // User A's connection
313+ // connA, err := pool.GetWithCredentials(ctx, "cn=userA,dc=example,dc=com", "passwordA")
314+ // if err != nil {
315+ // return err
316+ // }
317+ // defer pool.Put(connA)
318+ //
319+ // // User B's connection (different connection from User A)
320+ // connB, err := pool.GetWithCredentials(ctx, "cn=userB,dc=example,dc=com", "passwordB")
321+ // if err != nil {
322+ // return err
323+ // }
324+ // defer pool.Put(connB)
325+ func (p * ConnectionPool ) GetWithCredentials (ctx context.Context , dn , password string ) (* ldap.Conn , error ) {
326+ p .mu .RLock ()
327+ if p .closed {
328+ p .mu .RUnlock ()
329+ return nil , ErrPoolClosed
330+ }
331+ p .mu .RUnlock ()
332+
333+ creds := & ConnectionCredentials {
334+ DN : dn ,
335+ Password : password ,
336+ }
337+
338+ // Try to get connection with timeout
339+ timeoutCtx , cancel := context .WithTimeout (ctx , p .config .GetTimeout )
340+ defer cancel ()
341+
342+ // Try to find an available connection with matching credentials
343+ for {
344+ select {
345+ case conn := <- p .available :
346+ if conn != nil && p .canReuseConnection (conn , creds ) {
347+ conn .inUse = true
348+ conn .lastUsed = time .Now ()
349+ atomic .AddInt64 (& conn .usageCount , 1 )
350+ atomic .AddInt32 (& p .stats .ActiveConnections , 1 )
351+ atomic .AddInt32 (& p .stats .IdleConnections , - 1 )
352+ atomic .AddInt64 (& p .stats .PoolHits , 1 )
353+
354+ // Add to connection map for tracking
355+ p .connMapMu .Lock ()
356+ p .connMap [conn .conn ] = conn
357+ p .connMapMu .Unlock ()
358+
359+ p .logger .Debug ("connection_retrieved_with_credentials" ,
360+ slog .String ("dn" , dn ),
361+ slog .Time ("created_at" , conn .createdAt ),
362+ slog .Int64 ("usage_count" , conn .usageCount ))
363+
364+ return conn .conn , nil
365+ }
366+ // Connection doesn't match credentials, close it and continue searching
367+ p .closeConnection (conn )
368+ atomic .AddInt32 (& p .stats .IdleConnections , - 1 )
369+
370+ case <- timeoutCtx .Done ():
371+ // Timeout or context cancelled, try to create new connection
372+ p .mu .RLock ()
373+ totalConns := atomic .LoadInt32 (& p .stats .TotalConnections )
374+ canCreate := int (totalConns ) < p .config .MaxConnections
375+ p .mu .RUnlock ()
376+
377+ if canCreate {
378+ // Create new connection with credentials
379+ conn , err := p .createConnectionWithCredentials (ctx , creds )
380+ if err != nil {
381+ atomic .AddInt64 (& p .stats .PoolMisses , 1 )
382+ return nil , err
383+ }
384+ return conn , nil
385+ }
386+
387+ return nil , ErrPoolExhausted
388+
389+ default :
390+ // No available connections, try to create new one
391+ p .mu .RLock ()
392+ totalConns := atomic .LoadInt32 (& p .stats .TotalConnections )
393+ canCreate := int (totalConns ) < p .config .MaxConnections
394+ p .mu .RUnlock ()
395+
396+ if canCreate {
397+ conn , err := p .createConnectionWithCredentials (ctx , creds )
398+ if err != nil {
399+ atomic .AddInt64 (& p .stats .PoolMisses , 1 )
400+ return nil , err
401+ }
402+ return conn , nil
403+ }
404+
405+ // Pool exhausted, wait a bit and retry
406+ select {
407+ case <- timeoutCtx .Done ():
408+ return nil , ErrPoolExhausted
409+ case <- time .After (10 * time .Millisecond ):
410+ // Continue loop to retry
411+ }
412+ }
413+ }
414+ }
415+
416+ // canReuseConnection determines if a connection can be reused for the given credentials.
417+ // A connection can be reused if:
418+ // - It's healthy
419+ // - It hasn't exceeded MaxIdleTime
420+ // - Credentials match (or both are nil for readonly connections)
421+ func (p * ConnectionPool ) canReuseConnection (conn * pooledConnection , creds * ConnectionCredentials ) bool {
422+ // Check health status
423+ if ! conn .isHealthy {
424+ return false
425+ }
426+
427+ now := time .Now ()
428+
429+ // Check idle time expiry
430+ if now .Sub (conn .lastUsed ) > p .config .MaxIdleTime {
431+ return false
432+ }
433+
434+ // Check credential matching
435+ if conn .credentials != nil && creds != nil {
436+ // Both have credentials - must match exactly
437+ return conn .credentials .DN == creds .DN &&
438+ conn .credentials .Password == creds .Password
439+ }
440+
441+ // Allow reuse only if both are nil (readonly connections)
442+ // This prevents mixing readonly and authenticated connections
443+ return conn .credentials == nil && creds == nil
444+ }
445+
446+ // createConnectionWithCredentials creates a new LDAP connection with specific credentials
447+ func (p * ConnectionPool ) createConnectionWithCredentials (ctx context.Context , creds * ConnectionCredentials ) (* ldap.Conn , error ) {
448+ // Check if we've reached max connections
449+ p .mu .RLock ()
450+ currentTotal := len (p .connections )
451+ p .mu .RUnlock ()
452+
453+ if currentTotal >= p .config .MaxConnections {
454+ atomic .AddInt64 (& p .stats .PoolMisses , 1 )
455+ return nil , ErrPoolExhausted
456+ }
457+
458+ // Create connection with timeout
459+ connCtx , cancel := context .WithTimeout (ctx , p .config .ConnectionTimeout )
460+ defer cancel ()
461+
462+ start := time .Now ()
463+ p .logger .Debug ("creating_connection_with_credentials" ,
464+ slog .String ("server" , p .ldapConfig .Server ),
465+ slog .String ("dn" , creds .DN ))
466+
467+ dialOpts := make ([]ldap.DialOpt , 0 )
468+ if p .ldapConfig .DialOptions != nil {
469+ dialOpts = p .ldapConfig .DialOptions
470+ }
471+
472+ // Check for context cancellation before dialing
473+ select {
474+ case <- connCtx .Done ():
475+ return nil , connCtx .Err ()
476+ default :
477+ }
478+
479+ conn , err := ldap .DialURL (p .ldapConfig .Server , dialOpts ... )
480+ if err != nil {
481+ p .logger .Error ("connection_dial_failed" ,
482+ slog .String ("server" , p .ldapConfig .Server ),
483+ slog .String ("error" , err .Error ()),
484+ slog .Duration ("duration" , time .Since (start )))
485+ return nil , err
486+ }
487+
488+ // Check for context cancellation before binding
489+ select {
490+ case <- connCtx .Done ():
491+ if closeErr := conn .Close (); closeErr != nil {
492+ p .logger .Debug ("connection_close_error" ,
493+ slog .String ("operation" , "createConnectionWithCredentials" ),
494+ slog .String ("error" , closeErr .Error ()))
495+ }
496+ return nil , connCtx .Err ()
497+ default :
498+ }
499+
500+ // Bind with provided credentials (or pool credentials if nil)
501+ bindDN := p .user
502+ bindPassword := p .password
503+ if creds != nil && creds .DN != "" {
504+ bindDN = creds .DN
505+ bindPassword = creds .Password
506+ }
507+
508+ if err = conn .Bind (bindDN , bindPassword ); err != nil {
509+ if closeErr := conn .Close (); closeErr != nil {
510+ p .logger .Debug ("connection_close_error" ,
511+ slog .String ("operation" , "createConnectionWithCredentials" ),
512+ slog .String ("error" , closeErr .Error ()))
513+ }
514+ p .logger .Error ("connection_bind_failed" ,
515+ slog .String ("server" , p .ldapConfig .Server ),
516+ slog .String ("dn" , bindDN ),
517+ slog .String ("error" , err .Error ()),
518+ slog .Duration ("duration" , time .Since (start )))
519+ return nil , err
520+ }
521+
522+ // Create pooled connection wrapper
523+ pooledConn := & pooledConnection {
524+ conn : conn ,
525+ credentials : creds ,
526+ createdAt : time .Now (),
527+ lastUsed : time .Now (),
528+ isHealthy : true ,
529+ inUse : true ,
530+ usageCount : 1 ,
531+ }
532+
533+ // Track connection
534+ p .mu .Lock ()
535+ p .connections = append (p .connections , pooledConn )
536+ p .mu .Unlock ()
537+
538+ atomic .AddInt32 (& p .stats .TotalConnections , 1 )
539+ atomic .AddInt32 (& p .stats .ActiveConnections , 1 )
540+ atomic .AddInt64 (& p .stats .ConnectionsCreated , 1 )
541+ atomic .AddInt64 (& p .stats .PoolMisses , 1 )
542+
543+ // Add to connection map for tracking
544+ p .connMapMu .Lock ()
545+ p .connMap [conn ] = pooledConn
546+ p .connMapMu .Unlock ()
547+
548+ p .logger .Debug ("connection_created_with_credentials" ,
549+ slog .String ("dn" , bindDN ),
550+ slog .Duration ("duration" , time .Since (start )))
551+
552+ return conn , nil
553+ }
554+
287555// Stats returns current pool statistics
288556func (p * ConnectionPool ) Stats () PoolStats {
289557 return PoolStats {
0 commit comments