mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LDAP: refactoring (#17479)
* LDAP: use only one struct * Use only models.ExternalUserInfo * Add additional helper method :/ * Move all the helpers to one module * LDAP: refactoring * Rename some of the public methods and change their behaviour * Remove outdated methods * Simplify logic * More tests There is no and never were tests for settings.go, added tests for helper methods (cover is now about 100% for them). Added tests for the main LDAP logic, but there is some stuff to add. Dial() is not tested and not decoupled. It might be a challenge to do it properly * Restructure tests: * they wouldn't depend on external modules * more consistent naming * logical division * More guards for erroneous paths * Login: make login service an explicit dependency * LDAP: remove no longer needed test helper fns * LDAP: remove useless import * LDAP: Use new interface in multildap module * LDAP: corrections for the groups of multiple users * In case there is several users their groups weren't detected correctly * Simplify helpers module
This commit is contained in:
parent
c78b6e2a67
commit
1b1d951495
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
|
"github.com/grafana/grafana/pkg/services/login"
|
||||||
"github.com/grafana/grafana/pkg/services/quota"
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -66,6 +67,7 @@ type HTTPServer struct {
|
|||||||
QuotaService *quota.QuotaService `inject:""`
|
QuotaService *quota.QuotaService `inject:""`
|
||||||
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
||||||
ProvisioningService ProvisioningService `inject:""`
|
ProvisioningService ProvisioningService `inject:""`
|
||||||
|
Login *login.LoginService `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) Init() error {
|
func (hs *HTTPServer) Init() error {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package login
|
package login
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/multildap"
|
"github.com/grafana/grafana/pkg/services/multildap"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
)
|
)
|
||||||
@ -36,15 +36,15 @@ var loginUsingLDAP = func(query *models.LoginUserQuery) (bool, error) {
|
|||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
login, err := user.Upsert(&user.UpsertArgs{
|
upsert := &models.UpsertUserCommand{
|
||||||
ExternalUser: externalUser,
|
ExternalUser: externalUser,
|
||||||
SignupAllowed: setting.LDAPAllowSignup,
|
SignupAllowed: setting.LDAPAllowSignup,
|
||||||
})
|
}
|
||||||
|
err = bus.Dispatch(upsert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
query.User = upsert.Result
|
||||||
query.User = login
|
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ldap"
|
"github.com/grafana/grafana/pkg/services/ldap"
|
||||||
"github.com/grafana/grafana/pkg/services/multildap"
|
"github.com/grafana/grafana/pkg/services/multildap"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -211,16 +210,17 @@ func (auth *AuthProxy) LoginViaLDAP() (int64, *Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Have to sync grafana and LDAP user during log in
|
// Have to sync grafana and LDAP user during log in
|
||||||
user, err := user.Upsert(&user.UpsertArgs{
|
upsert := &models.UpsertUserCommand{
|
||||||
ReqContext: auth.ctx,
|
ReqContext: auth.ctx,
|
||||||
SignupAllowed: auth.LDAPAllowSignup,
|
SignupAllowed: auth.LDAPAllowSignup,
|
||||||
ExternalUser: extUser,
|
ExternalUser: extUser,
|
||||||
})
|
}
|
||||||
|
err = bus.Dispatch(upsert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, newError(err.Error(), nil)
|
return 0, newError(err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.Id, nil
|
return upsert.Result.Id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginViaHeader logs in user from the header only
|
// LoginViaHeader logs in user from the header only
|
||||||
@ -256,16 +256,17 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := user.Upsert(&user.UpsertArgs{
|
upsert := &models.UpsertUserCommand{
|
||||||
ReqContext: auth.ctx,
|
ReqContext: auth.ctx,
|
||||||
SignupAllowed: true,
|
SignupAllowed: true,
|
||||||
ExternalUser: extUser,
|
ExternalUser: extUser,
|
||||||
})
|
}
|
||||||
|
err := bus.Dispatch(upsert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.Id, nil
|
return upsert.Result.Id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSignedUser get full signed user info
|
// GetSignedUser get full signed user info
|
||||||
|
@ -26,7 +26,10 @@ var (
|
|||||||
ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN)
|
ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN)
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetContextHandler(ats m.UserTokenService, remoteCache *remotecache.RemoteCache) macaron.Handler {
|
func GetContextHandler(
|
||||||
|
ats m.UserTokenService,
|
||||||
|
remoteCache *remotecache.RemoteCache,
|
||||||
|
) macaron.Handler {
|
||||||
return func(c *macaron.Context) {
|
return func(c *macaron.Context) {
|
||||||
ctx := &m.ReqContext{
|
ctx := &m.ReqContext{
|
||||||
Context: c,
|
Context: c,
|
||||||
|
49
pkg/services/ldap/helpers.go
Normal file
49
pkg/services/ldap/helpers.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/ldap.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isMemberOf(memberOf []string, group string) bool {
|
||||||
|
if group == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, member := range memberOf {
|
||||||
|
if strings.EqualFold(member, group) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendIfNotEmpty(slice []string, values ...string) []string {
|
||||||
|
for _, v := range values {
|
||||||
|
if v != "" {
|
||||||
|
slice = append(slice, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAttribute(name string, entry *ldap.Entry) string {
|
||||||
|
for _, attr := range entry.Attributes {
|
||||||
|
if attr.Name == name {
|
||||||
|
if len(attr.Values) > 0 {
|
||||||
|
return attr.Values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArrayAttribute(name string, entry *ldap.Entry) []string {
|
||||||
|
for _, attr := range entry.Attributes {
|
||||||
|
if attr.Name == name && len(attr.Values) > 0 {
|
||||||
|
return attr.Values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"gopkg.in/ldap.v3"
|
"gopkg.in/ldap.v3"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
@ -30,17 +31,16 @@ type IConnection interface {
|
|||||||
type IServer interface {
|
type IServer interface {
|
||||||
Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error)
|
Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error)
|
||||||
Users([]string) ([]*models.ExternalUserInfo, error)
|
Users([]string) ([]*models.ExternalUserInfo, error)
|
||||||
InitialBind(string, string) error
|
Auth(string, string) error
|
||||||
Dial() error
|
Dial() error
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is basic struct of LDAP authorization
|
// Server is basic struct of LDAP authorization
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Config *ServerConfig
|
Config *ServerConfig
|
||||||
Connection IConnection
|
Connection IConnection
|
||||||
requireSecondBind bool
|
log log.Logger
|
||||||
log log.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -49,10 +49,6 @@ var (
|
|||||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
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
|
// New creates the new LDAP auth
|
||||||
func New(config *ServerConfig) IServer {
|
func New(config *ServerConfig) IServer {
|
||||||
return &Server{
|
return &Server{
|
||||||
@ -96,7 +92,7 @@ func (server *Server) Dial() error {
|
|||||||
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
|
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
|
||||||
}
|
}
|
||||||
if server.Config.StartTLS {
|
if server.Config.StartTLS {
|
||||||
server.Connection, err = dial("tcp", address)
|
server.Connection, err = ldap.Dial("tcp", address)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err = server.Connection.StartTLS(tlsCfg); err == nil {
|
if err = server.Connection.StartTLS(tlsCfg); err == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -106,7 +102,7 @@ func (server *Server) Dial() error {
|
|||||||
server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg)
|
server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
server.Connection, err = dial("tcp", address)
|
server.Connection, err = ldap.Dial("tcp", address)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -125,9 +121,8 @@ func (server *Server) Close() {
|
|||||||
func (server *Server) Login(query *models.LoginUserQuery) (
|
func (server *Server) Login(query *models.LoginUserQuery) (
|
||||||
*models.ExternalUserInfo, error,
|
*models.ExternalUserInfo, error,
|
||||||
) {
|
) {
|
||||||
|
// Authentication
|
||||||
// Perform initial authentication
|
err := server.Auth(query.Username, query.Password)
|
||||||
err := server.InitialBind(query.Username, query.Password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -145,20 +140,11 @@ func (server *Server) Login(query *models.LoginUserQuery) (
|
|||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a second user bind is needed
|
|
||||||
user := users[0]
|
user := users[0]
|
||||||
|
|
||||||
if err := server.validateGrafanaUser(user); err != nil {
|
if err := server.validateGrafanaUser(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if server.requireSecondBind {
|
|
||||||
err = server.secondBind(user, query.Password)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,8 +154,8 @@ func (server *Server) Users(logins []string) (
|
|||||||
error,
|
error,
|
||||||
) {
|
) {
|
||||||
var result *ldap.SearchResult
|
var result *ldap.SearchResult
|
||||||
var err error
|
|
||||||
var Config = server.Config
|
var Config = server.Config
|
||||||
|
var err error
|
||||||
|
|
||||||
for _, base := range Config.SearchBaseDNs {
|
for _, base := range Config.SearchBaseDNs {
|
||||||
result, err = server.Connection.Search(
|
result, err = server.Connection.Search(
|
||||||
@ -184,11 +170,17 @@ func (server *Server) Users(logins []string) (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(result.Entries) == 0 {
|
||||||
|
return []*models.ExternalUserInfo{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
serializedUsers, err := server.serializeUsers(result)
|
serializedUsers, err := server.serializeUsers(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.log.Debug("LDAP users found", "users", spew.Sdump(serializedUsers))
|
||||||
|
|
||||||
return serializedUsers, nil
|
return serializedUsers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,108 +268,71 @@ func (server *Server) getSearchRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo
|
// buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo
|
||||||
func (server *Server) buildGrafanaUser(user *UserInfo) *models.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{
|
extUser := &models.ExternalUserInfo{
|
||||||
AuthModule: models.AuthModuleLDAP,
|
AuthModule: models.AuthModuleLDAP,
|
||||||
AuthId: user.DN,
|
AuthId: user.DN,
|
||||||
Name: strings.TrimSpace(
|
Name: strings.TrimSpace(
|
||||||
fmt.Sprintf("%s %s", user.FirstName, user.LastName),
|
fmt.Sprintf(
|
||||||
|
"%s %s",
|
||||||
|
getAttribute(attrs.Name, user),
|
||||||
|
getAttribute(attrs.Surname, user),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Login: user.Username,
|
Login: getAttribute(attrs.Username, user),
|
||||||
Email: user.Email,
|
Email: getAttribute(attrs.Email, user),
|
||||||
Groups: user.MemberOf,
|
Groups: memberOf,
|
||||||
OrgRoles: map[int64]models.RoleType{},
|
OrgRoles: map[int64]models.RoleType{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range server.Config.Groups {
|
for _, group := range server.Config.Groups {
|
||||||
// only use the first match for each org
|
// only use the first match for each org
|
||||||
if extUser.OrgRoles[group.OrgId] != "" {
|
if extUser.OrgRoles[group.OrgID] != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.isMemberOf(group.GroupDN) {
|
if isMemberOf(memberOf, group.GroupDN) {
|
||||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
extUser.OrgRoles[group.OrgID] = group.OrgRole
|
||||||
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
|
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
|
||||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extUser
|
return extUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) serverBind() error {
|
// shouldBindAdmin checks if we should use
|
||||||
bindFn := func() error {
|
// admin username & password for LDAP bind
|
||||||
return server.Connection.Bind(
|
func (server *Server) shouldBindAdmin() bool {
|
||||||
server.Config.BindDN,
|
return server.Config.BindPassword != ""
|
||||||
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(
|
// Auth authentificates user in LDAP.
|
||||||
user *models.ExternalUserInfo,
|
// It might not use passed password and username,
|
||||||
userPassword string,
|
// since they can be overwritten with admin config values -
|
||||||
) error {
|
// see "bind_dn" and "bind_password" options in LDAP config
|
||||||
err := server.Connection.Bind(user.AuthId, userPassword)
|
func (server *Server) Auth(username, password string) error {
|
||||||
if err != nil {
|
path := server.Config.BindDN
|
||||||
server.log.Info("Second bind failed", "error", err)
|
|
||||||
|
|
||||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
if server.shouldBindAdmin() {
|
||||||
if ldapErr.ResultCode == 49 {
|
password = server.Config.BindPassword
|
||||||
return ErrInvalidCredentials
|
} else {
|
||||||
}
|
path = fmt.Sprintf(path, username)
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitialBind intiates first bind to LDAP server
|
|
||||||
func (server *Server) InitialBind(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 {
|
bindFn := func() error {
|
||||||
return server.Connection.Bind(bindPath, userPassword)
|
return server.Connection.Bind(path, password)
|
||||||
}
|
|
||||||
|
|
||||||
if userPassword == "" {
|
|
||||||
bindFn = func() error {
|
|
||||||
return server.Connection.UnauthenticatedBind(bindPath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bindFn(); err != nil {
|
if err := bindFn(); err != nil {
|
||||||
server.log.Info("Initial bind failed", "error", err)
|
server.log.Error("Cannot authentificate in LDAP", "err", err)
|
||||||
|
|
||||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||||
if ldapErr.ResultCode == 49 {
|
if ldapErr.ResultCode == 49 {
|
||||||
@ -391,19 +346,23 @@ func (server *Server) InitialBind(username, userPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// requestMemberOf use this function when POSIX LDAP schema does not support memberOf, so it manually search the groups
|
// 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) {
|
func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) {
|
||||||
var memberOf []string
|
var memberOf []string
|
||||||
|
var config = server.Config
|
||||||
|
|
||||||
for _, groupSearchBase := range server.Config.GroupSearchBaseDNs {
|
for _, groupSearchBase := range config.GroupSearchBaseDNs {
|
||||||
var filterReplace string
|
var filterReplace string
|
||||||
if server.Config.GroupSearchFilterUserAttribute == "" {
|
if config.GroupSearchFilterUserAttribute == "" {
|
||||||
filterReplace = getLDAPAttr(server.Config.Attr.Username, searchResult)
|
filterReplace = getAttribute(config.Attr.Username, entry)
|
||||||
} else {
|
} else {
|
||||||
filterReplace = getLDAPAttr(server.Config.GroupSearchFilterUserAttribute, searchResult)
|
filterReplace = getAttribute(
|
||||||
|
config.GroupSearchFilterUserAttribute,
|
||||||
|
entry,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := strings.Replace(
|
filter := strings.Replace(
|
||||||
server.Config.GroupSearchFilter, "%s",
|
config.GroupSearchFilter, "%s",
|
||||||
ldap.EscapeFilter(filterReplace),
|
ldap.EscapeFilter(filterReplace),
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
@ -411,7 +370,7 @@ func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string
|
|||||||
server.log.Info("Searching for user's groups", "filter", filter)
|
server.log.Info("Searching for user's groups", "filter", filter)
|
||||||
|
|
||||||
// support old way of reading settings
|
// support old way of reading settings
|
||||||
groupIDAttribute := server.Config.Attr.MemberOf
|
groupIDAttribute := config.Attr.MemberOf
|
||||||
// but prefer dn attribute if default settings are used
|
// but prefer dn attribute if default settings are used
|
||||||
if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
|
if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
|
||||||
groupIDAttribute = "dn"
|
groupIDAttribute = "dn"
|
||||||
@ -431,8 +390,11 @@ func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(groupSearchResult.Entries) > 0 {
|
if len(groupSearchResult.Entries) > 0 {
|
||||||
for i := range groupSearchResult.Entries {
|
for _, group := range groupSearchResult.Entries {
|
||||||
memberOf = append(memberOf, getLDAPAttrN(groupIDAttribute, groupSearchResult, i))
|
memberOf = append(
|
||||||
|
memberOf,
|
||||||
|
getAttribute(groupIDAttribute, group),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -448,104 +410,32 @@ func (server *Server) serializeUsers(
|
|||||||
) ([]*models.ExternalUserInfo, error) {
|
) ([]*models.ExternalUserInfo, error) {
|
||||||
var serialized []*models.ExternalUserInfo
|
var serialized []*models.ExternalUserInfo
|
||||||
|
|
||||||
for index := range users.Entries {
|
for _, user := range users.Entries {
|
||||||
memberOf, err := server.getMemberOf(users)
|
extUser, err := server.buildGrafanaUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userInfo := &UserInfo{
|
serialized = append(serialized, extUser)
|
||||||
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
|
return serialized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMemberOf finds memberOf property or request it
|
// getMemberOf finds memberOf property or request it
|
||||||
func (server *Server) getMemberOf(search *ldap.SearchResult) (
|
func (server *Server) getMemberOf(result *ldap.Entry) (
|
||||||
[]string, error,
|
[]string, error,
|
||||||
) {
|
) {
|
||||||
if server.Config.GroupSearchFilter == "" {
|
if server.Config.GroupSearchFilter == "" {
|
||||||
memberOf := getLDAPAttrArray(server.Config.Attr.MemberOf, search)
|
memberOf := getArrayAttribute(server.Config.Attr.MemberOf, result)
|
||||||
|
|
||||||
return memberOf, nil
|
return memberOf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
memberOf, err := server.requestMemberOf(search)
|
memberOf, err := server.requestMemberOf(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return memberOf, nil
|
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{}
|
|
||||||
}
|
|
||||||
|
@ -5,136 +5,87 @@ import (
|
|||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
"gopkg.in/ldap.v3"
|
"gopkg.in/ldap.v3"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLDAPHelpers(t *testing.T) {
|
func TestLDAPHelpers(t *testing.T) {
|
||||||
Convey("serializeUsers()", t, func() {
|
Convey("isMemberOf()", t, func() {
|
||||||
Convey("simple case", func() {
|
Convey("Wildcard", func() {
|
||||||
server := &Server{
|
result := isMemberOf([]string{}, "*")
|
||||||
Config: &ServerConfig{
|
So(result, ShouldBeTrue)
|
||||||
Attr: AttributeMap{
|
|
||||||
Username: "username",
|
|
||||||
Name: "name",
|
|
||||||
MemberOf: "memberof",
|
|
||||||
Email: "email",
|
|
||||||
},
|
|
||||||
SearchBaseDNs: []string{"BaseDNHere"},
|
|
||||||
},
|
|
||||||
Connection: &MockConnection{},
|
|
||||||
log: log.New("test-logger"),
|
|
||||||
}
|
|
||||||
|
|
||||||
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"}},
|
|
||||||
}}
|
|
||||||
users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
|
||||||
|
|
||||||
result, err := server.serializeUsers(users)
|
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(result[0].Login, ShouldEqual, "roelgerrits")
|
|
||||||
So(result[0].Email, ShouldEqual, "roel@test.com")
|
|
||||||
So(result[0].Groups, ShouldContain, "admins")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("without lastname", func() {
|
Convey("Should find one", func() {
|
||||||
server := &Server{
|
result := isMemberOf([]string{"one", "Two", "three"}, "two")
|
||||||
Config: &ServerConfig{
|
So(result, ShouldBeTrue)
|
||||||
Attr: AttributeMap{
|
})
|
||||||
Username: "username",
|
|
||||||
Name: "name",
|
|
||||||
MemberOf: "memberof",
|
|
||||||
Email: "email",
|
|
||||||
},
|
|
||||||
SearchBaseDNs: []string{"BaseDNHere"},
|
|
||||||
},
|
|
||||||
Connection: &MockConnection{},
|
|
||||||
log: log.New("test-logger"),
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := ldap.Entry{
|
Convey("Should not find one", func() {
|
||||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
result := isMemberOf([]string{"one", "Two", "three"}, "twos")
|
||||||
{Name: "username", Values: []string{"roelgerrits"}},
|
So(result, ShouldBeFalse)
|
||||||
{Name: "email", Values: []string{"roel@test.com"}},
|
|
||||||
{Name: "name", Values: []string{"Roel"}},
|
|
||||||
{Name: "memberof", Values: []string{"admins"}},
|
|
||||||
}}
|
|
||||||
users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
|
||||||
|
|
||||||
result, err := server.serializeUsers(users)
|
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(result[0].Name, ShouldEqual, "Roel")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("serverBind()", t, func() {
|
Convey("getAttribute()", t, func() {
|
||||||
Convey("Given bind dn and password configured", func() {
|
Convey("Should get username", func() {
|
||||||
connection := &MockConnection{}
|
value := []string{"roelgerrits"}
|
||||||
var actualUsername, actualPassword string
|
entry := &ldap.Entry{
|
||||||
connection.bindProvider = func(username, password string) error {
|
Attributes: []*ldap.EntryAttribute{
|
||||||
actualUsername = username
|
{
|
||||||
actualPassword = password
|
Name: "username", Values: value,
|
||||||
return nil
|
},
|
||||||
}
|
|
||||||
server := &Server{
|
|
||||||
Connection: connection,
|
|
||||||
Config: &ServerConfig{
|
|
||||||
BindDN: "o=users,dc=grafana,dc=org",
|
|
||||||
BindPassword: "bindpwd",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := server.serverBind()
|
|
||||||
So(err, ShouldBeNil)
|
result := getAttribute("username", entry)
|
||||||
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
|
|
||||||
So(actualPassword, ShouldEqual, "bindpwd")
|
So(result, ShouldEqual, value[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Given bind dn configured", func() {
|
Convey("Should not get anything", func() {
|
||||||
connection := &MockConnection{}
|
value := []string{"roelgerrits"}
|
||||||
unauthenticatedBindWasCalled := false
|
entry := &ldap.Entry{
|
||||||
var actualUsername string
|
Attributes: []*ldap.EntryAttribute{
|
||||||
connection.unauthenticatedBindProvider = func(username string) error {
|
{
|
||||||
unauthenticatedBindWasCalled = true
|
Name: "killa", Values: value,
|
||||||
actualUsername = username
|
},
|
||||||
return nil
|
|
||||||
}
|
|
||||||
server := &Server{
|
|
||||||
Connection: connection,
|
|
||||||
Config: &ServerConfig{
|
|
||||||
BindDN: "o=users,dc=grafana,dc=org",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := server.serverBind()
|
|
||||||
So(err, ShouldBeNil)
|
result := getAttribute("username", entry)
|
||||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
|
||||||
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
|
So(result, ShouldEqual, "")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("getArrayAttribute()", t, func() {
|
||||||
|
Convey("Should get username", func() {
|
||||||
|
value := []string{"roelgerrits"}
|
||||||
|
entry := &ldap.Entry{
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "username", Values: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := getArrayAttribute("username", entry)
|
||||||
|
|
||||||
|
So(result, ShouldResemble, value)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Given empty bind dn and password", func() {
|
Convey("Should not get anything", func() {
|
||||||
connection := &MockConnection{}
|
value := []string{"roelgerrits"}
|
||||||
unauthenticatedBindWasCalled := false
|
entry := &ldap.Entry{
|
||||||
var actualUsername string
|
Attributes: []*ldap.EntryAttribute{
|
||||||
connection.unauthenticatedBindProvider = func(username string) error {
|
{
|
||||||
unauthenticatedBindWasCalled = true
|
Name: "username", Values: value,
|
||||||
actualUsername = username
|
},
|
||||||
return nil
|
},
|
||||||
}
|
}
|
||||||
server := &Server{
|
|
||||||
Connection: connection,
|
result := getArrayAttribute("something", entry)
|
||||||
Config: &ServerConfig{},
|
|
||||||
}
|
So(result, ShouldResemble, []string{})
|
||||||
err := server.serverBind()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
|
||||||
So(actualUsername, ShouldBeEmpty)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,88 +1,24 @@
|
|||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
|
||||||
"gopkg.in/ldap.v3"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
"gopkg.in/ldap.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLDAPLogin(t *testing.T) {
|
func TestLDAPLogin(t *testing.T) {
|
||||||
|
defaultLogin := &models.LoginUserQuery{
|
||||||
|
Username: "user",
|
||||||
|
Password: "pwd",
|
||||||
|
IpAddress: "192.168.1.1:56433",
|
||||||
|
}
|
||||||
|
|
||||||
Convey("Login()", t, func() {
|
Convey("Login()", t, func() {
|
||||||
serverScenario("When user is log in and updated", func(sc *scenarioContext) {
|
Convey("Should get invalid credentials when auth fails", func() {
|
||||||
// arrange
|
|
||||||
mockConnection := &MockConnection{}
|
|
||||||
|
|
||||||
server := &Server{
|
|
||||||
Config: &ServerConfig{
|
|
||||||
Host: "",
|
|
||||||
RootCACert: "",
|
|
||||||
Groups: []*GroupToOrgRole{
|
|
||||||
{GroupDN: "*", OrgRole: "Admin"},
|
|
||||||
},
|
|
||||||
Attr: AttributeMap{
|
|
||||||
Username: "username",
|
|
||||||
Surname: "surname",
|
|
||||||
Email: "email",
|
|
||||||
Name: "name",
|
|
||||||
MemberOf: "memberof",
|
|
||||||
},
|
|
||||||
SearchBaseDNs: []string{"BaseDNHere"},
|
|
||||||
},
|
|
||||||
Connection: mockConnection,
|
|
||||||
log: log.New("test-logger"),
|
|
||||||
}
|
|
||||||
|
|
||||||
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}}
|
|
||||||
mockConnection.setSearchResult(&result)
|
|
||||||
|
|
||||||
query := &models.LoginUserQuery{
|
|
||||||
Username: "roelgerrits",
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.userQueryReturns(&models.User{
|
|
||||||
Id: 1,
|
|
||||||
Email: "roel@test.net",
|
|
||||||
Name: "Roel Gerrits",
|
|
||||||
Login: "roelgerrits",
|
|
||||||
})
|
|
||||||
sc.userOrgsQueryReturns([]*models.UserOrgDTO{})
|
|
||||||
|
|
||||||
// act
|
|
||||||
extUser, _ := server.Login(query)
|
|
||||||
userInfo, err := user.Upsert(&user.UpsertArgs{
|
|
||||||
SignupAllowed: true,
|
|
||||||
ExternalUser: extUser,
|
|
||||||
})
|
|
||||||
|
|
||||||
// assert
|
|
||||||
|
|
||||||
// Check absence of the error
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
// User should be searched in ldap
|
|
||||||
So(mockConnection.SearchCalled, ShouldBeTrue)
|
|
||||||
|
|
||||||
// Info should be updated (email differs)
|
|
||||||
So(userInfo.Email, ShouldEqual, "roel@test.com")
|
|
||||||
|
|
||||||
// User should have admin privileges
|
|
||||||
So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
|
|
||||||
})
|
|
||||||
|
|
||||||
serverScenario("When login with invalid credentials", func(scenario *scenarioContext) {
|
|
||||||
connection := &MockConnection{}
|
connection := &MockConnection{}
|
||||||
entry := ldap.Entry{}
|
entry := ldap.Entry{}
|
||||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||||
@ -95,25 +31,60 @@ func TestLDAPLogin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
server := &Server{
|
server := &Server{
|
||||||
Config: &ServerConfig{
|
Config: &ServerConfig{
|
||||||
Attr: AttributeMap{
|
|
||||||
Username: "username",
|
|
||||||
Name: "name",
|
|
||||||
MemberOf: "memberof",
|
|
||||||
},
|
|
||||||
SearchBaseDNs: []string{"BaseDNHere"},
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
},
|
},
|
||||||
Connection: connection,
|
Connection: connection,
|
||||||
log: log.New("test-logger"),
|
log: log.New("test-logger"),
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := server.Login(scenario.loginUserQuery)
|
_, err := server.Login(defaultLogin)
|
||||||
|
|
||||||
Convey("it should return invalid credentials error", func() {
|
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
serverScenario("When login with valid credentials", func(scenario *scenarioContext) {
|
Convey("Returns an error when search hasn't find anything", func() {
|
||||||
|
connection := &MockConnection{}
|
||||||
|
result := ldap.SearchResult{Entries: []*ldap.Entry{}}
|
||||||
|
connection.setSearchResult(&result)
|
||||||
|
|
||||||
|
connection.bindProvider = func(username, password string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
|
},
|
||||||
|
Connection: connection,
|
||||||
|
log: log.New("test-logger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := server.Login(defaultLogin)
|
||||||
|
|
||||||
|
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When search returns an error", func() {
|
||||||
|
connection := &MockConnection{}
|
||||||
|
expected := errors.New("Killa-gorilla")
|
||||||
|
connection.setSearchError(expected)
|
||||||
|
|
||||||
|
connection.bindProvider = func(username, password string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
|
},
|
||||||
|
Connection: connection,
|
||||||
|
log: log.New("test-logger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := server.Login(defaultLogin)
|
||||||
|
|
||||||
|
So(err, ShouldEqual, expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When login with valid credentials", func() {
|
||||||
connection := &MockConnection{}
|
connection := &MockConnection{}
|
||||||
entry := ldap.Entry{
|
entry := ldap.Entry{
|
||||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||||
@ -143,107 +114,10 @@ func TestLDAPLogin(t *testing.T) {
|
|||||||
log: log.New("test-logger"),
|
log: log.New("test-logger"),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := server.Login(scenario.loginUserQuery)
|
resp, err := server.Login(defaultLogin)
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(resp.Login, ShouldEqual, "markelog")
|
So(resp.Login, ShouldEqual, "markelog")
|
||||||
})
|
})
|
||||||
|
|
||||||
serverScenario("When user not found in LDAP, but exist in Grafana", func(scenario *scenarioContext) {
|
|
||||||
connection := &MockConnection{}
|
|
||||||
result := ldap.SearchResult{Entries: []*ldap.Entry{}}
|
|
||||||
connection.setSearchResult(&result)
|
|
||||||
|
|
||||||
externalUser := &models.ExternalUserInfo{UserId: 42, IsDisabled: false}
|
|
||||||
scenario.getExternalUserInfoByLoginQueryReturns(externalUser)
|
|
||||||
|
|
||||||
connection.bindProvider = func(username, password string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
server := &Server{
|
|
||||||
Config: &ServerConfig{
|
|
||||||
SearchBaseDNs: []string{"BaseDNHere"},
|
|
||||||
},
|
|
||||||
Connection: connection,
|
|
||||||
log: log.New("test-logger"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := server.Login(scenario.loginUserQuery)
|
|
||||||
|
|
||||||
Convey("it should disable user", func() {
|
|
||||||
So(scenario.disableExternalUserCalled, ShouldBeTrue)
|
|
||||||
So(scenario.disableUserCmd.IsDisabled, ShouldBeTrue)
|
|
||||||
So(scenario.disableUserCmd.UserId, ShouldEqual, 42)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("it should return invalid credentials error", func() {
|
|
||||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
serverScenario("When user not found in LDAP, and disabled in Grafana already", func(scenario *scenarioContext) {
|
|
||||||
connection := &MockConnection{}
|
|
||||||
result := ldap.SearchResult{Entries: []*ldap.Entry{}}
|
|
||||||
connection.setSearchResult(&result)
|
|
||||||
|
|
||||||
externalUser := &models.ExternalUserInfo{UserId: 42, IsDisabled: true}
|
|
||||||
scenario.getExternalUserInfoByLoginQueryReturns(externalUser)
|
|
||||||
|
|
||||||
connection.bindProvider = func(username, password string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
server := &Server{
|
|
||||||
Config: &ServerConfig{
|
|
||||||
SearchBaseDNs: []string{"BaseDNHere"},
|
|
||||||
},
|
|
||||||
Connection: connection,
|
|
||||||
log: log.New("test-logger"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := server.Login(scenario.loginUserQuery)
|
|
||||||
|
|
||||||
Convey("it should't call disable function", func() {
|
|
||||||
So(scenario.disableExternalUserCalled, ShouldBeFalse)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("it should return invalid credentials error", func() {
|
|
||||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
serverScenario("When user found in LDAP, and disabled in Grafana", func(scenario *scenarioContext) {
|
|
||||||
connection := &MockConnection{}
|
|
||||||
entry := ldap.Entry{}
|
|
||||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
|
||||||
connection.setSearchResult(&result)
|
|
||||||
scenario.userQueryReturns(&models.User{Id: 42, IsDisabled: true})
|
|
||||||
|
|
||||||
connection.bindProvider = func(username, password string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
server := &Server{
|
|
||||||
Config: &ServerConfig{
|
|
||||||
SearchBaseDNs: []string{"BaseDNHere"},
|
|
||||||
},
|
|
||||||
Connection: connection,
|
|
||||||
log: log.New("test-logger"),
|
|
||||||
}
|
|
||||||
|
|
||||||
extUser, _ := server.Login(scenario.loginUserQuery)
|
|
||||||
_, err := user.Upsert(&user.UpsertArgs{
|
|
||||||
SignupAllowed: true,
|
|
||||||
ExternalUser: extUser,
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("it should re-enable user", func() {
|
|
||||||
So(scenario.disableExternalUserCalled, ShouldBeTrue)
|
|
||||||
So(scenario.disableUserCmd.IsDisabled, ShouldBeFalse)
|
|
||||||
So(scenario.disableUserCmd.UserId, ShouldEqual, 42)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("it should not return error", func() {
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
146
pkg/services/ldap/ldap_private_test.go
Normal file
146
pkg/services/ldap/ldap_private_test.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
"gopkg.in/ldap.v3"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLDAPPrivateMethods(t *testing.T) {
|
||||||
|
Convey("serializeUsers()", t, func() {
|
||||||
|
Convey("simple case", func() {
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
Attr: AttributeMap{
|
||||||
|
Username: "username",
|
||||||
|
Name: "name",
|
||||||
|
MemberOf: "memberof",
|
||||||
|
Email: "email",
|
||||||
|
},
|
||||||
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
|
},
|
||||||
|
Connection: &MockConnection{},
|
||||||
|
log: log.New("test-logger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
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"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||||
|
|
||||||
|
result, err := server.serializeUsers(users)
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(result[0].Login, ShouldEqual, "roelgerrits")
|
||||||
|
So(result[0].Email, ShouldEqual, "roel@test.com")
|
||||||
|
So(result[0].Groups, ShouldContain, "admins")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("without lastname", func() {
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
Attr: AttributeMap{
|
||||||
|
Username: "username",
|
||||||
|
Name: "name",
|
||||||
|
MemberOf: "memberof",
|
||||||
|
Email: "email",
|
||||||
|
},
|
||||||
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
|
},
|
||||||
|
Connection: &MockConnection{},
|
||||||
|
log: log.New("test-logger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := ldap.Entry{
|
||||||
|
DN: "dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{Name: "username", Values: []string{"roelgerrits"}},
|
||||||
|
{Name: "email", Values: []string{"roel@test.com"}},
|
||||||
|
{Name: "name", Values: []string{"Roel"}},
|
||||||
|
{Name: "memberof", Values: []string{"admins"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||||
|
|
||||||
|
result, err := server.serializeUsers(users)
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(result[0].Name, ShouldEqual, "Roel")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("validateGrafanaUser()", t, func() {
|
||||||
|
Convey("Returns error when user does not belong in any of the specified LDAP groups", func() {
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
Groups: []*GroupToOrgRole{
|
||||||
|
{
|
||||||
|
OrgID: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
log: logger.New("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &models.ExternalUserInfo{
|
||||||
|
Login: "markelog",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := server.validateGrafanaUser(user)
|
||||||
|
|
||||||
|
So(result, ShouldEqual, ErrInvalidCredentials)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Does not return error when group config is empty", func() {
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
Groups: []*GroupToOrgRole{},
|
||||||
|
},
|
||||||
|
log: logger.New("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &models.ExternalUserInfo{
|
||||||
|
Login: "markelog",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := server.validateGrafanaUser(user)
|
||||||
|
|
||||||
|
So(result, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Does not return error when groups are there", func() {
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
Groups: []*GroupToOrgRole{
|
||||||
|
{
|
||||||
|
OrgID: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
log: logger.New("test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &models.ExternalUserInfo{
|
||||||
|
Login: "markelog",
|
||||||
|
OrgRoles: map[int64]models.RoleType{
|
||||||
|
1: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := server.validateGrafanaUser(user)
|
||||||
|
|
||||||
|
So(result, ShouldBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,17 +1,29 @@
|
|||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
ldap "gopkg.in/ldap.v3"
|
"gopkg.in/ldap.v3"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPublicAPI(t *testing.T) {
|
func TestPublicAPI(t *testing.T) {
|
||||||
|
Convey("New()", t, func() {
|
||||||
|
Convey("Should return ", func() {
|
||||||
|
result := New(&ServerConfig{
|
||||||
|
Attr: AttributeMap{},
|
||||||
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
|
})
|
||||||
|
|
||||||
|
So(result, ShouldImplement, (*IServer)(nil))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Users()", t, func() {
|
Convey("Users()", t, func() {
|
||||||
Convey("find one user", func() {
|
Convey("Finds one user", func() {
|
||||||
MockConnection := &MockConnection{}
|
MockConnection := &MockConnection{}
|
||||||
entry := ldap.Entry{
|
entry := ldap.Entry{
|
||||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||||
@ -49,10 +61,49 @@ func TestPublicAPI(t *testing.T) {
|
|||||||
// No empty attributes should be added to the search request
|
// No empty attributes should be added to the search request
|
||||||
So(len(MockConnection.SearchAttributes), ShouldEqual, 3)
|
So(len(MockConnection.SearchAttributes), ShouldEqual, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Handles a error", func() {
|
||||||
|
expected := errors.New("Killa-gorilla")
|
||||||
|
MockConnection := &MockConnection{}
|
||||||
|
MockConnection.setSearchError(expected)
|
||||||
|
|
||||||
|
// Set up attribute map without surname and email
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
|
},
|
||||||
|
Connection: MockConnection,
|
||||||
|
log: log.New("test-logger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := server.Users([]string{"roelgerrits"})
|
||||||
|
|
||||||
|
So(err, ShouldEqual, expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should return empty slice if none were found", func() {
|
||||||
|
MockConnection := &MockConnection{}
|
||||||
|
result := ldap.SearchResult{Entries: []*ldap.Entry{}}
|
||||||
|
MockConnection.setSearchResult(&result)
|
||||||
|
|
||||||
|
// Set up attribute map without surname and email
|
||||||
|
server := &Server{
|
||||||
|
Config: &ServerConfig{
|
||||||
|
SearchBaseDNs: []string{"BaseDNHere"},
|
||||||
|
},
|
||||||
|
Connection: MockConnection,
|
||||||
|
log: log.New("test-logger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResult, err := server.Users([]string{"roelgerrits"})
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(searchResult, ShouldBeEmpty)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("InitialBind", t, func() {
|
Convey("Auth()", t, func() {
|
||||||
Convey("Given bind dn and password configured", func() {
|
Convey("Should ignore passsed username and password", func() {
|
||||||
connection := &MockConnection{}
|
connection := &MockConnection{}
|
||||||
var actualUsername, actualPassword string
|
var actualUsername, actualPassword string
|
||||||
connection.bindProvider = func(username, password string) error {
|
connection.bindProvider = func(username, password string) error {
|
||||||
@ -63,14 +114,13 @@ func TestPublicAPI(t *testing.T) {
|
|||||||
server := &Server{
|
server := &Server{
|
||||||
Connection: connection,
|
Connection: connection,
|
||||||
Config: &ServerConfig{
|
Config: &ServerConfig{
|
||||||
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
BindDN: "cn=admin,dc=grafana,dc=org",
|
||||||
BindPassword: "bindpwd",
|
BindPassword: "bindpwd",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := server.InitialBind("user", "pwd")
|
err := server.Auth("user", "pwd")
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(server.requireSecondBind, ShouldBeTrue)
|
So(actualUsername, ShouldEqual, "cn=admin,dc=grafana,dc=org")
|
||||||
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
|
|
||||||
So(actualPassword, ShouldEqual, "bindpwd")
|
So(actualPassword, ShouldEqual, "bindpwd")
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -88,31 +138,29 @@ func TestPublicAPI(t *testing.T) {
|
|||||||
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := server.InitialBind("user", "pwd")
|
err := server.Auth("user", "pwd")
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(server.requireSecondBind, ShouldBeFalse)
|
|
||||||
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
|
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
|
||||||
So(actualPassword, ShouldEqual, "pwd")
|
So(actualPassword, ShouldEqual, "pwd")
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Given empty bind dn and password", func() {
|
Convey("Should handle an error", func() {
|
||||||
connection := &MockConnection{}
|
connection := &MockConnection{}
|
||||||
unauthenticatedBindWasCalled := false
|
expected := &ldap.Error{
|
||||||
var actualUsername string
|
ResultCode: uint16(25),
|
||||||
connection.unauthenticatedBindProvider = func(username string) error {
|
}
|
||||||
unauthenticatedBindWasCalled = true
|
connection.bindProvider = func(username, password string) error {
|
||||||
actualUsername = username
|
return expected
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
server := &Server{
|
server := &Server{
|
||||||
Connection: connection,
|
Connection: connection,
|
||||||
Config: &ServerConfig{},
|
Config: &ServerConfig{
|
||||||
|
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
||||||
|
},
|
||||||
|
log: log.New("test-logger"),
|
||||||
}
|
}
|
||||||
err := server.InitialBind("user", "pwd")
|
err := server.Auth("user", "pwd")
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldEqual, expected)
|
||||||
So(server.requireSecondBind, ShouldBeTrue)
|
|
||||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
|
||||||
So(actualUsername, ShouldBeEmpty)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ type ServerConfig struct {
|
|||||||
Groups []*GroupToOrgRole `toml:"group_mappings"`
|
Groups []*GroupToOrgRole `toml:"group_mappings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttributeMap is a struct representation for LDAP "attributes" setting
|
||||||
type AttributeMap struct {
|
type AttributeMap struct {
|
||||||
Username string `toml:"username"`
|
Username string `toml:"username"`
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
@ -50,14 +51,19 @@ type AttributeMap struct {
|
|||||||
MemberOf string `toml:"member_of"`
|
MemberOf string `toml:"member_of"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GroupToOrgRole is a struct representation of LDAP
|
||||||
|
// config "group_mappings" setting
|
||||||
type GroupToOrgRole struct {
|
type GroupToOrgRole struct {
|
||||||
GroupDN string `toml:"group_dn"`
|
GroupDN string `toml:"group_dn"`
|
||||||
OrgId int64 `toml:"org_id"`
|
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"`
|
// This pointer specifies if setting was set (for backwards compatibility)
|
||||||
|
IsGrafanaAdmin *bool `toml:"grafana_admin"`
|
||||||
|
|
||||||
|
OrgRole m.RoleType `toml:"org_role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config *Config
|
// logger for all LDAP stuff
|
||||||
var logger = log.New("ldap")
|
var logger = log.New("ldap")
|
||||||
|
|
||||||
// loadingMutex locks the reading of the config so multiple requests for reloading are sequential.
|
// loadingMutex locks the reading of the config so multiple requests for reloading are sequential.
|
||||||
@ -82,6 +88,10 @@ func ReloadConfig() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We need to define in this space so `GetConfig` fn
|
||||||
|
// could be defined as singleton
|
||||||
|
var config *Config
|
||||||
|
|
||||||
// GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of
|
// GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of
|
||||||
// the config or it reads it and caches it first.
|
// the config or it reads it and caches it first.
|
||||||
func GetConfig() (*Config, error) {
|
func GetConfig() (*Config, error) {
|
||||||
@ -129,8 +139,8 @@ func readConfig(configFile string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, groupMap := range server.Groups {
|
for _, groupMap := range server.Groups {
|
||||||
if groupMap.OrgId == 0 {
|
if groupMap.OrgID == 0 {
|
||||||
groupMap.OrgId = 1
|
groupMap.OrgID = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,214 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockConnection struct for testing
|
|
||||||
type MockConnection struct {
|
|
||||||
SearchResult *ldap.SearchResult
|
|
||||||
SearchCalled bool
|
|
||||||
SearchAttributes []string
|
|
||||||
|
|
||||||
AddParams *ldap.AddRequest
|
|
||||||
AddCalled bool
|
|
||||||
|
|
||||||
DelParams *ldap.DelRequest
|
|
||||||
DelCalled bool
|
|
||||||
|
|
||||||
bindProvider func(username, password string) error
|
|
||||||
unauthenticatedBindProvider func(username string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind mocks Bind connection function
|
|
||||||
func (c *MockConnection) Bind(username, password string) error {
|
|
||||||
if c.bindProvider != nil {
|
|
||||||
return c.bindProvider(username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnauthenticatedBind mocks UnauthenticatedBind connection function
|
|
||||||
func (c *MockConnection) UnauthenticatedBind(username string) error {
|
|
||||||
if c.unauthenticatedBindProvider != nil {
|
|
||||||
return c.unauthenticatedBindProvider(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close mocks Close connection function
|
|
||||||
func (c *MockConnection) Close() {}
|
|
||||||
|
|
||||||
func (c *MockConnection) setSearchResult(result *ldap.SearchResult) {
|
|
||||||
c.SearchResult = result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search mocks Search connection function
|
|
||||||
func (c *MockConnection) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
|
||||||
c.SearchCalled = true
|
|
||||||
c.SearchAttributes = sr.Attributes
|
|
||||||
return c.SearchResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add mocks Add connection function
|
|
||||||
func (c *MockConnection) Add(request *ldap.AddRequest) error {
|
|
||||||
c.AddCalled = true
|
|
||||||
c.AddParams = request
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Del mocks Del connection function
|
|
||||||
func (c *MockConnection) Del(request *ldap.DelRequest) error {
|
|
||||||
c.DelCalled = true
|
|
||||||
c.DelParams = request
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartTLS mocks StartTLS connection function
|
|
||||||
func (c *MockConnection) StartTLS(*tls.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func serverScenario(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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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.GetExternalUserInfoByLoginQuery) error {
|
|
||||||
sc.getExternalUserInfoByLoginQuery = cmd
|
|
||||||
sc.getExternalUserInfoByLoginQuery.Result = &models.ExternalUserInfo{UserId: 42, IsDisabled: false}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
bus.AddHandler("test", func(cmd *models.DisableUserCommand) error {
|
|
||||||
sc.disableExternalUserCalled = true
|
|
||||||
sc.disableUserCmd = cmd
|
|
||||||
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
|
|
||||||
getExternalUserInfoByLoginQuery *models.GetExternalUserInfoByLoginQuery
|
|
||||||
getUserOrgListQuery *models.GetUserOrgListQuery
|
|
||||||
createUserCmd *models.CreateUserCommand
|
|
||||||
disableUserCmd *models.DisableUserCommand
|
|
||||||
addOrgUserCmd *models.AddOrgUserCommand
|
|
||||||
updateOrgUserCmd *models.UpdateOrgUserCommand
|
|
||||||
removeOrgUserCmd *models.RemoveOrgUserCommand
|
|
||||||
updateUserCmd *models.UpdateUserCommand
|
|
||||||
setUsingOrgCmd *models.SetUsingOrgCommand
|
|
||||||
updateUserPermissionsCmd *models.UpdateUserPermissionsCommand
|
|
||||||
disableExternalUserCalled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *scenarioContext) getExternalUserInfoByLoginQueryReturns(externalUser *models.ExternalUserInfo) {
|
|
||||||
bus.AddHandler("test", func(cmd *models.GetExternalUserInfoByLoginQuery) error {
|
|
||||||
sc.getExternalUserInfoByLoginQuery = cmd
|
|
||||||
sc.getExternalUserInfoByLoginQuery.Result = &models.ExternalUserInfo{
|
|
||||||
UserId: externalUser.UserId,
|
|
||||||
IsDisabled: externalUser.IsDisabled,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type scenarioFunc func(c *scenarioContext)
|
|
84
pkg/services/ldap/test_test.go
Normal file
84
pkg/services/ldap/test_test.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"gopkg.in/ldap.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockConnection struct for testing
|
||||||
|
type MockConnection struct {
|
||||||
|
SearchResult *ldap.SearchResult
|
||||||
|
SearchError error
|
||||||
|
SearchCalled bool
|
||||||
|
SearchAttributes []string
|
||||||
|
|
||||||
|
AddParams *ldap.AddRequest
|
||||||
|
AddCalled bool
|
||||||
|
|
||||||
|
DelParams *ldap.DelRequest
|
||||||
|
DelCalled bool
|
||||||
|
|
||||||
|
bindProvider func(username, password string) error
|
||||||
|
unauthenticatedBindProvider func(username string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind mocks Bind connection function
|
||||||
|
func (c *MockConnection) Bind(username, password string) error {
|
||||||
|
if c.bindProvider != nil {
|
||||||
|
return c.bindProvider(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnauthenticatedBind mocks UnauthenticatedBind connection function
|
||||||
|
func (c *MockConnection) UnauthenticatedBind(username string) error {
|
||||||
|
if c.unauthenticatedBindProvider != nil {
|
||||||
|
return c.unauthenticatedBindProvider(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close mocks Close connection function
|
||||||
|
func (c *MockConnection) Close() {}
|
||||||
|
|
||||||
|
func (c *MockConnection) setSearchResult(result *ldap.SearchResult) {
|
||||||
|
c.SearchResult = result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockConnection) setSearchError(err error) {
|
||||||
|
c.SearchError = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search mocks Search connection function
|
||||||
|
func (c *MockConnection) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||||
|
c.SearchCalled = true
|
||||||
|
c.SearchAttributes = sr.Attributes
|
||||||
|
|
||||||
|
if c.SearchError != nil {
|
||||||
|
return nil, c.SearchError
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SearchResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add mocks Add connection function
|
||||||
|
func (c *MockConnection) Add(request *ldap.AddRequest) error {
|
||||||
|
c.AddCalled = true
|
||||||
|
c.AddParams = request
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del mocks Del connection function
|
||||||
|
func (c *MockConnection) Del(request *ldap.DelRequest) error {
|
||||||
|
c.DelCalled = true
|
||||||
|
c.DelParams = request
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTLS mocks StartTLS connection function
|
||||||
|
func (c *MockConnection) StartTLS(*tls.Config) error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -35,7 +35,7 @@ func (mock *mockLDAP) Users([]string) ([]*models.ExternalUserInfo, error) {
|
|||||||
|
|
||||||
return mock.usersRestReturn, mock.usersErrReturn
|
return mock.usersRestReturn, mock.usersErrReturn
|
||||||
}
|
}
|
||||||
func (mock *mockLDAP) InitialBind(string, string) error {
|
func (mock *mockLDAP) Auth(string, string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (mock *mockLDAP) Dial() error {
|
func (mock *mockLDAP) Dial() error {
|
@ -1,39 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UpsertArgs are object for Upsert method
|
|
||||||
type UpsertArgs struct {
|
|
||||||
ReqContext *models.ReqContext
|
|
||||||
ExternalUser *models.ExternalUserInfo
|
|
||||||
SignupAllowed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upsert add/update grafana user
|
|
||||||
func Upsert(args *UpsertArgs) (*models.User, error) {
|
|
||||||
query := &models.UpsertUserCommand{
|
|
||||||
ReqContext: args.ReqContext,
|
|
||||||
ExternalUser: args.ExternalUser,
|
|
||||||
SignupAllowed: args.SignupAllowed,
|
|
||||||
}
|
|
||||||
err := bus.Dispatch(query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.Result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the users
|
|
||||||
func Get(
|
|
||||||
query *models.SearchUsersQuery,
|
|
||||||
) ([]*models.UserSearchHitDTO, error) {
|
|
||||||
if err := bus.Dispatch(query); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.Result.Users, nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user