From a906fa178a675b5961f5276adf1265fcbbb742ed Mon Sep 17 00:00:00 2001 From: Alex Bligh Date: Sun, 11 Oct 2015 17:14:46 +0100 Subject: [PATCH 1/3] Support multiple space-separated LDAP hosts Signed-off-by: Alex Bligh --- conf/ldap.toml | 2 +- pkg/login/ldap.go | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/conf/ldap.toml b/conf/ldap.toml index 9455c8df3a1..0688b84657a 100644 --- a/conf/ldap.toml +++ b/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 diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index af9982e77df..1bfbd817957 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -24,18 +24,23 @@ 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, + 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, + } + a.conn, err = ldap.DialTLS("tcp", address, tlsCfg) + } else { + a.conn, err = ldap.Dial("tcp", address) } - a.conn, err = ldap.DialTLS("tcp", address, tlsCfg) - } else { - a.conn, err = ldap.Dial("tcp", address) - } + if err == nil { + return nil + } + } return err } From 458e6da7001eee58ba677784bd73a04a01839f6a Mon Sep 17 00:00:00 2001 From: Alex Bligh Date: Sun, 11 Oct 2015 17:38:33 +0100 Subject: [PATCH 2/3] Allow user specified CA certs Signed-off-by: Alex Bligh --- conf/ldap.toml | 2 ++ pkg/login/ldap.go | 16 ++++++++++++++++ pkg/login/settings.go | 1 + 3 files changed, 19 insertions(+) diff --git a/conf/ldap.toml b/conf/ldap.toml index 0688b84657a..af131ad23b9 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -10,6 +10,8 @@ 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" diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 1bfbd817957..b98e54446f2 100644 --- a/pkg/login/ldap.go +++ b/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" @@ -25,12 +27,26 @@ func NewLdapAuthenticator(server *LdapServerConf) *ldapAuther { func (a *ldapAuther) Dial() error { var err error + 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) + } + } + } + } 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 { diff --git a/pkg/login/settings.go b/pkg/login/settings.go index 93ff04054c1..64fcf4da56d 100644 --- a/pkg/login/settings.go +++ b/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"` From e8256f0ad75f7dab9cd775bf59a91270e3a0b3b2 Mon Sep 17 00:00:00 2001 From: Alex Bligh Date: Tue, 13 Oct 2015 19:36:21 +0100 Subject: [PATCH 3/3] Add support for POSIX LDAP schema In the POSIX LDAP schema, there is no 'memberOf' attribute returned in relation to which groups a person is a member of. Rather, it is necessary to query the group objects which have the people as members. This commit adds an additional filter, which if specified explicitly searches for groups, rather than relying on the 'memberOf' attribute. This enables Grafana to work with LDAP POSIX schema (e.g. OpenLDAP etc.) Signed-off-by: Alex Bligh --- conf/ldap.toml | 21 +++++++++++++++++++++ pkg/login/ldap.go | 43 ++++++++++++++++++++++++++++++++++++++++--- pkg/login/settings.go | 3 +++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/conf/ldap.toml b/conf/ldap.toml index af131ad23b9..bfe28d4e835 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -18,11 +18,32 @@ 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" diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index b98e54446f2..6337f932256 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -311,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] @@ -332,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 { diff --git a/pkg/login/settings.go b/pkg/login/settings.go index 64fcf4da56d..b181dac3281 100644 --- a/pkg/login/settings.go +++ b/pkg/login/settings.go @@ -27,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"` }