lib/api: Add LDAP search filters (fixes #5376) (#6488)

This adds the functionality to run a user search with a filter for LDAP
authentication. The search is done after successful bind, as the binding
user. The typical use case is to limit authentication to users who are
member of a group or under a certain OU. For example, to only match
users in the "Syncthing" group in otherwise default Active Directory
set up for example.com:

    <searchBaseDN>CN=Users,DC=example,DC=com</searchBaseDN>
    <searchFilter>(&amp;(sAMAccountName=%s)(memberOf=CN=Syncthing,CN=Users,DC=example,DC=com))</searchFilter>

The search filter is an "and" of two criteria (with the ampersand being
XML quoted),

- "(sAMAccountName=%s)" matches the user logging in
- "(memberOf=CN=Syncthing,CN=Users,DC=example,DC=com)" matches members
  of the group in question.

Authentication will only proceed if the search filter matches precisely
one user.
This commit is contained in:
Jakob Borg 2020-04-04 11:33:43 +02:00 committed by GitHub
parent f69c0b550c
commit 48f9d323fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 31 additions and 0 deletions

View File

@ -166,6 +166,35 @@ func authLDAP(username string, password string, cfg config.LDAPConfiguration) bo
return false return false
} }
if cfg.SearchFilter == "" && cfg.SearchBaseDN == "" {
// We're done here.
return true
}
if cfg.SearchFilter == "" || cfg.SearchBaseDN == "" {
l.Warnln("LDAP configuration: both searchFilter and searchBaseDN must be set, or neither.")
return false
}
// If a search filter and search base is set we do an LDAP search for
// the user. If this matches precisely one user then we are good to go.
// The search filter uses the same %s interpolation as the bind DN.
searchString := fmt.Sprintf(cfg.SearchFilter, username)
const sizeLimit = 2 // we search for up to two users -- we only want to match one, so getting any number >1 is a failure.
const timeLimit = 60 // Search for up to a minute...
searchReq := ldap.NewSearchRequest(cfg.SearchBaseDN, ldap.ScopeWholeSubtree, ldap.DerefFindingBaseObj, sizeLimit, timeLimit, false, searchString, nil, nil)
res, err := connection.Search(searchReq)
if err != nil {
l.Warnln("LDAP Search:", err)
return false
}
if len(res.Entries) != 1 {
l.Infof("Wrong number of LDAP search results, %d != 1", len(res.Entries))
return false
}
return true return true
} }

View File

@ -11,6 +11,8 @@ type LDAPConfiguration struct {
BindDN string `xml:"bindDN,omitempty" json:"bindDN"` BindDN string `xml:"bindDN,omitempty" json:"bindDN"`
Transport LDAPTransport `xml:"transport,omitempty" json:"transport"` Transport LDAPTransport `xml:"transport,omitempty" json:"transport"`
InsecureSkipVerify bool `xml:"insecureSkipVerify,omitempty" json:"insecureSkipVerify" default:"false"` InsecureSkipVerify bool `xml:"insecureSkipVerify,omitempty" json:"insecureSkipVerify" default:"false"`
SearchBaseDN string `xml:"searchBaseDN,omitempty" json:"searchBaseDN"`
SearchFilter string `xml:"searchFilter,omitempty" json:"searchFilter"`
} }
func (c LDAPConfiguration) Copy() LDAPConfiguration { func (c LDAPConfiguration) Copy() LDAPConfiguration {