Browse Source

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.
Jakob Borg 5 years ago
parent
commit
48f9d323fa
2 changed files with 31 additions and 0 deletions
  1. 29 0
      lib/api/api_auth.go
  2. 2 0
      lib/config/ldapconfiguration.go

+ 29 - 0
lib/api/api_auth.go

@@ -166,6 +166,35 @@ func authLDAP(username string, password string, cfg config.LDAPConfiguration) bo
 		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
 }
 

+ 2 - 0
lib/config/ldapconfiguration.go

@@ -11,6 +11,8 @@ type LDAPConfiguration struct {
 	BindDN             string        `xml:"bindDN,omitempty" json:"bindDN"`
 	Transport          LDAPTransport `xml:"transport,omitempty" json:"transport"`
 	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 {