Browse Source
fix: address PR review security issues for STS handlers
fix: address PR review security issues for STS handlers
This commit addresses all critical security issues from PR review: Security Fixes: - Use crypto/rand for cryptographically secure credential generation instead of time.Now().UnixNano() (fixes predictable credentials) - Add sts:AssumeRole permission check via VerifyActionPermission to prevent unauthorized role assumption - Generate proper session tokens using crypto/rand instead of placeholder strings Code Quality Improvements: - Refactor DurationSeconds parsing into reusable parseDurationSeconds() helper function used by all three STS handlers - Create generateSecureCredentials() helper for consistent and secure temporary credential generation - Fix iamMatcher to check query string as fallback when Action not found in form data LDAP Provider Implementation: - Add go-ldap/ldap/v3 dependency - Create LDAPProvider implementing IdentityProvider interface with full LDAP authentication support (connect, bind, search, groups) - Update ProviderFactory to create real LDAP providers - Wire LDAP provider into AssumeRoleWithLDAPIdentity handler Test Infrastructure: - Add LDAP user creation verification step in setup_all_tests.shpull/8003/head
7 changed files with 622 additions and 102 deletions
-
2go.mod
-
4go.sum
-
8test/s3/iam/setup_all_tests.sh
-
428weed/iam/ldap/ldap_provider.go
-
8weed/iam/sts/provider_factory.go
-
20weed/s3api/s3api_server.go
-
254weed/s3api/s3api_sts.go
@ -0,0 +1,428 @@ |
|||
package ldap |
|||
|
|||
import ( |
|||
"context" |
|||
"crypto/tls" |
|||
"fmt" |
|||
"strings" |
|||
"sync" |
|||
"time" |
|||
|
|||
"github.com/go-ldap/ldap/v3" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/iam/providers" |
|||
) |
|||
|
|||
// LDAPConfig holds configuration for LDAP provider
|
|||
type LDAPConfig struct { |
|||
// Server is the LDAP server URL (ldap:// or ldaps://)
|
|||
Server string `json:"server"` |
|||
|
|||
// BindDN is the DN used to bind for searches (optional for anonymous bind)
|
|||
BindDN string `json:"bindDN,omitempty"` |
|||
|
|||
// BindPassword is the password for the bind DN
|
|||
BindPassword string `json:"bindPassword,omitempty"` |
|||
|
|||
// BaseDN is the base DN for user searches
|
|||
BaseDN string `json:"baseDN"` |
|||
|
|||
// UserFilter is the filter to find users (use %s for username placeholder)
|
|||
// Example: "(uid=%s)" or "(cn=%s)" or "(&(objectClass=person)(uid=%s))"
|
|||
UserFilter string `json:"userFilter"` |
|||
|
|||
// GroupFilter is the filter to find user groups (use %s for user DN placeholder)
|
|||
// Example: "(member=%s)" or "(memberUid=%s)"
|
|||
GroupFilter string `json:"groupFilter,omitempty"` |
|||
|
|||
// GroupBaseDN is the base DN for group searches (defaults to BaseDN)
|
|||
GroupBaseDN string `json:"groupBaseDN,omitempty"` |
|||
|
|||
// Attributes to retrieve from LDAP
|
|||
Attributes LDAPAttributes `json:"attributes,omitempty"` |
|||
|
|||
// UseTLS enables StartTLS
|
|||
UseTLS bool `json:"useTLS,omitempty"` |
|||
|
|||
// InsecureSkipVerify skips TLS certificate verification
|
|||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` |
|||
|
|||
// ConnectionTimeout is the connection timeout
|
|||
ConnectionTimeout time.Duration `json:"connectionTimeout,omitempty"` |
|||
} |
|||
|
|||
// LDAPAttributes maps LDAP attribute names
|
|||
type LDAPAttributes struct { |
|||
Email string `json:"email,omitempty"` // Default: mail
|
|||
DisplayName string `json:"displayName,omitempty"` // Default: cn
|
|||
Groups string `json:"groups,omitempty"` // Default: memberOf
|
|||
UID string `json:"uid,omitempty"` // Default: uid
|
|||
} |
|||
|
|||
// LDAPProvider implements the IdentityProvider interface for LDAP
|
|||
type LDAPProvider struct { |
|||
name string |
|||
config *LDAPConfig |
|||
initialized bool |
|||
mu sync.RWMutex |
|||
} |
|||
|
|||
// 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 provider with configuration
|
|||
func (p *LDAPProvider) Initialize(config interface{}) error { |
|||
p.mu.Lock() |
|||
defer p.mu.Unlock() |
|||
|
|||
cfg, ok := config.(*LDAPConfig) |
|||
if !ok { |
|||
// Try to convert from map
|
|||
if cfgMap, ok := config.(map[string]interface{}); ok { |
|||
cfg = &LDAPConfig{} |
|||
if v, ok := cfgMap["server"].(string); ok { |
|||
cfg.Server = v |
|||
} |
|||
if v, ok := cfgMap["bindDN"].(string); ok { |
|||
cfg.BindDN = v |
|||
} |
|||
if v, ok := cfgMap["bindPassword"].(string); ok { |
|||
cfg.BindPassword = v |
|||
} |
|||
if v, ok := cfgMap["baseDN"].(string); ok { |
|||
cfg.BaseDN = v |
|||
} |
|||
if v, ok := cfgMap["userFilter"].(string); ok { |
|||
cfg.UserFilter = v |
|||
} |
|||
if v, ok := cfgMap["groupFilter"].(string); ok { |
|||
cfg.GroupFilter = v |
|||
} |
|||
if v, ok := cfgMap["groupBaseDN"].(string); ok { |
|||
cfg.GroupBaseDN = v |
|||
} |
|||
if v, ok := cfgMap["useTLS"].(bool); ok { |
|||
cfg.UseTLS = v |
|||
} |
|||
if v, ok := cfgMap["insecureSkipVerify"].(bool); ok { |
|||
cfg.InsecureSkipVerify = v |
|||
} |
|||
// Parse attributes
|
|||
if attrs, ok := cfgMap["attributes"].(map[string]interface{}); ok { |
|||
if v, ok := attrs["email"].(string); ok { |
|||
cfg.Attributes.Email = v |
|||
} |
|||
if v, ok := attrs["displayName"].(string); ok { |
|||
cfg.Attributes.DisplayName = v |
|||
} |
|||
if v, ok := attrs["groups"].(string); ok { |
|||
cfg.Attributes.Groups = v |
|||
} |
|||
if v, ok := attrs["uid"].(string); ok { |
|||
cfg.Attributes.UID = v |
|||
} |
|||
} |
|||
} else { |
|||
return fmt.Errorf("invalid LDAP configuration type: %T", config) |
|||
} |
|||
} |
|||
|
|||
// Validate required fields
|
|||
if cfg.Server == "" { |
|||
return fmt.Errorf("LDAP server URL is required") |
|||
} |
|||
if cfg.BaseDN == "" { |
|||
return fmt.Errorf("LDAP base DN is required") |
|||
} |
|||
if cfg.UserFilter == "" { |
|||
cfg.UserFilter = "(cn=%s)" // Default filter
|
|||
} |
|||
|
|||
// Set default attributes
|
|||
if cfg.Attributes.Email == "" { |
|||
cfg.Attributes.Email = "mail" |
|||
} |
|||
if cfg.Attributes.DisplayName == "" { |
|||
cfg.Attributes.DisplayName = "cn" |
|||
} |
|||
if cfg.Attributes.Groups == "" { |
|||
cfg.Attributes.Groups = "memberOf" |
|||
} |
|||
if cfg.Attributes.UID == "" { |
|||
cfg.Attributes.UID = "uid" |
|||
} |
|||
if cfg.GroupBaseDN == "" { |
|||
cfg.GroupBaseDN = cfg.BaseDN |
|||
} |
|||
if cfg.ConnectionTimeout == 0 { |
|||
cfg.ConnectionTimeout = 10 * time.Second |
|||
} |
|||
|
|||
p.config = cfg |
|||
p.initialized = true |
|||
|
|||
glog.V(1).Infof("LDAP provider '%s' initialized: server=%s, baseDN=%s", |
|||
p.name, cfg.Server, cfg.BaseDN) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// connect establishes a connection to the LDAP server
|
|||
func (p *LDAPProvider) connect() (*ldap.Conn, error) { |
|||
var conn *ldap.Conn |
|||
var err error |
|||
|
|||
// Parse server URL
|
|||
if strings.HasPrefix(p.config.Server, "ldaps://") { |
|||
// LDAPS connection
|
|||
tlsConfig := &tls.Config{ |
|||
InsecureSkipVerify: p.config.InsecureSkipVerify, |
|||
} |
|||
conn, err = ldap.DialURL(p.config.Server, ldap.DialWithTLSConfig(tlsConfig)) |
|||
} else { |
|||
// LDAP connection
|
|||
conn, err = ldap.DialURL(p.config.Server) |
|||
if err == nil && p.config.UseTLS { |
|||
// StartTLS
|
|||
tlsConfig := &tls.Config{ |
|||
InsecureSkipVerify: p.config.InsecureSkipVerify, |
|||
} |
|||
err = conn.StartTLS(tlsConfig) |
|||
} |
|||
} |
|||
|
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to connect to LDAP server: %w", err) |
|||
} |
|||
|
|||
return conn, nil |
|||
} |
|||
|
|||
// Authenticate authenticates a user with username:password credentials
|
|||
func (p *LDAPProvider) Authenticate(ctx context.Context, credentials string) (*providers.ExternalIdentity, error) { |
|||
p.mu.RLock() |
|||
if !p.initialized { |
|||
p.mu.RUnlock() |
|||
return nil, fmt.Errorf("LDAP provider not initialized") |
|||
} |
|||
config := p.config |
|||
p.mu.RUnlock() |
|||
|
|||
// 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] |
|||
|
|||
if username == "" || password == "" { |
|||
return nil, fmt.Errorf("username and password are required") |
|||
} |
|||
|
|||
// Connect to LDAP
|
|||
conn, err := p.connect() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
defer conn.Close() |
|||
|
|||
// First, bind with service account to search for user
|
|||
if config.BindDN != "" { |
|||
err = conn.Bind(config.BindDN, config.BindPassword) |
|||
if err != nil { |
|||
glog.V(2).Infof("LDAP service bind failed: %v", err) |
|||
return nil, fmt.Errorf("LDAP service bind failed: %w", err) |
|||
} |
|||
} |
|||
|
|||
// Search for the user
|
|||
userFilter := fmt.Sprintf(config.UserFilter, ldap.EscapeFilter(username)) |
|||
searchRequest := ldap.NewSearchRequest( |
|||
config.BaseDN, |
|||
ldap.ScopeWholeSubtree, |
|||
ldap.NeverDerefAliases, |
|||
1, // Size limit
|
|||
int(config.ConnectionTimeout.Seconds()), |
|||
false, |
|||
userFilter, |
|||
[]string{"dn", config.Attributes.Email, config.Attributes.DisplayName, config.Attributes.UID, config.Attributes.Groups}, |
|||
nil, |
|||
) |
|||
|
|||
result, err := conn.Search(searchRequest) |
|||
if err != nil { |
|||
glog.V(2).Infof("LDAP user search failed: %v", err) |
|||
return nil, fmt.Errorf("LDAP user search failed: %w", err) |
|||
} |
|||
|
|||
if len(result.Entries) == 0 { |
|||
return nil, fmt.Errorf("user not found") |
|||
} |
|||
if len(result.Entries) > 1 { |
|||
return nil, fmt.Errorf("multiple users found") |
|||
} |
|||
|
|||
userEntry := result.Entries[0] |
|||
userDN := userEntry.DN |
|||
|
|||
// Bind as the user to verify password
|
|||
err = conn.Bind(userDN, password) |
|||
if err != nil { |
|||
glog.V(2).Infof("LDAP user bind failed for %s: %v", username, err) |
|||
return nil, fmt.Errorf("authentication failed: invalid credentials") |
|||
} |
|||
|
|||
// Build identity from LDAP attributes
|
|||
identity := &providers.ExternalIdentity{ |
|||
UserID: username, |
|||
Email: userEntry.GetAttributeValue(config.Attributes.Email), |
|||
DisplayName: userEntry.GetAttributeValue(config.Attributes.DisplayName), |
|||
Groups: userEntry.GetAttributeValues(config.Attributes.Groups), |
|||
Provider: p.name, |
|||
Attributes: map[string]string{ |
|||
"dn": userDN, |
|||
"uid": userEntry.GetAttributeValue(config.Attributes.UID), |
|||
}, |
|||
} |
|||
|
|||
// If no groups from memberOf, try group search
|
|||
if len(identity.Groups) == 0 && config.GroupFilter != "" { |
|||
groups, err := p.searchUserGroups(conn, userDN, config) |
|||
if err != nil { |
|||
glog.V(2).Infof("Group search failed for %s: %v", username, err) |
|||
} else { |
|||
identity.Groups = groups |
|||
} |
|||
} |
|||
|
|||
glog.V(2).Infof("LDAP authentication successful for user: %s, groups: %v", username, identity.Groups) |
|||
return identity, nil |
|||
} |
|||
|
|||
// searchUserGroups searches for groups the user belongs to
|
|||
func (p *LDAPProvider) searchUserGroups(conn *ldap.Conn, userDN string, config *LDAPConfig) ([]string, error) { |
|||
groupFilter := fmt.Sprintf(config.GroupFilter, ldap.EscapeFilter(userDN)) |
|||
searchRequest := ldap.NewSearchRequest( |
|||
config.GroupBaseDN, |
|||
ldap.ScopeWholeSubtree, |
|||
ldap.NeverDerefAliases, |
|||
0, |
|||
int(config.ConnectionTimeout.Seconds()), |
|||
false, |
|||
groupFilter, |
|||
[]string{"cn", "dn"}, |
|||
nil, |
|||
) |
|||
|
|||
result, err := conn.Search(searchRequest) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
var groups []string |
|||
for _, entry := range result.Entries { |
|||
cn := entry.GetAttributeValue("cn") |
|||
if cn != "" { |
|||
groups = append(groups, cn) |
|||
} |
|||
} |
|||
|
|||
return groups, nil |
|||
} |
|||
|
|||
// GetUserInfo retrieves user information by user ID
|
|||
func (p *LDAPProvider) GetUserInfo(ctx context.Context, userID string) (*providers.ExternalIdentity, error) { |
|||
p.mu.RLock() |
|||
if !p.initialized { |
|||
p.mu.RUnlock() |
|||
return nil, fmt.Errorf("LDAP provider not initialized") |
|||
} |
|||
config := p.config |
|||
p.mu.RUnlock() |
|||
|
|||
// Connect to LDAP
|
|||
conn, err := p.connect() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
defer conn.Close() |
|||
|
|||
// Bind with service account
|
|||
if config.BindDN != "" { |
|||
err = conn.Bind(config.BindDN, config.BindPassword) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("LDAP service bind failed: %w", err) |
|||
} |
|||
} |
|||
|
|||
// Search for the user
|
|||
userFilter := fmt.Sprintf(config.UserFilter, ldap.EscapeFilter(userID)) |
|||
searchRequest := ldap.NewSearchRequest( |
|||
config.BaseDN, |
|||
ldap.ScopeWholeSubtree, |
|||
ldap.NeverDerefAliases, |
|||
1, |
|||
int(config.ConnectionTimeout.Seconds()), |
|||
false, |
|||
userFilter, |
|||
[]string{"dn", config.Attributes.Email, config.Attributes.DisplayName, config.Attributes.UID, config.Attributes.Groups}, |
|||
nil, |
|||
) |
|||
|
|||
result, err := conn.Search(searchRequest) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("LDAP user search failed: %w", err) |
|||
} |
|||
|
|||
if len(result.Entries) == 0 { |
|||
return nil, fmt.Errorf("user not found") |
|||
} |
|||
|
|||
userEntry := result.Entries[0] |
|||
return &providers.ExternalIdentity{ |
|||
UserID: userID, |
|||
Email: userEntry.GetAttributeValue(config.Attributes.Email), |
|||
DisplayName: userEntry.GetAttributeValue(config.Attributes.DisplayName), |
|||
Groups: userEntry.GetAttributeValues(config.Attributes.Groups), |
|||
Provider: p.name, |
|||
Attributes: map[string]string{ |
|||
"dn": userEntry.DN, |
|||
"uid": userEntry.GetAttributeValue(config.Attributes.UID), |
|||
}, |
|||
}, nil |
|||
} |
|||
|
|||
// ValidateToken validates credentials (username:password format) and returns claims
|
|||
func (p *LDAPProvider) ValidateToken(ctx context.Context, token string) (*providers.TokenClaims, error) { |
|||
identity, err := p.Authenticate(ctx, token) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return &providers.TokenClaims{ |
|||
Subject: identity.UserID, |
|||
Claims: map[string]interface{}{ |
|||
"email": identity.Email, |
|||
"name": identity.DisplayName, |
|||
"groups": identity.Groups, |
|||
"dn": identity.Attributes["dn"], |
|||
"provider": p.name, |
|||
}, |
|||
}, nil |
|||
} |
|||
|
|||
// IsInitialized returns whether the provider is initialized
|
|||
func (p *LDAPProvider) IsInitialized() bool { |
|||
p.mu.RLock() |
|||
defer p.mu.RUnlock() |
|||
return p.initialized |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue