mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
5
pkg/services/ldap/hooks.go
Normal file
5
pkg/services/ldap/hooks.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package ldap
|
||||
|
||||
var (
|
||||
hookDial func(*Auth) error
|
||||
)
|
||||
559
pkg/services/ldap/ldap.go
Normal file
559
pkg/services/ldap/ldap.go
Normal file
@@ -0,0 +1,559 @@
|
||||
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{}
|
||||
}
|
||||
86
pkg/services/ldap/ldap_login_test.go
Normal file
86
pkg/services/ldap/ldap_login_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
)
|
||||
|
||||
func TestLdapLogin(t *testing.T) {
|
||||
Convey("Login using ldap", t, func() {
|
||||
AuthScenario("When login with invalid credentials", func(scenario *scenarioContext) {
|
||||
conn := &mockLdapConn{}
|
||||
entry := ldap.Entry{}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
conn.setSearchResult(&result)
|
||||
|
||||
conn.bindProvider = func(username, password string) error {
|
||||
return &ldap.Error{
|
||||
ResultCode: 49,
|
||||
}
|
||||
}
|
||||
auth := &Auth{
|
||||
server: &ServerConfig{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
conn: conn,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
err := auth.Login(scenario.loginUserQuery)
|
||||
|
||||
Convey("it should return invalid credentials error", func() {
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
})
|
||||
})
|
||||
|
||||
AuthScenario("When login with valid credentials", func(scenario *scenarioContext) {
|
||||
conn := &mockLdapConn{}
|
||||
entry := ldap.Entry{
|
||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "username", Values: []string{"markelog"}},
|
||||
{Name: "surname", Values: []string{"Gaidarenko"}},
|
||||
{Name: "email", Values: []string{"markelog@gmail.com"}},
|
||||
{Name: "name", Values: []string{"Oleg"}},
|
||||
{Name: "memberof", Values: []string{"admins"}},
|
||||
},
|
||||
}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
conn.setSearchResult(&result)
|
||||
|
||||
conn.bindProvider = func(username, password string) error {
|
||||
return nil
|
||||
}
|
||||
auth := &Auth{
|
||||
server: &ServerConfig{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
conn: conn,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
err := auth.Login(scenario.loginUserQuery)
|
||||
|
||||
Convey("it should not return error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("it should get user", func() {
|
||||
So(scenario.loginUserQuery.User.Login, ShouldEqual, "markelog")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
496
pkg/services/ldap/ldap_test.go
Normal file
496
pkg/services/ldap/ldap_test.go
Normal file
@@ -0,0 +1,496 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
Convey("initialBind", t, func() {
|
||||
Convey("Given bind dn and password configured", func() {
|
||||
conn := &mockLdapConn{}
|
||||
var actualUsername, actualPassword string
|
||||
conn.bindProvider = func(username, password string) error {
|
||||
actualUsername = username
|
||||
actualPassword = password
|
||||
return nil
|
||||
}
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &ServerConfig{
|
||||
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
||||
BindPassword: "bindpwd",
|
||||
},
|
||||
}
|
||||
err := Auth.initialBind("user", "pwd")
|
||||
So(err, ShouldBeNil)
|
||||
So(Auth.requireSecondBind, ShouldBeTrue)
|
||||
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
|
||||
So(actualPassword, ShouldEqual, "bindpwd")
|
||||
})
|
||||
|
||||
Convey("Given bind dn configured", func() {
|
||||
conn := &mockLdapConn{}
|
||||
var actualUsername, actualPassword string
|
||||
conn.bindProvider = func(username, password string) error {
|
||||
actualUsername = username
|
||||
actualPassword = password
|
||||
return nil
|
||||
}
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &ServerConfig{
|
||||
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
||||
},
|
||||
}
|
||||
err := Auth.initialBind("user", "pwd")
|
||||
So(err, ShouldBeNil)
|
||||
So(Auth.requireSecondBind, ShouldBeFalse)
|
||||
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
|
||||
So(actualPassword, ShouldEqual, "pwd")
|
||||
})
|
||||
|
||||
Convey("Given empty bind dn and password", func() {
|
||||
conn := &mockLdapConn{}
|
||||
unauthenticatedBindWasCalled := false
|
||||
var actualUsername string
|
||||
conn.unauthenticatedBindProvider = func(username string) error {
|
||||
unauthenticatedBindWasCalled = true
|
||||
actualUsername = username
|
||||
return nil
|
||||
}
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &ServerConfig{},
|
||||
}
|
||||
err := Auth.initialBind("user", "pwd")
|
||||
So(err, ShouldBeNil)
|
||||
So(Auth.requireSecondBind, ShouldBeTrue)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldBeEmpty)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("serverBind", t, func() {
|
||||
Convey("Given bind dn and password configured", func() {
|
||||
conn := &mockLdapConn{}
|
||||
var actualUsername, actualPassword string
|
||||
conn.bindProvider = func(username, password string) error {
|
||||
actualUsername = username
|
||||
actualPassword = password
|
||||
return nil
|
||||
}
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &ServerConfig{
|
||||
BindDN: "o=users,dc=grafana,dc=org",
|
||||
BindPassword: "bindpwd",
|
||||
},
|
||||
}
|
||||
err := Auth.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
|
||||
So(actualPassword, ShouldEqual, "bindpwd")
|
||||
})
|
||||
|
||||
Convey("Given bind dn configured", func() {
|
||||
conn := &mockLdapConn{}
|
||||
unauthenticatedBindWasCalled := false
|
||||
var actualUsername string
|
||||
conn.unauthenticatedBindProvider = func(username string) error {
|
||||
unauthenticatedBindWasCalled = true
|
||||
actualUsername = username
|
||||
return nil
|
||||
}
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &ServerConfig{
|
||||
BindDN: "o=users,dc=grafana,dc=org",
|
||||
},
|
||||
}
|
||||
err := Auth.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
|
||||
})
|
||||
|
||||
Convey("Given empty bind dn and password", func() {
|
||||
conn := &mockLdapConn{}
|
||||
unauthenticatedBindWasCalled := false
|
||||
var actualUsername string
|
||||
conn.unauthenticatedBindProvider = func(username string) error {
|
||||
unauthenticatedBindWasCalled = true
|
||||
actualUsername = username
|
||||
return nil
|
||||
}
|
||||
Auth := &Auth{
|
||||
conn: conn,
|
||||
server: &ServerConfig{},
|
||||
}
|
||||
err := Auth.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldBeEmpty)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When translating ldap user to grafana user", t, func() {
|
||||
|
||||
var user1 = &m.User{}
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpsertUserCommand) error {
|
||||
cmd.Result = user1
|
||||
cmd.Result.Login = "torkelo"
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("Given no ldap group map match", func() {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{{}},
|
||||
})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
|
||||
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
})
|
||||
|
||||
AuthScenario("Given wildcard group match", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "*", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
AuthScenario("Given exact group match", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"cn=users"}})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
AuthScenario("Given group match with different case", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"CN=users"}})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
AuthScenario("Given no existing grafana user", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=admin", OrgRole: "Admin"},
|
||||
{GroupDN: "cn=editor", OrgRole: "Editor"},
|
||||
{GroupDN: "*", OrgRole: "Viewer"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userQueryReturns(nil)
|
||||
|
||||
result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
DN: "torkelo",
|
||||
Username: "torkelo",
|
||||
Email: "my@email.com",
|
||||
MemberOf: []string{"cn=editor"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should return new user", func() {
|
||||
So(result.Login, ShouldEqual, "torkelo")
|
||||
})
|
||||
|
||||
Convey("Should set isGrafanaAdmin to false by default", func() {
|
||||
So(result.IsAdmin, ShouldBeFalse)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Convey("When syncing ldap groups to grafana org roles", t, func() {
|
||||
AuthScenario("given no current user orgs", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
Convey("Should create new org user", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.addOrgUserCmd, ShouldNotBeNil)
|
||||
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
|
||||
})
|
||||
})
|
||||
|
||||
AuthScenario("given different current org role", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
Convey("Should update org role", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.updateOrgUserCmd, ShouldNotBeNil)
|
||||
So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
AuthScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{
|
||||
{OrgId: 1, Role: m.ROLE_EDITOR},
|
||||
{OrgId: 2, Role: m.ROLE_EDITOR},
|
||||
})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
Convey("Should remove org role", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.removeOrgUserCmd, ShouldNotBeNil)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
AuthScenario("given org role is updated in config", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=admin", OrgId: 1, OrgRole: "Admin"},
|
||||
{GroupDN: "cn=users", OrgId: 1, OrgRole: "Viewer"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
Convey("Should update org role", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.removeOrgUserCmd, ShouldBeNil)
|
||||
So(sc.updateOrgUserCmd, ShouldNotBeNil)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
AuthScenario("given multiple matching ldap groups", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
|
||||
{GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
Convey("Should take first match, and ignore subsequent matches", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.updateOrgUserCmd, ShouldBeNil)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
AuthScenario("given multiple matching ldap groups and no existing groups", func(sc *scenarioContext) {
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
|
||||
{GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
Convey("Should take first match, and ignore subsequent matches", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("Should not update permissions unless specified", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.updateUserPermissionsCmd, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
AuthScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
|
||||
trueVal := true
|
||||
|
||||
Auth := New(&ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
Convey("Should create user with admin set to true", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When calling SyncUser", t, func() {
|
||||
mockLdapConnection := &mockLdapConn{}
|
||||
|
||||
auth := &Auth{
|
||||
server: &ServerConfig{
|
||||
Host: "",
|
||||
RootCACert: "",
|
||||
Groups: []*GroupToOrgRole{
|
||||
{GroupDN: "*", OrgRole: "Admin"},
|
||||
},
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Surname: "surname",
|
||||
Email: "email",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
conn: mockLdapConnection,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
dialCalled := false
|
||||
dial = func(network, addr string) (IConnection, error) {
|
||||
dialCalled = true
|
||||
return mockLdapConnection, nil
|
||||
}
|
||||
|
||||
entry := ldap.Entry{
|
||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "username", Values: []string{"roelgerrits"}},
|
||||
{Name: "surname", Values: []string{"Gerrits"}},
|
||||
{Name: "email", Values: []string{"roel@test.com"}},
|
||||
{Name: "name", Values: []string{"Roel"}},
|
||||
{Name: "memberof", Values: []string{"admins"}},
|
||||
}}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
mockLdapConnection.setSearchResult(&result)
|
||||
|
||||
AuthScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
|
||||
// arrange
|
||||
query := &m.LoginUserQuery{
|
||||
Username: "roelgerrits",
|
||||
}
|
||||
|
||||
hookDial = nil
|
||||
|
||||
sc.userQueryReturns(&m.User{
|
||||
Id: 1,
|
||||
Email: "roel@test.net",
|
||||
Name: "Roel Gerrits",
|
||||
Login: "roelgerrits",
|
||||
})
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
|
||||
// act
|
||||
syncErrResult := auth.SyncUser(query)
|
||||
|
||||
// assert
|
||||
So(dialCalled, ShouldBeTrue)
|
||||
So(syncErrResult, ShouldBeNil)
|
||||
// User should be searched in ldap
|
||||
So(mockLdapConnection.searchCalled, ShouldBeTrue)
|
||||
// Info should be updated (email differs)
|
||||
So(sc.updateUserCmd.Email, ShouldEqual, "roel@test.com")
|
||||
// User should have admin privileges
|
||||
So(sc.addOrgUserCmd.UserId, ShouldEqual, 1)
|
||||
So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When searching for a user and not all five attributes are mapped", t, func() {
|
||||
mockLdapConnection := &mockLdapConn{}
|
||||
entry := ldap.Entry{
|
||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "username", Values: []string{"roelgerrits"}},
|
||||
{Name: "surname", Values: []string{"Gerrits"}},
|
||||
{Name: "email", Values: []string{"roel@test.com"}},
|
||||
{Name: "name", Values: []string{"Roel"}},
|
||||
{Name: "memberof", Values: []string{"admins"}},
|
||||
}}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
mockLdapConnection.setSearchResult(&result)
|
||||
|
||||
// Set up attribute map without surname and email
|
||||
Auth := &Auth{
|
||||
server: &ServerConfig{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
conn: mockLdapConnection,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
searchResult, err := Auth.searchForUser("roelgerrits")
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(searchResult, ShouldNotBeNil)
|
||||
|
||||
// User should be searched in ldap
|
||||
So(mockLdapConnection.searchCalled, ShouldBeTrue)
|
||||
|
||||
// No empty attributes should be added to the search request
|
||||
So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3)
|
||||
})
|
||||
}
|
||||
126
pkg/services/ldap/settings.go
Normal file
126
pkg/services/ldap/settings.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Servers []*ServerConfig `toml:"servers"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
UseSSL bool `toml:"use_ssl"`
|
||||
StartTLS bool `toml:"start_tls"`
|
||||
SkipVerifySSL bool `toml:"ssl_skip_verify"`
|
||||
RootCACert string `toml:"root_ca_cert"`
|
||||
ClientCert string `toml:"client_cert"`
|
||||
ClientKey string `toml:"client_key"`
|
||||
BindDN string `toml:"bind_dn"`
|
||||
BindPassword string `toml:"bind_password"`
|
||||
Attr AttributeMap `toml:"attributes"`
|
||||
|
||||
SearchFilter string `toml:"search_filter"`
|
||||
SearchBaseDNs []string `toml:"search_base_dns"`
|
||||
|
||||
GroupSearchFilter string `toml:"group_search_filter"`
|
||||
GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute"`
|
||||
GroupSearchBaseDNs []string `toml:"group_search_base_dns"`
|
||||
|
||||
Groups []*GroupToOrgRole `toml:"group_mappings"`
|
||||
}
|
||||
|
||||
type AttributeMap struct {
|
||||
Username string `toml:"username"`
|
||||
Name string `toml:"name"`
|
||||
Surname string `toml:"surname"`
|
||||
Email string `toml:"email"`
|
||||
MemberOf string `toml:"member_of"`
|
||||
}
|
||||
|
||||
type GroupToOrgRole struct {
|
||||
GroupDN string `toml:"group_dn"`
|
||||
OrgId int64 `toml:"org_id"`
|
||||
IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatibility)
|
||||
OrgRole m.RoleType `toml:"org_role"`
|
||||
}
|
||||
|
||||
var config *Config
|
||||
var logger = log.New("ldap")
|
||||
|
||||
// IsEnabled checks if ldap is enabled
|
||||
func IsEnabled() bool {
|
||||
return setting.LdapEnabled
|
||||
}
|
||||
|
||||
// ReadConfig reads the config if
|
||||
// ldap is enabled otherwise it will return nil
|
||||
func ReadConfig() *Config {
|
||||
if IsEnabled() == false {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make it a singleton
|
||||
if config != nil {
|
||||
return config
|
||||
}
|
||||
|
||||
config = getConfig(setting.LdapConfigFile)
|
||||
|
||||
return config
|
||||
}
|
||||
func getConfig(configFile string) *Config {
|
||||
result := &Config{}
|
||||
|
||||
logger.Info("Ldap enabled, reading config file", "file", configFile)
|
||||
|
||||
_, err := toml.DecodeFile(configFile, result)
|
||||
if err != nil {
|
||||
logger.Crit("Failed to load ldap config file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(result.Servers) == 0 {
|
||||
logger.Crit("ldap enabled but no ldap servers defined in config file")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// set default org id
|
||||
for _, server := range result.Servers {
|
||||
assertNotEmptyCfg(server.SearchFilter, "search_filter")
|
||||
assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns")
|
||||
|
||||
for _, groupMap := range server.Groups {
|
||||
if groupMap.OrgId == 0 {
|
||||
groupMap.OrgId = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func assertNotEmptyCfg(val interface{}, propName string) {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
logger.Crit("LDAP config file is missing option", "option", propName)
|
||||
os.Exit(1)
|
||||
}
|
||||
case []string:
|
||||
if len(v) == 0 {
|
||||
logger.Crit("LDAP config file is missing option", "option", propName)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Println("unknown")
|
||||
}
|
||||
}
|
||||
165
pkg/services/ldap/test.go
Normal file
165
pkg/services/ldap/test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
)
|
||||
|
||||
type mockLdapConn struct {
|
||||
result *ldap.SearchResult
|
||||
searchCalled bool
|
||||
searchAttributes []string
|
||||
bindProvider func(username, password string) error
|
||||
unauthenticatedBindProvider func(username string) error
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Bind(username, password string) error {
|
||||
if c.bindProvider != nil {
|
||||
return c.bindProvider(username, password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) UnauthenticatedBind(username string) error {
|
||||
if c.unauthenticatedBindProvider != nil {
|
||||
return c.unauthenticatedBindProvider(username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Close() {}
|
||||
|
||||
func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) {
|
||||
c.result = result
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
c.searchCalled = true
|
||||
c.searchAttributes = sr.Attributes
|
||||
return c.result, nil
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) StartTLS(*tls.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AuthScenario(desc string, fn scenarioFunc) {
|
||||
Convey(desc, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := &scenarioContext{
|
||||
loginUserQuery: &models.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
},
|
||||
}
|
||||
|
||||
hookDial = func(auth *Auth) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
loginService := &login.LoginService{
|
||||
Bus: bus.GetBus(),
|
||||
}
|
||||
|
||||
bus.AddHandler("test", loginService.UpsertUser)
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *models.SyncTeamsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *models.UpdateUserPermissionsCommand) error {
|
||||
sc.updateUserPermissionsCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.GetUserByAuthInfoQuery) error {
|
||||
sc.getUserByAuthInfoQuery = cmd
|
||||
sc.getUserByAuthInfoQuery.Result = &models.User{Login: cmd.Login}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.GetUserOrgListQuery) error {
|
||||
sc.getUserOrgListQuery = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.CreateUserCommand) error {
|
||||
sc.createUserCmd = cmd
|
||||
sc.createUserCmd.Result = models.User{Login: cmd.Login}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.AddOrgUserCommand) error {
|
||||
sc.addOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.UpdateOrgUserCommand) error {
|
||||
sc.updateOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.RemoveOrgUserCommand) error {
|
||||
sc.removeOrgUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.UpdateUserCommand) error {
|
||||
sc.updateUserCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.SetUsingOrgCommand) error {
|
||||
sc.setUsingOrgCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
loginUserQuery *models.LoginUserQuery
|
||||
getUserByAuthInfoQuery *models.GetUserByAuthInfoQuery
|
||||
getUserOrgListQuery *models.GetUserOrgListQuery
|
||||
createUserCmd *models.CreateUserCommand
|
||||
addOrgUserCmd *models.AddOrgUserCommand
|
||||
updateOrgUserCmd *models.UpdateOrgUserCommand
|
||||
removeOrgUserCmd *models.RemoveOrgUserCommand
|
||||
updateUserCmd *models.UpdateUserCommand
|
||||
setUsingOrgCmd *models.SetUsingOrgCommand
|
||||
updateUserPermissionsCmd *models.UpdateUserPermissionsCommand
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) userQueryReturns(user *models.User) {
|
||||
bus.AddHandler("test", func(query *models.GetUserByAuthInfoQuery) error {
|
||||
if user == nil {
|
||||
return models.ErrUserNotFound
|
||||
}
|
||||
query.Result = user
|
||||
return nil
|
||||
})
|
||||
bus.AddHandler("test", func(query *models.SetAuthInfoCommand) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) userOrgsQueryReturns(orgs []*models.UserOrgDTO) {
|
||||
bus.AddHandler("test", func(query *models.GetUserOrgListQuery) error {
|
||||
query.Result = orgs
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
27
pkg/services/ldap/user.go
Normal file
27
pkg/services/ldap/user.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
DN string
|
||||
FirstName string
|
||||
LastName string
|
||||
Username string
|
||||
Email string
|
||||
MemberOf []string
|
||||
}
|
||||
|
||||
func (u *UserInfo) isMemberOf(group string) bool {
|
||||
if group == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, member := range u.MemberOf {
|
||||
if strings.EqualFold(member, group) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user