package ldap import ( "crypto/tls" "crypto/x509" "errors" "fmt" "io/ioutil" "math" "strings" "github.com/davecgh/go-spew/spew" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "gopkg.in/ldap.v3" ) // IConnection is interface for LDAP connection manipulation type IConnection interface { Bind(username, password string) error UnauthenticatedBind(username string) error Add(*ldap.AddRequest) error Del(*ldap.DelRequest) error Search(*ldap.SearchRequest) (*ldap.SearchResult, error) StartTLS(*tls.Config) error Close() } // IServer is interface for LDAP authorization type IServer interface { Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error) Users([]string) ([]*models.ExternalUserInfo, error) Bind() error UserBind(string, string) error Dial() error Close() } // Server is basic struct of LDAP authorization type Server struct { Config *ServerConfig Connection IConnection log log.Logger } // Bind authenticates the connection with the LDAP server // - with the username and password setup in the config // - or, anonymously // // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. func (server *Server) Bind() error { if server.shouldAdminBind() { if err := server.AdminBind(); err != nil { return err } } else { err := server.Connection.UnauthenticatedBind(server.Config.BindDN) if err != nil { return err } } return nil } // UsersMaxRequest is a max amount of users we can request via Users(). // Since many LDAP servers has limitations // on how much items can we return in one request const UsersMaxRequest = 500 var ( // ErrInvalidCredentials is returned if username and password do not match ErrInvalidCredentials = errors.New("Invalid Username or Password") // ErrCouldNotFindUser is returned when username hasn't been found (not username+password) ErrCouldNotFindUser = errors.New("Can't find user in LDAP") ) // New creates the new LDAP connection func New(config *ServerConfig) IServer { return &Server{ Config: config, log: log.New("ldap"), } } // Dial dials in the LDAP // TODO: decrease cyclomatic complexity func (server *Server) Dial() error { var err error var certPool *x509.CertPool if server.Config.RootCACert != "" { certPool = x509.NewCertPool() for _, caCertFile := range strings.Split(server.Config.RootCACert, " ") { pem, err := ioutil.ReadFile(caCertFile) if err != nil { return err } if !certPool.AppendCertsFromPEM(pem) { return errors.New("Failed to append CA certificate " + caCertFile) } } } var clientCert tls.Certificate if server.Config.ClientCert != "" && server.Config.ClientKey != "" { clientCert, err = tls.LoadX509KeyPair(server.Config.ClientCert, server.Config.ClientKey) if err != nil { return err } } for _, host := range strings.Split(server.Config.Host, " ") { address := fmt.Sprintf("%s:%d", host, server.Config.Port) if server.Config.UseSSL { tlsCfg := &tls.Config{ InsecureSkipVerify: server.Config.SkipVerifySSL, ServerName: host, RootCAs: certPool, } if len(clientCert.Certificate) > 0 { tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) } if server.Config.StartTLS { server.Connection, err = ldap.Dial("tcp", address) if err == nil { if err = server.Connection.StartTLS(tlsCfg); err == nil { return nil } } } else { server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg) } } else { server.Connection, err = ldap.Dial("tcp", address) } if err == nil { return nil } } return err } // Close closes the LDAP connection // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. func (server *Server) Close() { server.Connection.Close() } // Login the user. // There are several cases - // 1. "admin" user // Bind the "admin" user (defined in Grafana config file) which has the search privileges // in LDAP server, then we search the targeted user through that bind, then the second // perform the bind via passed login/password. // 2. Single bind // // If all the users meant to be used with Grafana have the ability to search in LDAP server // then we bind with LDAP server with targeted login/password // and then search for the said user in order to retrive all the information about them // 3. Unauthenticated bind // For some LDAP configurations it is allowed to search the // user without login/password binding with LDAP server, in such case // we will perform "unauthenticated bind", then search for the // targeted user and then perform the bind with passed login/password. // // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. func (server *Server) Login(query *models.LoginUserQuery) ( *models.ExternalUserInfo, error, ) { var err error var authAndBind bool // Check if we can use a search user if server.shouldAdminBind() { if err := server.AdminBind(); err != nil { return nil, err } } else if server.shouldSingleBind() { authAndBind = true err = server.UserBind( server.singleBindDN(query.Username), query.Password, ) if err != nil { return nil, err } } else { err := server.Connection.UnauthenticatedBind(server.Config.BindDN) if err != nil { return nil, err } } // Find user entry & attributes users, err := server.Users([]string{query.Username}) if err != nil { return nil, err } // If we couldn't find the user - // we should show incorrect credentials err if len(users) == 0 { return nil, ErrCouldNotFindUser } user := users[0] if err := server.validateGrafanaUser(user); err != nil { return nil, err } if !authAndBind { // Authenticate user err = server.UserBind(user.AuthId, query.Password) if err != nil { return nil, err } } return user, nil } // shouldAdminBind checks if we should use // admin username & password for LDAP bind func (server *Server) shouldAdminBind() bool { return server.Config.BindPassword != "" } // singleBindDN combines the bind with the username // in order to get the proper path func (server *Server) singleBindDN(username string) string { return fmt.Sprintf(server.Config.BindDN, username) } // shouldSingleBind checks if we can use "single bind" approach func (server *Server) shouldSingleBind() bool { return strings.Contains(server.Config.BindDN, "%s") } // Users gets LDAP users by logins // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. func (server *Server) Users(logins []string) ( []*models.ExternalUserInfo, error, ) { var users []*ldap.Entry err := getUsersIteration(logins, func(previous, current int) error { entries, err := server.users(logins[previous:current]) if err != nil { return err } users = append(users, entries...) return nil }) if err != nil { return nil, err } if len(users) == 0 { return []*models.ExternalUserInfo{}, nil } serializedUsers, err := server.serializeUsers(users) if err != nil { return nil, err } server.log.Debug( "LDAP users found", "users", spew.Sdump(serializedUsers), ) return serializedUsers, nil } // getUsersIteration is a helper function for Users() method. // It divides the users by equal parts for the anticipated requests func getUsersIteration(logins []string, fn func(int, int) error) error { lenLogins := len(logins) iterations := int( math.Ceil( float64(lenLogins) / float64(UsersMaxRequest), ), ) for i := 1; i < iterations+1; i++ { previous := float64(UsersMaxRequest * (i - 1)) current := math.Min(float64(i*UsersMaxRequest), float64(lenLogins)) err := fn(int(previous), int(current)) if err != nil { return err } } return nil } // users is helper method for the Users() func (server *Server) users(logins []string) ( []*ldap.Entry, error, ) { var result *ldap.SearchResult var Config = server.Config var err error for _, base := range Config.SearchBaseDNs { result, err = server.Connection.Search( server.getSearchRequest(base, logins), ) if err != nil { return nil, err } if len(result.Entries) > 0 { break } } return result.Entries, nil } // validateGrafanaUser validates user access. // If there are no ldap group mappings access is true // otherwise a single group must match func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error { if len(server.Config.Groups) > 0 && len(user.OrgRoles) < 1 { server.log.Error( "User does not belong in any of the specified LDAP groups", "username", user.Login, "groups", user.Groups, ) return ErrInvalidCredentials } return nil } // getSearchRequest returns LDAP search request for users func (server *Server) getSearchRequest( base string, logins []string, ) *ldap.SearchRequest { attributes := []string{} inputs := server.Config.Attr attributes = appendIfNotEmpty( attributes, inputs.Username, inputs.Surname, inputs.Email, inputs.Name, inputs.MemberOf, // In case for the POSIX LDAP schema server server.Config.GroupSearchFilterUserAttribute, ) search := "" for _, login := range logins { query := strings.Replace( server.Config.SearchFilter, "%s", ldap.EscapeFilter(login), -1, ) search = search + query } filter := fmt.Sprintf("(|%s)", search) return &ldap.SearchRequest{ BaseDN: base, Scope: ldap.ScopeWholeSubtree, DerefAliases: ldap.NeverDerefAliases, Attributes: attributes, Filter: filter, } } // buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserInfo, error) { memberOf, err := server.getMemberOf(user) if err != nil { return nil, err } attrs := server.Config.Attr extUser := &models.ExternalUserInfo{ AuthModule: models.AuthModuleLDAP, AuthId: user.DN, Name: strings.TrimSpace( fmt.Sprintf( "%s %s", getAttribute(attrs.Name, user), getAttribute(attrs.Surname, user), ), ), Login: getAttribute(attrs.Username, user), Email: getAttribute(attrs.Email, user), Groups: memberOf, OrgRoles: map[int64]models.RoleType{}, } for _, group := range server.Config.Groups { // only use the first match for each org if extUser.OrgRoles[group.OrgId] != "" { continue } if isMemberOf(memberOf, group.GroupDN) { extUser.OrgRoles[group.OrgId] = group.OrgRole if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin { extUser.IsGrafanaAdmin = group.IsGrafanaAdmin } } } return extUser, nil } // UserBind binds the user with the LDAP server // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. func (server *Server) UserBind(username, password string) error { err := server.userBind(username, password) if err != nil { server.log.Error( fmt.Sprintf("Cannot bind user %s with LDAP", username), "error", err, ) return err } return nil } // AdminBind binds "admin" user with LDAP // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. func (server *Server) AdminBind() error { err := server.userBind(server.Config.BindDN, server.Config.BindPassword) if err != nil { server.log.Error( "Cannot authentificate admin user in LDAP", "error", err, ) return err } return nil } // userBind binds the user with the LDAP server func (server *Server) userBind(path, password string) error { err := server.Connection.Bind(path, password) if err != nil { if ldapErr, ok := err.(*ldap.Error); ok { if ldapErr.ResultCode == 49 { return ErrInvalidCredentials } } return err } return nil } // requestMemberOf use this function when POSIX LDAP // schema does not support memberOf, so it manually search the groups func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) { var memberOf []string var config = server.Config for _, groupSearchBase := range config.GroupSearchBaseDNs { var filterReplace string if config.GroupSearchFilterUserAttribute == "" { filterReplace = getAttribute(config.Attr.Username, entry) } else { filterReplace = getAttribute( config.GroupSearchFilterUserAttribute, entry, ) } filter := strings.Replace( config.GroupSearchFilter, "%s", ldap.EscapeFilter(filterReplace), -1, ) server.log.Info("Searching for user's groups", "filter", filter) // support old way of reading settings groupIDAttribute := config.Attr.MemberOf // but prefer dn attribute if default settings are used if groupIDAttribute == "" || groupIDAttribute == "memberOf" { groupIDAttribute = "dn" } groupSearchReq := ldap.SearchRequest{ BaseDN: groupSearchBase, Scope: ldap.ScopeWholeSubtree, DerefAliases: ldap.NeverDerefAliases, Attributes: []string{groupIDAttribute}, Filter: filter, } groupSearchResult, err := server.Connection.Search(&groupSearchReq) if err != nil { return nil, err } if len(groupSearchResult.Entries) > 0 { for _, group := range groupSearchResult.Entries { memberOf = append( memberOf, getAttribute(groupIDAttribute, group), ) } break } } return memberOf, nil } // serializeUsers serializes the users // from LDAP result to ExternalInfo struct func (server *Server) serializeUsers( entries []*ldap.Entry, ) ([]*models.ExternalUserInfo, error) { var serialized []*models.ExternalUserInfo for _, user := range entries { extUser, err := server.buildGrafanaUser(user) if err != nil { return nil, err } serialized = append(serialized, extUser) } return serialized, nil } // getMemberOf finds memberOf property or request it func (server *Server) getMemberOf(result *ldap.Entry) ( []string, error, ) { if server.Config.GroupSearchFilter == "" { memberOf := getArrayAttribute(server.Config.Attr.MemberOf, result) return memberOf, nil } memberOf, err := server.requestMemberOf(result) if err != nil { return nil, err } return memberOf, nil }