Files
grafana/pkg/login/ldap.go
bergquist c22495b6f8 Merge branch 'master' into external-auth
* master:
  changelog: adds note about closing #11613 and #11602
  fix: sign in link should have target self to trigger full page reload, fixes #11626
  codespell: exclude by words instead of files
  Use sort.Strings() (gosimple)
  Remove unused return value assignment (gosimple)
  Remove unnecessary fmt.Sprintf() calls (gosimple)
  Merge variable declaration with assignment (gosimple)
  Use fmt.Errorf() (gosimple)
  Simplify make() (gosimple)
  Use raw strings to avoid double escapes (gosimple)
  Simplify if expression (gosimple)
  Simplify comparison to bool constant (gosimple)
  Simplify error returns (gosimple)
  Remove redundant break statements (gosimple)
2018-04-18 09:34:45 +02:00

371 lines
9.1 KiB
Go

package login
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/go-ldap/ldap"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
type ILdapConn interface {
Bind(username, password string) error
Search(*ldap.SearchRequest) (*ldap.SearchResult, error)
StartTLS(*tls.Config) error
Close()
}
type ILdapAuther interface {
Login(query *m.LoginUserQuery) error
SyncUser(query *m.LoginUserQuery) error
GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
}
type ldapAuther struct {
server *LdapServerConf
conn ILdapConn
requireSecondBind bool
log log.Logger
}
var NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther {
return &ldapAuther{server: server, log: log.New("ldap")}
}
var ldapDial = func(network, addr string) (ILdapConn, error) {
return ldap.Dial(network, addr)
}
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 certificate " + 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,
}
if a.server.StartTLS {
a.conn, err = ldap.Dial("tcp", address)
if err == nil {
if err = a.conn.StartTLS(tlsCfg); err == nil {
return nil
}
}
} else {
a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
}
} else {
a.conn, err = ldapDial("tcp", address)
}
if err == nil {
return nil
}
}
return err
}
func (a *ldapAuther) Login(query *m.LoginUserQuery) error {
// connect to ldap server
if err := a.Dial(); err != nil {
return err
}
defer a.conn.Close()
// perform initial authentication
if err := a.initialBind(query.Username, query.Password); err != nil {
return err
}
// find user entry & attributes
ldapUser, err := a.searchForUser(query.Username)
if err != nil {
return err
}
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
// check if a second user bind is needed
if a.requireSecondBind {
err = a.secondBind(ldapUser, query.Password)
if err != nil {
return err
}
}
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
if err != nil {
return err
}
query.User = grafanaUser
return nil
}
func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error {
// connect to ldap server
err := a.Dial()
if err != nil {
return err
}
defer a.conn.Close()
err = a.serverBind()
if err != nil {
return err
}
// find user entry & attributes
ldapUser, err := a.searchForUser(query.Username)
if err != nil {
a.log.Error("Failed searching for user in ldap", "error", err)
return err
}
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
if err != nil {
return err
}
query.User = grafanaUser
return nil
}
func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
extUser := &m.ExternalUserInfo{
AuthModule: "ldap",
AuthId: ldapUser.DN,
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
Login: ldapUser.Username,
Email: ldapUser.Email,
OrgRoles: map[int64]m.RoleType{},
}
for _, group := range a.server.LdapGroups {
// only use the first match for each org
if extUser.OrgRoles[group.OrgId] != "" {
continue
}
if ldapUser.isMemberOf(group.GroupDN) {
extUser.OrgRoles[group.OrgId] = group.OrgRole
}
}
// validate that the user has access
// if there are no ldap group mappings access is true
// otherwise a single group must match
if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 {
a.log.Info(
"Ldap Auth: user does not belong in any of the specified ldap groups",
"username", ldapUser.Username,
"groups", ldapUser.MemberOf)
return nil, ErrInvalidCredentials
}
// add/update user in grafana
userQuery := &m.UpsertUserCommand{
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: setting.LdapAllowSignup,
}
err := bus.Dispatch(userQuery)
if err != nil {
return nil, err
}
return userQuery.Result, nil
}
func (a *ldapAuther) serverBind() error {
// bind_dn and bind_password to bind
if err := a.conn.Bind(a.server.BindDN, a.server.BindPassword); err != nil {
a.log.Info("LDAP initial bind failed, %v", err)
if ldapErr, ok := err.(*ldap.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
}
return err
}
return nil
}
func (a *ldapAuther) secondBind(ldapUser *LdapUserInfo, userPassword string) error {
if err := a.conn.Bind(ldapUser.DN, userPassword); err != nil {
a.log.Info("Second bind failed", "error", err)
if ldapErr, ok := err.(*ldap.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
}
return err
}
return nil
}
func (a *ldapAuther) initialBind(username, userPassword string) error {
if a.server.BindPassword != "" || a.server.BindDN == "" {
userPassword = a.server.BindPassword
a.requireSecondBind = true
}
bindPath := a.server.BindDN
if strings.Contains(bindPath, "%s") {
bindPath = fmt.Sprintf(a.server.BindDN, username)
}
if err := a.conn.Bind(bindPath, userPassword); err != nil {
a.log.Info("Initial bind failed", "error", err)
if ldapErr, ok := err.(*ldap.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
}
return err
}
return nil
}
func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
var searchResult *ldap.SearchResult
var err error
for _, searchBase := range a.server.SearchBaseDNs {
searchReq := ldap.SearchRequest{
BaseDN: searchBase,
Scope: ldap.ScopeWholeSubtree,
DerefAliases: ldap.NeverDerefAliases,
Attributes: []string{
a.server.Attr.Username,
a.server.Attr.Surname,
a.server.Attr.Email,
a.server.Attr.Name,
a.server.Attr.MemberOf,
},
Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1),
}
searchResult, err = a.conn.Search(&searchReq)
if err != nil {
return nil, err
}
if len(searchResult.Entries) > 0 {
break
}
}
if len(searchResult.Entries) == 0 {
return nil, ErrInvalidCredentials
}
if len(searchResult.Entries) > 1 {
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_replace := getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
if a.server.GroupSearchFilterUserAttribute == "" {
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
}
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
a.log.Info("Searching for user's groups", "filter", filter)
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: memberOf,
}, nil
}
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]
}
}
}
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 {
return attr.Values
}
}
return []string{}
}