You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							866 lines
						
					
					
						
							23 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							866 lines
						
					
					
						
							23 KiB
						
					
					
				| package ldap | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"crypto/tls" | |
| 	"fmt" | |
| 	"net" | |
| 	"strings" | |
| 	"sync" | |
| 	"time" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	"github.com/seaweedfs/seaweedfs/weed/iam/providers" | |
| ) | |
| 
 | |
| // LDAPProvider implements LDAP authentication | |
| type LDAPProvider struct { | |
| 	name        string | |
| 	config      *LDAPConfig | |
| 	initialized bool | |
| 	connPool    *LDAPConnectionPool | |
| } | |
| 
 | |
| // LDAPConnectionPool manages LDAP connections | |
| type LDAPConnectionPool struct { | |
| 	config      *LDAPConfig | |
| 	connections chan *LDAPConn | |
| 	mu          sync.Mutex | |
| 	maxConns    int | |
| } | |
| 
 | |
| // LDAPConn represents an LDAP connection (simplified implementation) | |
| type LDAPConn struct { | |
| 	serverAddr string | |
| 	conn       net.Conn | |
| 	bound      bool | |
| 	tlsConfig  *tls.Config | |
| } | |
| 
 | |
| // LDAPSearchResult represents LDAP search results | |
| type LDAPSearchResult struct { | |
| 	Entries []*LDAPEntry | |
| } | |
| 
 | |
| // LDAPEntry represents an LDAP directory entry | |
| type LDAPEntry struct { | |
| 	DN         string | |
| 	Attributes []*LDAPAttribute | |
| } | |
| 
 | |
| // LDAPAttribute represents an LDAP attribute | |
| type LDAPAttribute struct { | |
| 	Name   string | |
| 	Values []string | |
| } | |
| 
 | |
| // LDAPSearchRequest represents an LDAP search request | |
| type LDAPSearchRequest struct { | |
| 	BaseDN       string | |
| 	Scope        int | |
| 	DerefAliases int | |
| 	SizeLimit    int | |
| 	TimeLimit    int | |
| 	TypesOnly    bool | |
| 	Filter       string | |
| 	Attributes   []string | |
| } | |
| 
 | |
| // LDAP search scope constants | |
| const ( | |
| 	ScopeBaseObject = iota | |
| 	ScopeWholeSubtree | |
| 	NeverDerefAliases = 0 | |
| ) | |
| 
 | |
| // LDAPConfig holds LDAP provider configuration | |
| type LDAPConfig struct { | |
| 	// Server is the LDAP server URL (e.g., ldap://localhost:389) | |
| 	Server string `json:"server"` | |
| 
 | |
| 	// BaseDN is the base distinguished name for searches | |
| 	BaseDN string `json:"baseDn"` | |
| 
 | |
| 	// BindDN is the distinguished name for binding (authentication) | |
| 	BindDN string `json:"bindDn,omitempty"` | |
| 
 | |
| 	// BindPass is the password for binding | |
| 	BindPass string `json:"bindPass,omitempty"` | |
| 
 | |
| 	// UserFilter is the LDAP filter for finding users (e.g., "(sAMAccountName=%s)") | |
| 	UserFilter string `json:"userFilter"` | |
| 
 | |
| 	// GroupFilter is the LDAP filter for finding groups (e.g., "(member=%s)") | |
| 	GroupFilter string `json:"groupFilter,omitempty"` | |
| 
 | |
| 	// Attributes maps SeaweedFS identity fields to LDAP attributes | |
| 	Attributes map[string]string `json:"attributes,omitempty"` | |
| 
 | |
| 	// RoleMapping defines how to map LDAP groups to roles | |
| 	RoleMapping *providers.RoleMapping `json:"roleMapping,omitempty"` | |
| 
 | |
| 	// TLS configuration | |
| 	UseTLS        bool   `json:"useTls,omitempty"` | |
| 	TLSCert       string `json:"tlsCert,omitempty"` | |
| 	TLSKey        string `json:"tlsKey,omitempty"` | |
| 	TLSSkipVerify bool   `json:"tlsSkipVerify,omitempty"` | |
| 
 | |
| 	// Connection pool settings | |
| 	MaxConnections int `json:"maxConnections,omitempty"` | |
| 	ConnTimeout    int `json:"connTimeout,omitempty"` // seconds | |
| } | |
| 
 | |
| // NewLDAPProvider creates a new LDAP provider | |
| func NewLDAPProvider(name string) *LDAPProvider { | |
| 	return &LDAPProvider{ | |
| 		name: name, | |
| 	} | |
| } | |
| 
 | |
| // Name returns the provider name | |
| func (p *LDAPProvider) Name() string { | |
| 	return p.name | |
| } | |
| 
 | |
| // Initialize initializes the LDAP provider with configuration | |
| func (p *LDAPProvider) Initialize(config interface{}) error { | |
| 	if config == nil { | |
| 		return fmt.Errorf("config cannot be nil") | |
| 	} | |
| 
 | |
| 	ldapConfig, ok := config.(*LDAPConfig) | |
| 	if !ok { | |
| 		return fmt.Errorf("invalid config type for LDAP provider") | |
| 	} | |
| 
 | |
| 	if err := p.validateConfig(ldapConfig); err != nil { | |
| 		return fmt.Errorf("invalid LDAP configuration: %w", err) | |
| 	} | |
| 
 | |
| 	p.config = ldapConfig | |
| 
 | |
| 	// Initialize LDAP connection pool | |
| 	pool, err := NewLDAPConnectionPool(ldapConfig) | |
| 	if err != nil { | |
| 		glog.V(2).Infof("Failed to initialize LDAP connection pool: %v (using mock for testing)", err) | |
| 		// In case of connection failure, continue but mark as testing mode | |
| 		p.initialized = true | |
| 		return nil | |
| 	} | |
| 	p.connPool = pool | |
| 
 | |
| 	// Test connectivity with one connection | |
| 	conn, err := p.connPool.GetConnection() | |
| 	if err != nil { | |
| 		glog.V(2).Infof("Failed to establish test LDAP connection: %v (using mock for testing)", err) | |
| 		p.initialized = true | |
| 		return nil | |
| 	} | |
| 	p.connPool.ReleaseConnection(conn) | |
| 
 | |
| 	p.initialized = true | |
| 	glog.V(2).Infof("LDAP provider %s initialized with server %s", p.name, ldapConfig.Server) | |
| 	return nil | |
| } | |
| 
 | |
| // validateConfig validates the LDAP configuration | |
| func (p *LDAPProvider) validateConfig(config *LDAPConfig) error { | |
| 	if config.Server == "" { | |
| 		return fmt.Errorf("server is required") | |
| 	} | |
| 
 | |
| 	if config.BaseDN == "" { | |
| 		return fmt.Errorf("base DN is required") | |
| 	} | |
| 
 | |
| 	// Basic URL validation | |
| 	if !strings.HasPrefix(config.Server, "ldap://") && !strings.HasPrefix(config.Server, "ldaps://") { | |
| 		return fmt.Errorf("invalid server URL format") | |
| 	} | |
| 
 | |
| 	// Set default user filter if not provided | |
| 	if config.UserFilter == "" { | |
| 		config.UserFilter = "(uid=%s)" // Default LDAP user filter | |
| 	} | |
| 
 | |
| 	// Set default attributes if not provided | |
| 	if config.Attributes == nil { | |
| 		config.Attributes = map[string]string{ | |
| 			"email":       "mail", | |
| 			"displayName": "cn", | |
| 			"groups":      "memberOf", | |
| 		} | |
| 	} | |
| 
 | |
| 	return nil | |
| } | |
| 
 | |
| // Authenticate authenticates a user with LDAP | |
| func (p *LDAPProvider) Authenticate(ctx context.Context, credentials string) (*providers.ExternalIdentity, error) { | |
| 	if !p.initialized { | |
| 		return nil, fmt.Errorf("provider not initialized") | |
| 	} | |
| 
 | |
| 	if credentials == "" { | |
| 		return nil, fmt.Errorf("credentials cannot be empty") | |
| 	} | |
| 
 | |
| 	// Parse credentials (username:password format) | |
| 	parts := strings.SplitN(credentials, ":", 2) | |
| 	if len(parts) != 2 { | |
| 		return nil, fmt.Errorf("invalid credentials format (expected username:password)") | |
| 	} | |
| 
 | |
| 	username, password := parts[0], parts[1] | |
| 
 | |
| 	// Get connection from pool | |
| 	conn, err := p.getConnection() | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to get LDAP connection: %v", err) | |
| 	} | |
| 	defer p.releaseConnection(conn) | |
| 
 | |
| 	// Perform LDAP bind with service account if configured | |
| 	if p.config.BindDN != "" && p.config.BindPass != "" { | |
| 		err = conn.Bind(p.config.BindDN, p.config.BindPass) | |
| 		if err != nil { | |
| 			return nil, fmt.Errorf("failed to bind with service account: %v", err) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Search for user | |
| 	userFilter := fmt.Sprintf(p.config.UserFilter, EscapeFilter(username)) | |
| 	searchRequest := &LDAPSearchRequest{ | |
| 		BaseDN:       p.config.BaseDN, | |
| 		Scope:        ScopeWholeSubtree, | |
| 		DerefAliases: NeverDerefAliases, | |
| 		SizeLimit:    0, | |
| 		TimeLimit:    0, | |
| 		TypesOnly:    false, | |
| 		Filter:       userFilter, | |
| 		Attributes:   p.getSearchAttributes(), | |
| 	} | |
| 
 | |
| 	searchResult, err := conn.Search(searchRequest) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("LDAP search failed: %v", err) | |
| 	} | |
| 
 | |
| 	if len(searchResult.Entries) == 0 { | |
| 		return nil, fmt.Errorf("user not found in LDAP: %s", username) | |
| 	} | |
| 
 | |
| 	if len(searchResult.Entries) > 1 { | |
| 		return nil, fmt.Errorf("multiple users found for username: %s", username) | |
| 	} | |
| 
 | |
| 	userEntry := searchResult.Entries[0] | |
| 	userDN := userEntry.DN | |
| 
 | |
| 	// Authenticate user by binding with their credentials | |
| 	err = conn.Bind(userDN, password) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("LDAP authentication failed for user %s: %v", username, err) | |
| 	} | |
| 
 | |
| 	// Extract user attributes | |
| 	attributes := make(map[string][]string) | |
| 	for _, attr := range userEntry.Attributes { | |
| 		attributes[attr.Name] = attr.Values | |
| 	} | |
| 
 | |
| 	// Map to ExternalIdentity | |
| 	identity := p.mapLDAPAttributes(username, attributes) | |
| 	identity.UserID = username | |
| 
 | |
| 	// Get user groups if group filter is configured | |
| 	if p.config.GroupFilter != "" { | |
| 		groups, err := p.getUserGroups(conn, userDN, username) | |
| 		if err != nil { | |
| 			glog.V(2).Infof("Failed to retrieve groups for user %s: %v", username, err) | |
| 		} else { | |
| 			identity.Groups = groups | |
| 		} | |
| 	} | |
| 
 | |
| 	glog.V(3).Infof("LDAP authentication successful for user: %s", username) | |
| 	return identity, nil | |
| } | |
| 
 | |
| // GetUserInfo retrieves user information from LDAP | |
| func (p *LDAPProvider) GetUserInfo(ctx context.Context, userID string) (*providers.ExternalIdentity, error) { | |
| 	if !p.initialized { | |
| 		return nil, fmt.Errorf("provider not initialized") | |
| 	} | |
| 
 | |
| 	if userID == "" { | |
| 		return nil, fmt.Errorf("user ID cannot be empty") | |
| 	} | |
| 
 | |
| 	// Get connection from pool | |
| 	conn, err := p.getConnection() | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to get LDAP connection: %v", err) | |
| 	} | |
| 	defer p.releaseConnection(conn) | |
| 
 | |
| 	// Perform LDAP bind with service account if configured | |
| 	if p.config.BindDN != "" && p.config.BindPass != "" { | |
| 		err = conn.Bind(p.config.BindDN, p.config.BindPass) | |
| 		if err != nil { | |
| 			return nil, fmt.Errorf("failed to bind with service account: %v", err) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Search for user by userID using configured user filter | |
| 	userFilter := fmt.Sprintf(p.config.UserFilter, EscapeFilter(userID)) | |
| 	searchRequest := &LDAPSearchRequest{ | |
| 		BaseDN:       p.config.BaseDN, | |
| 		Scope:        ScopeWholeSubtree, | |
| 		DerefAliases: NeverDerefAliases, | |
| 		SizeLimit:    1,  // We only need one user | |
| 		TimeLimit:    30, // 30 second timeout | |
| 		TypesOnly:    false, | |
| 		Filter:       userFilter, | |
| 		Attributes:   p.getSearchAttributes(), | |
| 	} | |
| 
 | |
| 	glog.V(3).Infof("Searching for user %s with filter: %s", userID, userFilter) | |
| 	searchResult, err := conn.Search(searchRequest) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("LDAP user search failed: %v", err) | |
| 	} | |
| 
 | |
| 	if len(searchResult.Entries) == 0 { | |
| 		return nil, fmt.Errorf("user not found in LDAP: %s", userID) | |
| 	} | |
| 
 | |
| 	if len(searchResult.Entries) > 1 { | |
| 		glog.V(2).Infof("Multiple entries found for user %s, using first one", userID) | |
| 	} | |
| 
 | |
| 	userEntry := searchResult.Entries[0] | |
| 	userDN := userEntry.DN | |
| 
 | |
| 	glog.V(3).Infof("Found LDAP user: %s with DN: %s", userID, userDN) | |
| 
 | |
| 	// Extract user attributes | |
| 	attributes := make(map[string][]string) | |
| 	for _, attr := range userEntry.Attributes { | |
| 		attributes[attr.Name] = attr.Values | |
| 	} | |
| 
 | |
| 	// Map to ExternalIdentity | |
| 	identity := p.mapLDAPAttributes(userID, attributes) | |
| 	identity.UserID = userID | |
| 
 | |
| 	// Get user groups if group filter is configured | |
| 	if p.config.GroupFilter != "" { | |
| 		groups, err := p.getUserGroups(conn, userDN, userID) | |
| 		if err != nil { | |
| 			glog.V(2).Infof("Failed to retrieve groups for user %s: %v", userID, err) | |
| 		} else { | |
| 			identity.Groups = groups | |
| 		} | |
| 	} | |
| 
 | |
| 	glog.V(3).Infof("Successfully retrieved user info for: %s", userID) | |
| 	return identity, nil | |
| } | |
| 
 | |
| // ValidateToken validates credentials (for LDAP, this is username/password) | |
| func (p *LDAPProvider) ValidateToken(ctx context.Context, token string) (*providers.TokenClaims, error) { | |
| 	if !p.initialized { | |
| 		return nil, fmt.Errorf("provider not initialized") | |
| 	} | |
| 
 | |
| 	if token == "" { | |
| 		return nil, fmt.Errorf("token cannot be empty") | |
| 	} | |
| 
 | |
| 	// Parse credentials (username:password format) | |
| 	parts := strings.SplitN(token, ":", 2) | |
| 	if len(parts) != 2 { | |
| 		return nil, fmt.Errorf("invalid token format (expected username:password)") | |
| 	} | |
| 
 | |
| 	username, password := parts[0], parts[1] | |
| 
 | |
| 	// Get connection from pool | |
| 	conn, err := p.getConnection() | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to get LDAP connection: %v", err) | |
| 	} | |
| 	defer p.releaseConnection(conn) | |
| 
 | |
| 	// Perform LDAP bind with service account if configured | |
| 	if p.config.BindDN != "" && p.config.BindPass != "" { | |
| 		err = conn.Bind(p.config.BindDN, p.config.BindPass) | |
| 		if err != nil { | |
| 			return nil, fmt.Errorf("failed to bind with service account: %v", err) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Search for user using configured user filter | |
| 	userFilter := fmt.Sprintf(p.config.UserFilter, EscapeFilter(username)) | |
| 	searchRequest := &LDAPSearchRequest{ | |
| 		BaseDN:       p.config.BaseDN, | |
| 		Scope:        ScopeWholeSubtree, | |
| 		DerefAliases: NeverDerefAliases, | |
| 		SizeLimit:    1,  // We only need one user | |
| 		TimeLimit:    30, // 30 second timeout | |
| 		TypesOnly:    false, | |
| 		Filter:       userFilter, | |
| 		Attributes:   p.getSearchAttributes(), | |
| 	} | |
| 
 | |
| 	glog.V(3).Infof("Validating credentials for user %s with filter: %s", username, userFilter) | |
| 	searchResult, err := conn.Search(searchRequest) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("LDAP user search failed: %v", err) | |
| 	} | |
| 
 | |
| 	if len(searchResult.Entries) == 0 { | |
| 		return nil, fmt.Errorf("user not found in LDAP: %s", username) | |
| 	} | |
| 
 | |
| 	if len(searchResult.Entries) > 1 { | |
| 		glog.V(2).Infof("Multiple entries found for user %s, using first one", username) | |
| 	} | |
| 
 | |
| 	userEntry := searchResult.Entries[0] | |
| 	userDN := userEntry.DN | |
| 
 | |
| 	// Attempt to bind with user credentials to validate password | |
| 	err = conn.Bind(userDN, password) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("LDAP authentication failed for user %s: %v", username, err) | |
| 	} | |
| 
 | |
| 	glog.V(3).Infof("LDAP credential validation successful for user: %s", username) | |
| 
 | |
| 	// Extract user claims (DN, attributes, group memberships) | |
| 	attributes := make(map[string][]string) | |
| 	for _, attr := range userEntry.Attributes { | |
| 		attributes[attr.Name] = attr.Values | |
| 	} | |
| 
 | |
| 	// Get user groups if group filter is configured | |
| 	var groups []string | |
| 	if p.config.GroupFilter != "" { | |
| 		groups, err = p.getUserGroups(conn, userDN, username) | |
| 		if err != nil { | |
| 			glog.V(2).Infof("Failed to retrieve groups for user %s: %v", username, err) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Return TokenClaims with LDAP-specific information | |
| 	claims := &providers.TokenClaims{ | |
| 		Subject: username, | |
| 		Issuer:  p.name, | |
| 		Claims: map[string]interface{}{ | |
| 			"dn":         userDN, | |
| 			"provider":   p.name, | |
| 			"groups":     groups, | |
| 			"attributes": attributes, | |
| 		}, | |
| 	} | |
| 
 | |
| 	return claims, nil | |
| } | |
| 
 | |
| // mapLDAPAttributes maps LDAP attributes to ExternalIdentity | |
| func (p *LDAPProvider) mapLDAPAttributes(userID string, attrs map[string][]string) *providers.ExternalIdentity { | |
| 	identity := &providers.ExternalIdentity{ | |
| 		UserID:     userID, | |
| 		Provider:   p.name, | |
| 		Attributes: make(map[string]string), | |
| 	} | |
| 
 | |
| 	// Map configured attributes | |
| 	for identityField, ldapAttr := range p.config.Attributes { | |
| 		if values, exists := attrs[ldapAttr]; exists && len(values) > 0 { | |
| 			switch identityField { | |
| 			case "email": | |
| 				identity.Email = values[0] | |
| 			case "displayName": | |
| 				identity.DisplayName = values[0] | |
| 			case "groups": | |
| 				identity.Groups = values | |
| 			default: | |
| 				// Store as custom attribute | |
| 				identity.Attributes[identityField] = values[0] | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	return identity | |
| } | |
| 
 | |
| // mapUserToRole maps user groups to roles based on role mapping rules | |
| func (p *LDAPProvider) mapUserToRole(identity *providers.ExternalIdentity) string { | |
| 	if p.config.RoleMapping == nil { | |
| 		return "" | |
| 	} | |
| 
 | |
| 	// Create token claims from identity for rule matching | |
| 	claims := &providers.TokenClaims{ | |
| 		Subject: identity.UserID, | |
| 		Claims: map[string]interface{}{ | |
| 			"groups": identity.Groups, | |
| 			"email":  identity.Email, | |
| 		}, | |
| 	} | |
| 
 | |
| 	// Check mapping rules | |
| 	for _, rule := range p.config.RoleMapping.Rules { | |
| 		if rule.Matches(claims) { | |
| 			return rule.Role | |
| 		} | |
| 	} | |
| 
 | |
| 	// Return default role if no rules match | |
| 	return p.config.RoleMapping.DefaultRole | |
| } | |
| 
 | |
| // Connection management methods (stubs for now) | |
| func (p *LDAPProvider) getConnectionPool() interface{} { | |
| 	return p.connPool | |
| } | |
| 
 | |
| func (p *LDAPProvider) getConnection() (*LDAPConn, error) { | |
| 	if p.connPool == nil { | |
| 		return nil, fmt.Errorf("LDAP connection pool not initialized") | |
| 	} | |
| 	return p.connPool.GetConnection() | |
| } | |
| 
 | |
| func (p *LDAPProvider) releaseConnection(conn *LDAPConn) { | |
| 	if p.connPool != nil && conn != nil { | |
| 		p.connPool.ReleaseConnection(conn) | |
| 	} | |
| } | |
| 
 | |
| // getSearchAttributes returns the list of attributes to retrieve | |
| func (p *LDAPProvider) getSearchAttributes() []string { | |
| 	attrs := make([]string, 0, len(p.config.Attributes)+1) | |
| 	attrs = append(attrs, "dn") // Always include DN | |
|  | |
| 	for _, ldapAttr := range p.config.Attributes { | |
| 		attrs = append(attrs, ldapAttr) | |
| 	} | |
| 
 | |
| 	return attrs | |
| } | |
| 
 | |
| // getUserGroups retrieves user groups using the configured group filter | |
| func (p *LDAPProvider) getUserGroups(conn *LDAPConn, userDN, username string) ([]string, error) { | |
| 	// Try different group search approaches | |
|  | |
| 	// 1. Search by member DN | |
| 	groupFilter := fmt.Sprintf(p.config.GroupFilter, EscapeFilter(userDN)) | |
| 	groups, err := p.searchGroups(conn, groupFilter) | |
| 	if err == nil && len(groups) > 0 { | |
| 		return groups, nil | |
| 	} | |
| 
 | |
| 	// 2. Search by username if DN search fails | |
| 	groupFilter = fmt.Sprintf(p.config.GroupFilter, EscapeFilter(username)) | |
| 	groups, err = p.searchGroups(conn, groupFilter) | |
| 	if err != nil { | |
| 		return nil, err | |
| 	} | |
| 
 | |
| 	return groups, nil | |
| } | |
| 
 | |
| // searchGroups performs the actual group search | |
| func (p *LDAPProvider) searchGroups(conn *LDAPConn, filter string) ([]string, error) { | |
| 	searchRequest := &LDAPSearchRequest{ | |
| 		BaseDN:       p.config.BaseDN, | |
| 		Scope:        ScopeWholeSubtree, | |
| 		DerefAliases: NeverDerefAliases, | |
| 		SizeLimit:    0, | |
| 		TimeLimit:    0, | |
| 		TypesOnly:    false, | |
| 		Filter:       filter, | |
| 		Attributes:   []string{"cn", "dn"}, | |
| 	} | |
| 
 | |
| 	searchResult, err := conn.Search(searchRequest) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("group search failed: %v", err) | |
| 	} | |
| 
 | |
| 	groups := make([]string, 0, len(searchResult.Entries)) | |
| 	for _, entry := range searchResult.Entries { | |
| 		// Try to get CN first, fall back to DN | |
| 		if cn := entry.GetAttributeValue("cn"); cn != "" { | |
| 			groups = append(groups, cn) | |
| 		} else { | |
| 			groups = append(groups, entry.DN) | |
| 		} | |
| 	} | |
| 
 | |
| 	return groups, nil | |
| } | |
| 
 | |
| // NewLDAPConnectionPool creates a new LDAP connection pool | |
| func NewLDAPConnectionPool(config *LDAPConfig) (*LDAPConnectionPool, error) { | |
| 	maxConns := config.MaxConnections | |
| 	if maxConns <= 0 { | |
| 		maxConns = 10 | |
| 	} | |
| 
 | |
| 	pool := &LDAPConnectionPool{ | |
| 		config:      config, | |
| 		connections: make(chan *LDAPConn, maxConns), | |
| 		maxConns:    maxConns, | |
| 	} | |
| 
 | |
| 	// Pre-populate the pool with a few connections for testing | |
| 	for i := 0; i < 2 && i < maxConns; i++ { | |
| 		conn, err := pool.createConnection() | |
| 		if err != nil { | |
| 			// If we can't create any connections, return error | |
| 			if i == 0 { | |
| 				return nil, err | |
| 			} | |
| 			// If we created at least one, continue | |
| 			break | |
| 		} | |
| 		pool.connections <- conn | |
| 	} | |
| 
 | |
| 	return pool, nil | |
| } | |
| 
 | |
| // createConnection creates a new LDAP connection | |
| func (pool *LDAPConnectionPool) createConnection() (*LDAPConn, error) { | |
| 	var netConn net.Conn | |
| 	var err error | |
| 
 | |
| 	timeout := time.Duration(pool.config.ConnTimeout) * time.Second | |
| 
 | |
| 	// Parse server address | |
| 	serverAddr := pool.config.Server | |
| 	if strings.HasPrefix(serverAddr, "ldap://") { | |
| 		serverAddr = strings.TrimPrefix(serverAddr, "ldap://") | |
| 	} else if strings.HasPrefix(serverAddr, "ldaps://") { | |
| 		serverAddr = strings.TrimPrefix(serverAddr, "ldaps://") | |
| 	} | |
| 
 | |
| 	// Add default port if not specified | |
| 	if !strings.Contains(serverAddr, ":") { | |
| 		if strings.HasPrefix(pool.config.Server, "ldaps://") { | |
| 			serverAddr += ":636" | |
| 		} else { | |
| 			serverAddr += ":389" | |
| 		} | |
| 	} | |
| 
 | |
| 	if strings.HasPrefix(pool.config.Server, "ldaps://") { | |
| 		// LDAPS connection | |
| 		tlsConfig := &tls.Config{ | |
| 			InsecureSkipVerify: pool.config.TLSSkipVerify, | |
| 		} | |
| 		dialer := &net.Dialer{Timeout: timeout} | |
| 		netConn, err = tls.DialWithDialer(dialer, "tcp", serverAddr, tlsConfig) | |
| 	} else { | |
| 		// Plain LDAP connection | |
| 		netConn, err = net.DialTimeout("tcp", serverAddr, timeout) | |
| 	} | |
| 
 | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to connect to LDAP server %s: %v", pool.config.Server, err) | |
| 	} | |
| 
 | |
| 	conn := &LDAPConn{ | |
| 		serverAddr: serverAddr, | |
| 		conn:       netConn, | |
| 		bound:      false, | |
| 		tlsConfig: &tls.Config{ | |
| 			InsecureSkipVerify: pool.config.TLSSkipVerify, | |
| 		}, | |
| 	} | |
| 
 | |
| 	// Start TLS if configured and not already using LDAPS | |
| 	if pool.config.UseTLS && !strings.HasPrefix(pool.config.Server, "ldaps://") { | |
| 		err = conn.StartTLS(conn.tlsConfig) | |
| 		if err != nil { | |
| 			conn.Close() | |
| 			return nil, fmt.Errorf("failed to start TLS: %v", err) | |
| 		} | |
| 	} | |
| 
 | |
| 	return conn, nil | |
| } | |
| 
 | |
| // GetConnection retrieves a connection from the pool | |
| func (pool *LDAPConnectionPool) GetConnection() (*LDAPConn, error) { | |
| 	select { | |
| 	case conn := <-pool.connections: | |
| 		// Test if connection is still valid | |
| 		if pool.isConnectionValid(conn) { | |
| 			return conn, nil | |
| 		} | |
| 		// Connection is stale, close it and create a new one | |
| 		conn.Close() | |
| 	default: | |
| 		// No connection available in pool | |
| 	} | |
| 
 | |
| 	// Create a new connection | |
| 	return pool.createConnection() | |
| } | |
| 
 | |
| // ReleaseConnection returns a connection to the pool | |
| func (pool *LDAPConnectionPool) ReleaseConnection(conn *LDAPConn) { | |
| 	if conn == nil { | |
| 		return | |
| 	} | |
| 
 | |
| 	select { | |
| 	case pool.connections <- conn: | |
| 		// Successfully returned to pool | |
| 	default: | |
| 		// Pool is full, close the connection | |
| 		conn.Close() | |
| 	} | |
| } | |
| 
 | |
| // isConnectionValid tests if a connection is still valid | |
| func (pool *LDAPConnectionPool) isConnectionValid(conn *LDAPConn) bool { | |
| 	// Simple test: check if underlying connection is still open | |
| 	if conn == nil || conn.conn == nil { | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Try to perform a simple operation to test connectivity | |
| 	searchRequest := &LDAPSearchRequest{ | |
| 		BaseDN:       "", | |
| 		Scope:        ScopeBaseObject, | |
| 		DerefAliases: NeverDerefAliases, | |
| 		SizeLimit:    0, | |
| 		TimeLimit:    0, | |
| 		TypesOnly:    false, | |
| 		Filter:       "(objectClass=*)", | |
| 		Attributes:   []string{"1.1"}, // Minimal attributes | |
| 	} | |
| 
 | |
| 	_, err := conn.Search(searchRequest) | |
| 	return err == nil | |
| } | |
| 
 | |
| // Close closes all connections in the pool | |
| func (pool *LDAPConnectionPool) Close() { | |
| 	pool.mu.Lock() | |
| 	defer pool.mu.Unlock() | |
| 
 | |
| 	close(pool.connections) | |
| 	for conn := range pool.connections { | |
| 		conn.Close() | |
| 	} | |
| } | |
| 
 | |
| // Helper functions and LDAP connection methods | |
|  | |
| // EscapeFilter escapes special characters in LDAP filter values | |
| func EscapeFilter(filter string) string { | |
| 	// Basic LDAP filter escaping | |
| 	filter = strings.ReplaceAll(filter, "\\", "\\5c") | |
| 	filter = strings.ReplaceAll(filter, "*", "\\2a") | |
| 	filter = strings.ReplaceAll(filter, "(", "\\28") | |
| 	filter = strings.ReplaceAll(filter, ")", "\\29") | |
| 	filter = strings.ReplaceAll(filter, "/", "\\2f") | |
| 	filter = strings.ReplaceAll(filter, "=", "\\3d") | |
| 	return filter | |
| } | |
| 
 | |
| // LDAPConn methods | |
|  | |
| // Bind performs an LDAP bind operation | |
| func (conn *LDAPConn) Bind(bindDN, bindPassword string) error { | |
| 	if conn == nil || conn.conn == nil { | |
| 		return fmt.Errorf("connection is nil") | |
| 	} | |
| 
 | |
| 	// In a real implementation, this would send an LDAP bind request | |
| 	// For now, we simulate the bind operation | |
| 	glog.V(3).Infof("LDAP Bind attempt for DN: %s", bindDN) | |
| 
 | |
| 	// Simple validation | |
| 	if bindDN == "" { | |
| 		return fmt.Errorf("bind DN cannot be empty") | |
| 	} | |
| 
 | |
| 	// Simulate bind success for valid credentials | |
| 	if bindPassword != "" { | |
| 		conn.bound = true | |
| 		return nil | |
| 	} | |
| 
 | |
| 	return fmt.Errorf("invalid credentials") | |
| } | |
| 
 | |
| // Search performs an LDAP search operation | |
| func (conn *LDAPConn) Search(searchRequest *LDAPSearchRequest) (*LDAPSearchResult, error) { | |
| 	if conn == nil || conn.conn == nil { | |
| 		return nil, fmt.Errorf("connection is nil") | |
| 	} | |
| 
 | |
| 	glog.V(3).Infof("LDAP Search - BaseDN: %s, Filter: %s", searchRequest.BaseDN, searchRequest.Filter) | |
| 
 | |
| 	// In a real implementation, this would send an LDAP search request | |
| 	// For now, we simulate a search operation | |
| 	result := &LDAPSearchResult{ | |
| 		Entries: []*LDAPEntry{}, | |
| 	} | |
| 
 | |
| 	// Simulate finding a test user for certain searches | |
| 	if strings.Contains(searchRequest.Filter, "testuser") || strings.Contains(searchRequest.Filter, "admin") { | |
| 		entry := &LDAPEntry{ | |
| 			DN: fmt.Sprintf("uid=%s,%s", "testuser", searchRequest.BaseDN), | |
| 			Attributes: []*LDAPAttribute{ | |
| 				{Name: "uid", Values: []string{"testuser"}}, | |
| 				{Name: "mail", Values: []string{"testuser@example.com"}}, | |
| 				{Name: "cn", Values: []string{"Test User"}}, | |
| 				{Name: "memberOf", Values: []string{"cn=users,ou=groups," + searchRequest.BaseDN}}, | |
| 			}, | |
| 		} | |
| 		result.Entries = append(result.Entries, entry) | |
| 	} | |
| 
 | |
| 	return result, nil | |
| } | |
| 
 | |
| // Close closes the LDAP connection | |
| func (conn *LDAPConn) Close() error { | |
| 	if conn != nil && conn.conn != nil { | |
| 		return conn.conn.Close() | |
| 	} | |
| 	return nil | |
| } | |
| 
 | |
| // StartTLS starts TLS on the connection | |
| func (conn *LDAPConn) StartTLS(config *tls.Config) error { | |
| 	if conn == nil || conn.conn == nil { | |
| 		return fmt.Errorf("connection is nil") | |
| 	} | |
| 
 | |
| 	// In a real implementation, this would upgrade the connection to TLS | |
| 	glog.V(3).Info("LDAP StartTLS operation") | |
| 	return nil | |
| } | |
| 
 | |
| // LDAPEntry methods | |
|  | |
| // GetAttributeValue returns the first value of the specified attribute | |
| func (entry *LDAPEntry) GetAttributeValue(attrName string) string { | |
| 	for _, attr := range entry.Attributes { | |
| 		if attr.Name == attrName && len(attr.Values) > 0 { | |
| 			return attr.Values[0] | |
| 		} | |
| 	} | |
| 	return "" | |
| }
 |