mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Move the ReloadLDAPCfg function to the debug file Appears to be a better suite place for this. * LDAP: Return the server information when we find a specific user We allow you to specify multiple LDAP servers as part of LDAP authentication integration. As part of searching for specific users, we need to understand from which server they come from. Returning the server configuration as part of the search will help us do two things: - Understand in which server we found the user - Have access the groups specified as part of the server configuration * LDAP: Adds the /api/admin/ldap/:username endpoint This endpoint returns a user found within the configured LDAP server(s). Moreso, it provides the mapping information for the user to help administrators understand how the users would be created within Grafana based on the current configuration. No changes are executed or saved to the database, this is all an in-memory representation of how the final result would look like.
557 lines
13 KiB
Go
557 lines
13 KiB
Go
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
}
|