Files
grafana/pkg/services/ldap/ldap.go
Oleg Gaidarenko 62b85a886e LDAP Refactoring to support syncronizing more than one user at a time. (#16705)
* Feature: add cron setting for the ldap settings

* Move ldap configuration read to special function

* Introduce cron setting (no docs for it yet, pending approval)

* Chore: duplicate ldap module as a service

* Feature: implement active sync

This is very early preliminary implementation of active sync.
There is only one thing that's going right for this code - it works.

Aside from that, there is no tests, error handling, docs, transactions,
it's very much duplicative and etc.

But this is the overall direction with architecture I'm going for

* Chore: introduce login service

* Chore: gradually switch to ldap service

* Chore: use new approach for auth_proxy

* Chore: use new approach along with refactoring

* Chore: use new ldap interface for auth_proxy

* Chore: improve auth_proxy and subsequently ldap

* Chore: more of the refactoring bits

* Chore: address comments from code review

* Chore: more refactoring stuff

* Chore: make linter happy

* Chore: add cron dep for grafana enterprise

* Chore: initialize config package var

* Chore: disable gosec for now

* Chore: update dependencies

* Chore: remove unused module

* Chore: address review comments

* Chore: make linter happy
2019-04-26 15:47:16 +03:00

560 lines
13 KiB
Go

package ldap
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"strings"
"github.com/davecgh/go-spew/spew"
LDAP "gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
models "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
// IConnection is interface for LDAP connection manipulation
type IConnection interface {
Bind(username, password string) error
UnauthenticatedBind(username string) error
Search(*LDAP.SearchRequest) (*LDAP.SearchResult, error)
StartTLS(*tls.Config) error
Close()
}
// IAuth is interface for LDAP authorization
type IAuth interface {
Login(query *models.LoginUserQuery) error
SyncUser(query *models.LoginUserQuery) error
GetGrafanaUserFor(
ctx *models.ReqContext,
user *UserInfo,
) (*models.User, error)
Users() ([]*UserInfo, error)
}
// Auth is basic struct of LDAP authorization
type Auth struct {
server *ServerConfig
conn 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(server *ServerConfig) IAuth {
return &Auth{
server: server,
log: log.New("ldap"),
}
}
// Dial dials in the LDAP
func (auth *Auth) Dial() error {
if hookDial != nil {
return hookDial(auth)
}
var err error
var certPool *x509.CertPool
if auth.server.RootCACert != "" {
certPool = x509.NewCertPool()
for _, caCertFile := range strings.Split(auth.server.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 auth.server.ClientCert != "" && auth.server.ClientKey != "" {
clientCert, err = tls.LoadX509KeyPair(auth.server.ClientCert, auth.server.ClientKey)
if err != nil {
return err
}
}
for _, host := range strings.Split(auth.server.Host, " ") {
address := fmt.Sprintf("%s:%d", host, auth.server.Port)
if auth.server.UseSSL {
tlsCfg := &tls.Config{
InsecureSkipVerify: auth.server.SkipVerifySSL,
ServerName: host,
RootCAs: certPool,
}
if len(clientCert.Certificate) > 0 {
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
}
if auth.server.StartTLS {
auth.conn, err = dial("tcp", address)
if err == nil {
if err = auth.conn.StartTLS(tlsCfg); err == nil {
return nil
}
}
} else {
auth.conn, err = LDAP.DialTLS("tcp", address, tlsCfg)
}
} else {
auth.conn, err = dial("tcp", address)
}
if err == nil {
return nil
}
}
return err
}
// Login logs in the user
func (auth *Auth) Login(query *models.LoginUserQuery) error {
// connect to ldap server
if err := auth.Dial(); err != nil {
return err
}
defer auth.conn.Close()
// perform initial authentication
if err := auth.initialBind(query.Username, query.Password); err != nil {
return err
}
// find user entry & attributes
user, err := auth.searchForUser(query.Username)
if err != nil {
return err
}
auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
// check if a second user bind is needed
if auth.requireSecondBind {
err = auth.secondBind(user, query.Password)
if err != nil {
return err
}
}
grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
if err != nil {
return err
}
query.User = grafanaUser
return nil
}
// SyncUser syncs user with Grafana
func (auth *Auth) SyncUser(query *models.LoginUserQuery) error {
// connect to ldap server
err := auth.Dial()
if err != nil {
return err
}
defer auth.conn.Close()
err = auth.serverBind()
if err != nil {
return err
}
// find user entry & attributes
user, err := auth.searchForUser(query.Username)
if err != nil {
auth.log.Error("Failed searching for user in ldap", "error", err)
return err
}
auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
if err != nil {
return err
}
query.User = grafanaUser
return nil
}
func (auth *Auth) GetGrafanaUserFor(
ctx *models.ReqContext,
user *UserInfo,
) (*models.User, error) {
extUser := &models.ExternalUserInfo{
AuthModule: "ldap",
AuthId: user.DN,
Name: 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 auth.server.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
}
}
}
// validate that the user has access
// if there are no ldap group mappings access is true
// otherwise a single group must match
if len(auth.server.Groups) > 0 && len(extUser.OrgRoles) < 1 {
auth.log.Info(
"Ldap Auth: user does not belong in any of the specified ldap groups",
"username", user.Username,
"groups", user.MemberOf,
)
return nil, ErrInvalidCredentials
}
// add/update user in grafana
upsertUserCmd := &models.UpsertUserCommand{
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: setting.LdapAllowSignup,
}
err := bus.Dispatch(upsertUserCmd)
if err != nil {
return nil, err
}
return upsertUserCmd.Result, nil
}
func (auth *Auth) serverBind() error {
bindFn := func() error {
return auth.conn.Bind(auth.server.BindDN, auth.server.BindPassword)
}
if auth.server.BindPassword == "" {
bindFn = func() error {
return auth.conn.UnauthenticatedBind(auth.server.BindDN)
}
}
// bind_dn and bind_password to bind
if err := bindFn(); err != nil {
auth.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 (auth *Auth) secondBind(user *UserInfo, userPassword string) error {
if err := auth.conn.Bind(user.DN, userPassword); err != nil {
auth.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 (auth *Auth) initialBind(username, userPassword string) error {
if auth.server.BindPassword != "" || auth.server.BindDN == "" {
userPassword = auth.server.BindPassword
auth.requireSecondBind = true
}
bindPath := auth.server.BindDN
if strings.Contains(bindPath, "%s") {
bindPath = fmt.Sprintf(auth.server.BindDN, username)
}
bindFn := func() error {
return auth.conn.Bind(bindPath, userPassword)
}
if userPassword == "" {
bindFn = func() error {
return auth.conn.UnauthenticatedBind(bindPath)
}
}
if err := bindFn(); err != nil {
auth.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 (auth *Auth) searchForUser(username string) (*UserInfo, error) {
var searchResult *LDAP.SearchResult
var err error
for _, searchBase := range auth.server.SearchBaseDNs {
attributes := make([]string, 0)
inputs := auth.server.Attr
attributes = appendIfNotEmpty(attributes,
inputs.Username,
inputs.Surname,
inputs.Email,
inputs.Name,
inputs.MemberOf)
searchReq := LDAP.SearchRequest{
BaseDN: searchBase,
Scope: LDAP.ScopeWholeSubtree,
DerefAliases: LDAP.NeverDerefAliases,
Attributes: attributes,
Filter: strings.Replace(
auth.server.SearchFilter,
"%s", LDAP.EscapeFilter(username),
-1,
),
}
auth.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
searchResult, err = auth.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 auth.server.GroupSearchFilter == "" {
memberOf = getLdapAttrArray(auth.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 auth.server.GroupSearchBaseDNs {
var filter_replace string
if auth.server.GroupSearchFilterUserAttribute == "" {
filter_replace = getLdapAttr(auth.server.Attr.Username, searchResult)
} else {
filter_replace = getLdapAttr(auth.server.GroupSearchFilterUserAttribute, searchResult)
}
filter := strings.Replace(
auth.server.GroupSearchFilter, "%s",
LDAP.EscapeFilter(filter_replace),
-1,
)
auth.log.Info("Searching for user's groups", "filter", filter)
// support old way of reading settings
groupIdAttribute := auth.server.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 = auth.conn.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 &UserInfo{
DN: searchResult.Entries[0].DN,
LastName: getLdapAttr(auth.server.Attr.Surname, searchResult),
FirstName: getLdapAttr(auth.server.Attr.Name, searchResult),
Username: getLdapAttr(auth.server.Attr.Username, searchResult),
Email: getLdapAttr(auth.server.Attr.Email, searchResult),
MemberOf: memberOf,
}, nil
}
func (ldap *Auth) Users() ([]*UserInfo, error) {
var result *LDAP.SearchResult
var err error
server := ldap.server
if err := ldap.Dial(); err != nil {
return nil, err
}
defer ldap.conn.Close()
for _, base := range server.SearchBaseDNs {
attributes := make([]string, 0)
inputs := server.Attr
attributes = appendIfNotEmpty(
attributes,
inputs.Username,
inputs.Surname,
inputs.Email,
inputs.Name,
inputs.MemberOf,
)
req := LDAP.SearchRequest{
BaseDN: base,
Scope: LDAP.ScopeWholeSubtree,
DerefAliases: LDAP.NeverDerefAliases,
Attributes: attributes,
// Doing a star here to get all the users in one go
Filter: strings.Replace(server.SearchFilter, "%s", "*", -1),
}
result, err = ldap.conn.Search(&req)
if err != nil {
return nil, err
}
if len(result.Entries) > 0 {
break
}
}
return ldap.serializeUsers(result), nil
}
func (ldap *Auth) serializeUsers(users *LDAP.SearchResult) []*UserInfo {
var serialized []*UserInfo
for index := range users.Entries {
serialize := &UserInfo{
DN: getLdapAttrN(
"dn",
users,
index,
),
LastName: getLdapAttrN(
ldap.server.Attr.Surname,
users,
index,
),
FirstName: getLdapAttrN(
ldap.server.Attr.Name,
users,
index,
),
Username: getLdapAttrN(
ldap.server.Attr.Username,
users,
index,
),
Email: getLdapAttrN(
ldap.server.Attr.Email,
users,
index,
),
MemberOf: getLdapAttrArrayN(
ldap.server.Attr.MemberOf,
users,
index,
),
}
serialized = append(serialized, serialize)
}
return serialized
}
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{}
}