grafana/pkg/services/ldap/ldap.go
Oleg Gaidarenko 35f227de11
Feature: LDAP refactoring (#16950)
* incapsulates multipleldap logic under one module

* abstracts users upsert and get logic

* changes some of the text error messages and import sort sequence

* heavily refactors the LDAP module – LDAP module now only deals with LDAP related behaviour

* integrates affected auth_proxy module and their tests

* refactoring of the auth_proxy logic
2019-05-17 14:57:26 +03:00

578 lines
13 KiB
Go

package ldap
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"strings"
"gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
// 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)
Add(string, map[string][]string) error
Remove(string) error
Users([]string) ([]*models.ExternalUserInfo, error)
ExtractGrafanaUser(*UserInfo) (*models.ExternalUserInfo, error)
Dial() error
Close()
}
// Server is basic struct of LDAP authorization
type Server struct {
config *ServerConfig
connection IConnection
requireSecondBind bool
log log.Logger
}
var (
// ErrInvalidCredentials is returned if username and password do not match
ErrInvalidCredentials = errors.New("Invalid Username or Password")
)
var dial = func(network, addr string) (IConnection, error) {
return ldap.Dial(network, addr)
}
// New creates the new LDAP auth
func New(config *ServerConfig) IServer {
return &Server{
config: config,
log: log.New("ldap"),
}
}
// Dial dials in the LDAP
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 = 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 = dial("tcp", address)
}
if err == nil {
return nil
}
}
return err
}
// Close closes the LDAP connection
func (server *Server) Close() {
server.connection.Close()
}
// Login intialBinds the user, search it and then serialize it
func (server *Server) Login(query *models.LoginUserQuery) (
*models.ExternalUserInfo, error,
) {
// Perform initial authentication
err := server.intialBind(query.Username, query.Password)
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, ErrInvalidCredentials
}
// Check if a second user bind is needed
user := users[0]
if server.requireSecondBind {
err = server.secondBind(user, query.Password)
if err != nil {
return nil, err
}
}
return user, nil
}
// Add adds stuff to LDAP
func (server *Server) Add(dn string, values map[string][]string) error {
err := server.intialBind(
server.config.BindDN,
server.config.BindPassword,
)
if err != nil {
return err
}
attributes := make([]ldap.Attribute, 0)
for key, value := range values {
attributes = append(attributes, ldap.Attribute{
Type: key,
Vals: value,
})
}
request := &ldap.AddRequest{
DN: dn,
Attributes: attributes,
}
err = server.connection.Add(request)
if err != nil {
return err
}
return nil
}
// Remove removes stuff from LDAP
func (server *Server) Remove(dn string) error {
err := server.intialBind(
server.config.BindDN,
server.config.BindPassword,
)
if err != nil {
return err
}
request := ldap.NewDelRequest(dn, nil)
err = server.connection.Del(request)
if err != nil {
return err
}
return nil
}
// Users gets LDAP users
func (server *Server) Users(logins []string) (
[]*models.ExternalUserInfo,
error,
) {
var result *ldap.SearchResult
var err error
var config = server.config
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
}
}
serializedUsers, err := server.serializeUsers(result)
if err != nil {
return nil, err
}
return serializedUsers, nil
}
// ExtractGrafanaUser extracts external user info from LDAP user
func (server *Server) ExtractGrafanaUser(user *UserInfo) (*models.ExternalUserInfo, error) {
result := server.buildGrafanaUser(user)
if err := server.validateGrafanaUser(result); err != nil {
return nil, err
}
return result, 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,
)
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 *UserInfo) *models.ExternalUserInfo {
extUser := &models.ExternalUserInfo{
AuthModule: "ldap",
AuthId: user.DN,
Name: strings.TrimSpace(
fmt.Sprintf("%s %s", user.FirstName, user.LastName),
),
Login: user.Username,
Email: user.Email,
Groups: user.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 user.isMemberOf(group.GroupDN) {
extUser.OrgRoles[group.OrgId] = group.OrgRole
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
}
}
}
return extUser
}
func (server *Server) serverBind() error {
bindFn := func() error {
return server.connection.Bind(
server.config.BindDN,
server.config.BindPassword,
)
}
if server.config.BindPassword == "" {
bindFn = func() error {
return server.connection.UnauthenticatedBind(server.config.BindDN)
}
}
// bind_dn and bind_password to bind
if err := bindFn(); err != nil {
server.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 (server *Server) secondBind(
user *models.ExternalUserInfo,
userPassword string,
) error {
err := server.connection.Bind(user.AuthId, userPassword)
if err != nil {
server.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 (server *Server) intialBind(username, userPassword string) error {
if server.config.BindPassword != "" || server.config.BindDN == "" {
userPassword = server.config.BindPassword
server.requireSecondBind = true
}
bindPath := server.config.BindDN
if strings.Contains(bindPath, "%s") {
bindPath = fmt.Sprintf(server.config.BindDN, username)
}
bindFn := func() error {
return server.connection.Bind(bindPath, userPassword)
}
if userPassword == "" {
bindFn = func() error {
return server.connection.UnauthenticatedBind(bindPath)
}
}
if err := bindFn(); err != nil {
server.log.Info("Initial bind failed", "error", err)
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(searchResult *ldap.SearchResult) ([]string, error) {
var memberOf []string
for _, groupSearchBase := range server.config.GroupSearchBaseDNs {
var filterReplace string
if server.config.GroupSearchFilterUserAttribute == "" {
filterReplace = getLdapAttr(server.config.Attr.Username, searchResult)
} else {
filterReplace = getLdapAttr(server.config.GroupSearchFilterUserAttribute, searchResult)
}
filter := strings.Replace(
server.config.GroupSearchFilter, "%s",
ldap.EscapeFilter(filterReplace),
-1,
)
server.log.Info("Searching for user's groups", "filter", filter)
// support old way of reading settings
groupIDAttribute := server.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 i := range groupSearchResult.Entries {
memberOf = append(memberOf, getLdapAttrN(groupIDAttribute, groupSearchResult, i))
}
break
}
}
return memberOf, nil
}
// serializeUsers serializes the users
// from LDAP result to ExternalInfo struct
func (server *Server) serializeUsers(
users *ldap.SearchResult,
) ([]*models.ExternalUserInfo, error) {
var serialized []*models.ExternalUserInfo
for index := range users.Entries {
memberOf, err := server.getMemberOf(users)
if err != nil {
return nil, err
}
userInfo := &UserInfo{
DN: getLdapAttrN(
"dn",
users,
index,
),
LastName: getLdapAttrN(
server.config.Attr.Surname,
users,
index,
),
FirstName: getLdapAttrN(
server.config.Attr.Name,
users,
index,
),
Username: getLdapAttrN(
server.config.Attr.Username,
users,
index,
),
Email: getLdapAttrN(
server.config.Attr.Email,
users,
index,
),
MemberOf: memberOf,
}
serialized = append(
serialized,
server.buildGrafanaUser(userInfo),
)
}
return serialized, nil
}
// getMemberOf finds memberOf property or request it
func (server *Server) getMemberOf(search *ldap.SearchResult) (
[]string, error,
) {
if server.config.GroupSearchFilter == "" {
memberOf := getLdapAttrArray(server.config.Attr.MemberOf, search)
return memberOf, nil
}
memberOf, err := server.requestMemberOf(search)
if err != nil {
return nil, err
}
return memberOf, nil
}
func appendIfNotEmpty(slice []string, values ...string) []string {
for _, v := range values {
if v != "" {
slice = append(slice, v)
}
}
return slice
}
func getLdapAttr(name string, result *ldap.SearchResult) string {
return getLdapAttrN(name, result, 0)
}
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
if strings.ToLower(name) == "dn" {
return result.Entries[n].DN
}
for _, attr := range result.Entries[n].Attributes {
if attr.Name == name {
if len(attr.Values) > 0 {
return attr.Values[0]
}
}
}
return ""
}
func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
return getLdapAttrArrayN(name, result, 0)
}
func getLdapAttrArrayN(name string, result *ldap.SearchResult, n int) []string {
for _, attr := range result.Entries[n].Attributes {
if attr.Name == name {
return attr.Values
}
}
return []string{}
}