Fix support for multiple LDAP user formats (#11276)

Fixes support for using multiple base DNs for user search in the LDAP directory
allowing users from different subtrees in the LDAP hierarchy to request
credentials.

- The username in the produced credentials is now the full DN of the LDAP user
to disambiguate users in different base DNs.
master
Aditya Manthramurthy 4 years ago committed by GitHub
parent 0dadfd1b3d
commit 3163a660aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 121
      cmd/config/identity/ldap/config.go
  2. 2
      cmd/config/identity/ldap/legacy.go
  3. 8
      cmd/sts-handlers.go
  4. 2
      docs/sts/ldap.md

@ -47,11 +47,9 @@ type Config struct {
// Format string for usernames // Format string for usernames
UsernameFormat string `json:"usernameFormat"` UsernameFormat string `json:"usernameFormat"`
UsernameFormats []string `json:"-"` UsernameFormats []string `json:"-"`
UsernameSearchFilter string `json:"-"`
UsernameSearchBaseDNS []string `json:"-"`
GroupSearchBaseDN string `json:"groupSearchBaseDN"` GroupSearchBaseDistName string `json:"groupSearchBaseDN"`
GroupSearchBaseDNS []string `json:"-"` GroupSearchBaseDistNames []string `json:"-"`
GroupSearchFilter string `json:"groupSearchFilter"` GroupSearchFilter string `json:"groupSearchFilter"`
GroupNameAttribute string `json:"groupNameAttribute"` GroupNameAttribute string `json:"groupNameAttribute"`
@ -82,8 +80,6 @@ const (
EnvServerInsecure = "MINIO_IDENTITY_LDAP_SERVER_INSECURE" EnvServerInsecure = "MINIO_IDENTITY_LDAP_SERVER_INSECURE"
EnvServerStartTLS = "MINIO_IDENTITY_LDAP_SERVER_STARTTLS" EnvServerStartTLS = "MINIO_IDENTITY_LDAP_SERVER_STARTTLS"
EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT" EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT"
EnvUsernameSearchFilter = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER"
EnvUsernameSearchBaseDN = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_BASE_DN"
EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER" EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"
EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE" EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE"
EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN" EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"
@ -157,96 +153,82 @@ func getGroups(conn *ldap.Conn, sreq *ldap.SearchRequest) ([]string, error) {
return groups, nil return groups, nil
} }
func (l *Config) bind(conn *ldap.Conn, username, password string) ([]string, error) { // bind - Iterates over all given username formats and expects that only one
var bindDNS = make([]string, len(l.UsernameFormats)) // will succeed if the credentials are valid. The succeeding bindDN is returned
// or an error.
//
// In the rare case that multiple username formats succeed, implying that two
// (or more) distinct users in the LDAP directory have the same username and
// password, we return an error as we cannot identify the account intended by
// the user.
func (l *Config) bind(conn *ldap.Conn, username, password string) (string, error) {
var bindDistNames []string
var errs = make([]error, len(l.UsernameFormats))
var successCount = 0
for i, usernameFormat := range l.UsernameFormats { for i, usernameFormat := range l.UsernameFormats {
bindDN := fmt.Sprintf(usernameFormat, username) bindDN := fmt.Sprintf(usernameFormat, username)
// Bind with user credentials to validate the password // Bind with user credentials to validate the password
if err := conn.Bind(bindDN, password); err != nil { errs[i] = conn.Bind(bindDN, password)
return nil, err if errs[i] == nil {
bindDistNames = append(bindDistNames, bindDN)
successCount++
} }
bindDNS[i] = bindDN
} }
return bindDNS, nil if successCount == 0 {
} var errStrings []string = []string{"All username formats failed with: "}
for _, err := range errs {
var standardAttributes = []string{ if err != nil {
"givenName", errStrings = append(errStrings, err.Error())
"sn", }
"cn", }
"memberOf", outErr := strings.Join(errStrings, "; ")
"email", return "", errors.New(outErr)
}
if successCount > 1 {
successDistNames := strings.Join(bindDistNames, ", ")
errMsg := fmt.Sprintf("Multiple username formats succeeded - ambiguous user login (succeeded for: %s)", successDistNames)
return "", errors.New(errMsg)
}
return bindDistNames[0], nil
} }
// Bind - binds to ldap, searches LDAP and returns list of groups. // Bind - binds to ldap, searches LDAP and returns the distinguished name of the
func (l *Config) Bind(username, password string) ([]string, error) { // user and the list of groups.
func (l *Config) Bind(username, password string) (string, []string, error) {
conn, err := l.Connect() conn, err := l.Connect()
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
defer conn.Close() defer conn.Close()
bindDNS, err := l.bind(conn, username, password) bindDN, err := l.bind(conn, username, password)
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
var groups []string var groups []string
if l.UsernameSearchFilter != "" {
for _, userSearchBase := range l.UsernameSearchBaseDNS {
filter := strings.Replace(l.UsernameSearchFilter, "%s",
ldap.EscapeFilter(username), -1)
searchRequest := ldap.NewSearchRequest(
userSearchBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter,
standardAttributes,
nil,
)
groups, err = getGroups(conn, searchRequest)
if err != nil {
return nil, err
}
}
}
if l.GroupSearchFilter != "" { if l.GroupSearchFilter != "" {
for _, groupSearchBase := range l.GroupSearchBaseDNS { for _, groupSearchBase := range l.GroupSearchBaseDistNames {
var filters []string filter := strings.Replace(l.GroupSearchFilter, "%s", ldap.EscapeFilter(bindDN), -1)
if l.GroupNameAttribute == "" {
filters = []string{strings.Replace(l.GroupSearchFilter, "%s",
ldap.EscapeFilter(username), -1)}
} else {
// With group name attribute specified, make sure to
// include search queries for CN distinguished name
for _, bindDN := range bindDNS {
filters = append(filters, strings.Replace(l.GroupSearchFilter, "%s",
ldap.EscapeFilter(bindDN), -1))
}
}
for _, filter := range filters {
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
groupSearchBase, groupSearchBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter, filter,
standardAttributes, []string{l.GroupNameAttribute},
nil, nil,
) )
var newGroups []string var newGroups []string
newGroups, err = getGroups(conn, searchRequest) newGroups, err = getGroups(conn, searchRequest)
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
groups = append(groups, newGroups...) groups = append(groups, newGroups...)
} }
} }
}
return groups, nil return bindDN, groups, nil
} }
// Connect connect to ldap server. // Connect connect to ldap server.
@ -343,17 +325,6 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
return l, fmt.Errorf("'%s' cannot be empty and must have a value", UsernameFormat) return l, fmt.Errorf("'%s' cannot be empty and must have a value", UsernameFormat)
} }
if v := env.Get(EnvUsernameSearchFilter, kvs.Get(UsernameSearchFilter)); v != "" {
if !strings.Contains(v, "%s") {
return l, errors.New("LDAP username search filter doesn't have '%s' substitution")
}
l.UsernameSearchFilter = v
}
if v := env.Get(EnvUsernameSearchBaseDN, kvs.Get(UsernameSearchBaseDN)); v != "" {
l.UsernameSearchBaseDNS = strings.Split(v, dnDelimiter)
}
grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter)) grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter))
grpSearchNameAttr := env.Get(EnvGroupNameAttribute, kvs.Get(GroupNameAttribute)) grpSearchNameAttr := env.Get(EnvGroupNameAttribute, kvs.Get(GroupNameAttribute))
grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN)) grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN))
@ -370,7 +341,7 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
if allSet { if allSet {
l.GroupSearchFilter = grpSearchFilter l.GroupSearchFilter = grpSearchFilter
l.GroupNameAttribute = grpSearchNameAttr l.GroupNameAttribute = grpSearchNameAttr
l.GroupSearchBaseDNS = strings.Split(grpSearchBaseDN, dnDelimiter) l.GroupSearchBaseDistNames = strings.Split(l.GroupSearchBaseDistName, dnDelimiter)
} }
l.rootCAs = rootCAs l.rootCAs = rootCAs

@ -47,7 +47,7 @@ func SetIdentityLDAP(s config.Config, ldapArgs Config) {
}, },
config.KV{ config.KV{
Key: GroupSearchBaseDN, Key: GroupSearchBaseDN,
Value: ldapArgs.GroupSearchBaseDN, Value: ldapArgs.GroupSearchBaseDistName,
}, },
} }
} }

@ -490,9 +490,9 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
} }
} }
groups, err := globalLDAPConfig.Bind(ldapUsername, ldapPassword) ldapUserDN, groups, err := globalLDAPConfig.Bind(ldapUsername, ldapPassword)
if err != nil { if err != nil {
err = fmt.Errorf("LDAP server connection failure: %w", err) err = fmt.Errorf("LDAP server error: %w", err)
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err)
return return
} }
@ -500,7 +500,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
expiryDur := globalLDAPConfig.GetExpiryDuration() expiryDur := globalLDAPConfig.GetExpiryDuration()
m := map[string]interface{}{ m := map[string]interface{}{
expClaim: UTCNow().Add(expiryDur).Unix(), expClaim: UTCNow().Add(expiryDur).Unix(),
ldapUser: ldapUsername, ldapUser: ldapUserDN,
} }
if len(sessionPolicyStr) > 0 { if len(sessionPolicyStr) > 0 {
@ -516,7 +516,7 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
// Set the parent of the temporary access key, this is useful // Set the parent of the temporary access key, this is useful
// in obtaining service accounts by this cred. // in obtaining service accounts by this cred.
cred.ParentUser = ldapUsername cred.ParentUser = ldapUserDN
// Set this value to LDAP groups, LDAP user can be part // Set this value to LDAP groups, LDAP user can be part
// of large number of groups // of large number of groups

@ -47,10 +47,8 @@ identity_ldap enable LDAP SSO support
ARGS: ARGS:
MINIO_IDENTITY_LDAP_SERVER_ADDR* (address) AD/LDAP server address e.g. "myldapserver.com:636" MINIO_IDENTITY_LDAP_SERVER_ADDR* (address) AD/LDAP server address e.g. "myldapserver.com:636"
MINIO_IDENTITY_LDAP_USERNAME_FORMAT* (list) ";" separated list of username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com" MINIO_IDENTITY_LDAP_USERNAME_FORMAT* (list) ";" separated list of username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com"
MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER* (string) user search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER* (string) search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))" MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER* (string) search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"
MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN* (list) ";" separated list of group search base DNs e.g. "dc=myldapserver,dc=com" MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN* (list) ";" separated list of group search base DNs e.g. "dc=myldapserver,dc=com"
MINIO_IDENTITY_LDAP_USERNAME_SEARCH_BASE_DN (list) ";" separated list of username search DNs
MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE (string) search attribute for group name e.g. "cn" MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE (string) search attribute for group name e.g. "cn"
MINIO_IDENTITY_LDAP_STS_EXPIRY (duration) temporary credentials validity duration in s,m,h,d. Default is "1h" MINIO_IDENTITY_LDAP_STS_EXPIRY (duration) temporary credentials validity duration in s,m,h,d. Default is "1h"
MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY (on|off) trust server TLS without verification, defaults to "off" (verify) MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY (on|off) trust server TLS without verification, defaults to "off" (verify)

Loading…
Cancel
Save