Browse Source

Merge branch 'ldap-improvements' of https://github.com/abligh/grafana into abligh-ldap-improvements

Torkel Ödegaard 10 years ago
parent
commit
38bd0d1aec
3 changed files with 97 additions and 12 deletions
  1. 24 1
      conf/ldap.toml
  2. 69 11
      pkg/login/ldap.go
  3. 4 0
      pkg/login/settings.go

+ 24 - 1
conf/ldap.toml

@@ -2,7 +2,7 @@
 verbose_logging = false
 
 [[servers]]
-# Ldap server host
+# Ldap server host (specify multiple hosts space separated)
 host = "127.0.0.1"
 # Default port is 389 or 636 if use_ssl = true
 port = 389
@@ -10,17 +10,40 @@ port = 389
 use_ssl = false
 # set to true if you want to skip ssl cert validation
 ssl_skip_verify = false
+# set to the path to your root CA certificate or leave unset to use system defaults
+# root_ca_cert = /path/to/certificate.crt
 
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
 # Search user bind password
 bind_password = 'grafana'
 
+# Schema's supporting memberOf
+
 # Search filter, for example "(cn=%s)" or "(sAMAccountName=%s)"
 search_filter = "(cn=%s)"
 # An array of base dns to search through
 search_base_dns = ["dc=grafana,dc=org"]
 
+# Uncomment this section (and comment out the previous 2 entries) to use POSIX schema.
+# In POSIX LDAP schemas, querying the people 'ou' gives you entries that do not have a
+# memberOf attribute, so a secondary query must be made for groups. This is done by
+# enabling group_search_filter below. You must also set
+#    member_of = "cn"
+# in [servers.attributes] below.
+#
+# Search filter, used to retrieve the user
+# search_filter = "(uid=%s)"
+#
+# An array of the base DNs to search through for users. Typically uses ou=people.
+# search_base_dns = ["ou=people,dc=grafana,dc=org"]
+#
+# Group search filter, to retrieve the groups of which the user is a member
+# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
+#
+# An array of the base DNs to search through for groups. Typically uses ou=groups
+# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+
 # Specify names of the ldap attributes your ldap uses
 [servers.attributes]
 name = "givenName"

+ 69 - 11
pkg/login/ldap.go

@@ -2,8 +2,10 @@ package login
 
 import (
 	"crypto/tls"
+	"crypto/x509"
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"strings"
 
 	"github.com/davecgh/go-spew/spew"
@@ -24,18 +26,37 @@ func NewLdapAuthenticator(server *LdapServerConf) *ldapAuther {
 }
 
 func (a *ldapAuther) Dial() error {
-	address := fmt.Sprintf("%s:%d", a.server.Host, a.server.Port)
 	var err error
-	if a.server.UseSSL {
-		tlsCfg := &tls.Config{
-			InsecureSkipVerify: a.server.SkipVerifySSL,
-			ServerName:         a.server.Host,
+	var certPool *x509.CertPool
+	if a.server.RootCACert != "" {
+		certPool := x509.NewCertPool()
+		for _, caCertFile := range strings.Split(a.server.RootCACert, " ") {
+			if pem, err := ioutil.ReadFile(caCertFile); err != nil {
+				return err
+			} else {
+				if !certPool.AppendCertsFromPEM(pem) {
+					return errors.New("Failed to append CA certficate " + caCertFile)
+				}
+			}
 		}
-		a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
-	} else {
-		a.conn, err = ldap.Dial("tcp", address)
 	}
+	for _, host := range strings.Split(a.server.Host, " ") {
+		address := fmt.Sprintf("%s:%d", host, a.server.Port)
+		if a.server.UseSSL {
+			tlsCfg := &tls.Config{
+				InsecureSkipVerify: a.server.SkipVerifySSL,
+				ServerName:         host,
+				RootCAs:            certPool,
+			}
+			a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
+		} else {
+			a.conn, err = ldap.Dial("tcp", address)
+		}
 
+		if err == nil {
+			return nil
+		}
+	}
 	return err
 }
 
@@ -290,18 +311,51 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
 		return nil, errors.New("Ldap search matched more than one entry, please review your filter setting")
 	}
 
+	var memberOf []string
+	if a.server.GroupSearchFilter == "" {
+		memberOf = getLdapAttrArray(a.server.Attr.MemberOf, searchResult)
+	} else {
+		// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
+		var groupSearchResult *ldap.SearchResult
+		for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
+			filter := strings.Replace(a.server.GroupSearchFilter, "%s", username, -1)
+			groupSearchReq := ldap.SearchRequest{
+				BaseDN:       groupSearchBase,
+				Scope:        ldap.ScopeWholeSubtree,
+				DerefAliases: ldap.NeverDerefAliases,
+				Attributes: []string{
+					// Here MemberOf would be the thing that identifies the group, which is normally 'cn'
+					a.server.Attr.MemberOf,
+				},
+				Filter: filter,
+			}
+
+			groupSearchResult, err = a.conn.Search(&groupSearchReq)
+			if err != nil {
+				return nil, err
+			}
+
+			if len(groupSearchResult.Entries) > 0 {
+				for i := range groupSearchResult.Entries {
+					memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
+				}
+				break
+			}
+		}
+	}
+
 	return &ldapUserInfo{
 		DN:        searchResult.Entries[0].DN,
 		LastName:  getLdapAttr(a.server.Attr.Surname, searchResult),
 		FirstName: getLdapAttr(a.server.Attr.Name, searchResult),
 		Username:  getLdapAttr(a.server.Attr.Username, searchResult),
 		Email:     getLdapAttr(a.server.Attr.Email, searchResult),
-		MemberOf:  getLdapAttrArray(a.server.Attr.MemberOf, searchResult),
+		MemberOf:  memberOf,
 	}, nil
 }
 
-func getLdapAttr(name string, result *ldap.SearchResult) string {
-	for _, attr := range result.Entries[0].Attributes {
+func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
+	for _, attr := range result.Entries[n].Attributes {
 		if attr.Name == name {
 			if len(attr.Values) > 0 {
 				return attr.Values[0]
@@ -311,6 +365,10 @@ func getLdapAttr(name string, result *ldap.SearchResult) string {
 	return ""
 }
 
+func getLdapAttr(name string, result *ldap.SearchResult) string {
+	return getLdapAttrN(name, result, 0)
+}
+
 func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
 	for _, attr := range result.Entries[0].Attributes {
 		if attr.Name == name {

+ 4 - 0
pkg/login/settings.go

@@ -19,6 +19,7 @@ type LdapServerConf struct {
 	Port          int              `toml:"port"`
 	UseSSL        bool             `toml:"use_ssl"`
 	SkipVerifySSL bool             `toml:"ssl_skip_verify"`
+	RootCACert    string           `toml:"root_ca_cert"`
 	BindDN        string           `toml:"bind_dn"`
 	BindPassword  string           `toml:"bind_password"`
 	Attr          LdapAttributeMap `toml:"attributes"`
@@ -26,6 +27,9 @@ type LdapServerConf struct {
 	SearchFilter  string   `toml:"search_filter"`
 	SearchBaseDNs []string `toml:"search_base_dns"`
 
+	GroupSearchFilter  string   `toml:"group_search_filter"`
+	GroupSearchBaseDNs []string `toml:"group_search_base_dns"`
+
 	LdapGroups []*LdapGroupToOrgRole `toml:"group_mappings"`
 }