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/services/datasources"
|
||||
"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/rendering"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -66,6 +67,7 @@ type HTTPServer struct {
|
||||
QuotaService *quota.QuotaService `inject:""`
|
||||
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
||||
ProvisioningService ProvisioningService `inject:""`
|
||||
Login *login.LoginService `inject:""`
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Init() error {
|
||||
|
@ -1,9 +1,9 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"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/util/errutil"
|
||||
)
|
||||
@ -36,15 +36,15 @@ var loginUsingLDAP = func(query *models.LoginUserQuery) (bool, error) {
|
||||
return true, err
|
||||
}
|
||||
|
||||
login, err := user.Upsert(&user.UpsertArgs{
|
||||
upsert := &models.UpsertUserCommand{
|
||||
ExternalUser: externalUser,
|
||||
SignupAllowed: setting.LDAPAllowSignup,
|
||||
})
|
||||
}
|
||||
err = bus.Dispatch(upsert)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
query.User = login
|
||||
query.User = upsert.Result
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"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
|
||||
user, err := user.Upsert(&user.UpsertArgs{
|
||||
upsert := &models.UpsertUserCommand{
|
||||
ReqContext: auth.ctx,
|
||||
SignupAllowed: auth.LDAPAllowSignup,
|
||||
ExternalUser: extUser,
|
||||
})
|
||||
}
|
||||
err = bus.Dispatch(upsert)
|
||||
if err != nil {
|
||||
return 0, newError(err.Error(), nil)
|
||||
}
|
||||
|
||||
return user.Id, nil
|
||||
return upsert.Result.Id, nil
|
||||
}
|
||||
|
||||
// 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,
|
||||
SignupAllowed: true,
|
||||
ExternalUser: extUser,
|
||||
})
|
||||
}
|
||||
err := bus.Dispatch(upsert)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result.Id, nil
|
||||
return upsert.Result.Id, nil
|
||||
}
|
||||
|
||||
// GetSignedUser get full signed user info
|
||||
|
@ -26,7 +26,10 @@ var (
|
||||
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) {
|
||||
ctx := &m.ReqContext{
|
||||
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"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
@ -30,17 +31,16 @@ type IConnection interface {
|
||||
type IServer interface {
|
||||
Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error)
|
||||
Users([]string) ([]*models.ExternalUserInfo, error)
|
||||
InitialBind(string, string) error
|
||||
Auth(string, string) error
|
||||
Dial() error
|
||||
Close()
|
||||
}
|
||||
|
||||
// Server is basic struct of LDAP authorization
|
||||
type Server struct {
|
||||
Config *ServerConfig
|
||||
Connection IConnection
|
||||
requireSecondBind bool
|
||||
log log.Logger
|
||||
Config *ServerConfig
|
||||
Connection IConnection
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
@ -49,10 +49,6 @@ var (
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
)
|
||||
|
||||
var dial = func(network, addr string) (IConnection, error) {
|
||||
return ldap.Dial(network, addr)
|
||||
}
|
||||
|
||||
// New creates the new LDAP auth
|
||||
func New(config *ServerConfig) IServer {
|
||||
return &Server{
|
||||
@ -96,7 +92,7 @@ func (server *Server) Dial() error {
|
||||
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
|
||||
}
|
||||
if server.Config.StartTLS {
|
||||
server.Connection, err = dial("tcp", address)
|
||||
server.Connection, err = ldap.Dial("tcp", address)
|
||||
if err == nil {
|
||||
if err = server.Connection.StartTLS(tlsCfg); err == nil {
|
||||
return nil
|
||||
@ -106,7 +102,7 @@ func (server *Server) Dial() error {
|
||||
server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg)
|
||||
}
|
||||
} else {
|
||||
server.Connection, err = dial("tcp", address)
|
||||
server.Connection, err = ldap.Dial("tcp", address)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
@ -125,9 +121,8 @@ func (server *Server) Close() {
|
||||
func (server *Server) Login(query *models.LoginUserQuery) (
|
||||
*models.ExternalUserInfo, error,
|
||||
) {
|
||||
|
||||
// Perform initial authentication
|
||||
err := server.InitialBind(query.Username, query.Password)
|
||||
// Authentication
|
||||
err := server.Auth(query.Username, query.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -145,20 +140,11 @@ func (server *Server) Login(query *models.LoginUserQuery) (
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Check if a second user bind is needed
|
||||
user := users[0]
|
||||
|
||||
if err := server.validateGrafanaUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if server.requireSecondBind {
|
||||
err = server.secondBind(user, query.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@ -168,8 +154,8 @@ func (server *Server) Users(logins []string) (
|
||||
error,
|
||||
) {
|
||||
var result *ldap.SearchResult
|
||||
var err error
|
||||
var Config = server.Config
|
||||
var err error
|
||||
|
||||
for _, base := range Config.SearchBaseDNs {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server.log.Debug("LDAP users found", "users", spew.Sdump(serializedUsers))
|
||||
|
||||
return serializedUsers, nil
|
||||
}
|
||||
|
||||
@ -276,108 +268,71 @@ func (server *Server) getSearchRequest(
|
||||
}
|
||||
|
||||
// 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{
|
||||
AuthModule: models.AuthModuleLDAP,
|
||||
AuthId: user.DN,
|
||||
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,
|
||||
Email: user.Email,
|
||||
Groups: user.MemberOf,
|
||||
Login: getAttribute(attrs.Username, user),
|
||||
Email: getAttribute(attrs.Email, user),
|
||||
Groups: memberOf,
|
||||
OrgRoles: map[int64]models.RoleType{},
|
||||
}
|
||||
|
||||
for _, group := range server.Config.Groups {
|
||||
// only use the first match for each org
|
||||
if extUser.OrgRoles[group.OrgId] != "" {
|
||||
if extUser.OrgRoles[group.OrgID] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if user.isMemberOf(group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
if isMemberOf(memberOf, group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgID] = group.OrgRole
|
||||
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extUser
|
||||
return extUser, nil
|
||||
}
|
||||
|
||||
func (server *Server) serverBind() error {
|
||||
bindFn := func() error {
|
||||
return server.Connection.Bind(
|
||||
server.Config.BindDN,
|
||||
server.Config.BindPassword,
|
||||
)
|
||||
}
|
||||
|
||||
if server.Config.BindPassword == "" {
|
||||
bindFn = func() error {
|
||||
return server.Connection.UnauthenticatedBind(server.Config.BindDN)
|
||||
}
|
||||
}
|
||||
|
||||
// bind_dn and bind_password to bind
|
||||
if err := bindFn(); err != nil {
|
||||
server.log.Info("LDAP initial bind failed, %v", err)
|
||||
|
||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// shouldBindAdmin checks if we should use
|
||||
// admin username & password for LDAP bind
|
||||
func (server *Server) shouldBindAdmin() bool {
|
||||
return server.Config.BindPassword != ""
|
||||
}
|
||||
|
||||
func (server *Server) secondBind(
|
||||
user *models.ExternalUserInfo,
|
||||
userPassword string,
|
||||
) error {
|
||||
err := server.Connection.Bind(user.AuthId, userPassword)
|
||||
if err != nil {
|
||||
server.log.Info("Second bind failed", "error", err)
|
||||
// Auth authentificates user in LDAP.
|
||||
// It might not use passed password and username,
|
||||
// since they can be overwritten with admin config values -
|
||||
// see "bind_dn" and "bind_password" options in LDAP config
|
||||
func (server *Server) Auth(username, password string) error {
|
||||
path := server.Config.BindDN
|
||||
|
||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
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)
|
||||
if server.shouldBindAdmin() {
|
||||
password = server.Config.BindPassword
|
||||
} else {
|
||||
path = fmt.Sprintf(path, username)
|
||||
}
|
||||
|
||||
bindFn := func() error {
|
||||
return server.Connection.Bind(bindPath, userPassword)
|
||||
}
|
||||
|
||||
if userPassword == "" {
|
||||
bindFn = func() error {
|
||||
return server.Connection.UnauthenticatedBind(bindPath)
|
||||
}
|
||||
return server.Connection.Bind(path, password)
|
||||
}
|
||||
|
||||
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.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
|
||||
func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string, error) {
|
||||
func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) {
|
||||
var memberOf []string
|
||||
var config = server.Config
|
||||
|
||||
for _, groupSearchBase := range server.Config.GroupSearchBaseDNs {
|
||||
for _, groupSearchBase := range config.GroupSearchBaseDNs {
|
||||
var filterReplace string
|
||||
if server.Config.GroupSearchFilterUserAttribute == "" {
|
||||
filterReplace = getLDAPAttr(server.Config.Attr.Username, searchResult)
|
||||
if config.GroupSearchFilterUserAttribute == "" {
|
||||
filterReplace = getAttribute(config.Attr.Username, entry)
|
||||
} else {
|
||||
filterReplace = getLDAPAttr(server.Config.GroupSearchFilterUserAttribute, searchResult)
|
||||
filterReplace = getAttribute(
|
||||
config.GroupSearchFilterUserAttribute,
|
||||
entry,
|
||||
)
|
||||
}
|
||||
|
||||
filter := strings.Replace(
|
||||
server.Config.GroupSearchFilter, "%s",
|
||||
config.GroupSearchFilter, "%s",
|
||||
ldap.EscapeFilter(filterReplace),
|
||||
-1,
|
||||
)
|
||||
@ -411,7 +370,7 @@ func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string
|
||||
server.log.Info("Searching for user's groups", "filter", filter)
|
||||
|
||||
// support old way of reading settings
|
||||
groupIDAttribute := server.Config.Attr.MemberOf
|
||||
groupIDAttribute := config.Attr.MemberOf
|
||||
// but prefer dn attribute if default settings are used
|
||||
if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
|
||||
groupIDAttribute = "dn"
|
||||
@ -431,8 +390,11 @@ func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string
|
||||
}
|
||||
|
||||
if len(groupSearchResult.Entries) > 0 {
|
||||
for i := range groupSearchResult.Entries {
|
||||
memberOf = append(memberOf, getLDAPAttrN(groupIDAttribute, groupSearchResult, i))
|
||||
for _, group := range groupSearchResult.Entries {
|
||||
memberOf = append(
|
||||
memberOf,
|
||||
getAttribute(groupIDAttribute, group),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -448,104 +410,32 @@ func (server *Server) serializeUsers(
|
||||
) ([]*models.ExternalUserInfo, error) {
|
||||
var serialized []*models.ExternalUserInfo
|
||||
|
||||
for index := range users.Entries {
|
||||
memberOf, err := server.getMemberOf(users)
|
||||
for _, user := range users.Entries {
|
||||
extUser, err := server.buildGrafanaUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo := &UserInfo{
|
||||
DN: getLDAPAttrN(
|
||||
"dn",
|
||||
users,
|
||||
index,
|
||||
),
|
||||
LastName: getLDAPAttrN(
|
||||
server.Config.Attr.Surname,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
FirstName: getLDAPAttrN(
|
||||
server.Config.Attr.Name,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
Username: getLDAPAttrN(
|
||||
server.Config.Attr.Username,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
Email: getLDAPAttrN(
|
||||
server.Config.Attr.Email,
|
||||
users,
|
||||
index,
|
||||
),
|
||||
MemberOf: memberOf,
|
||||
}
|
||||
|
||||
serialized = append(
|
||||
serialized,
|
||||
server.buildGrafanaUser(userInfo),
|
||||
)
|
||||
serialized = append(serialized, extUser)
|
||||
}
|
||||
|
||||
return serialized, nil
|
||||
}
|
||||
|
||||
// getMemberOf finds memberOf property or request it
|
||||
func (server *Server) getMemberOf(search *ldap.SearchResult) (
|
||||
func (server *Server) getMemberOf(result *ldap.Entry) (
|
||||
[]string, error,
|
||||
) {
|
||||
if server.Config.GroupSearchFilter == "" {
|
||||
memberOf := getLDAPAttrArray(server.Config.Attr.MemberOf, search)
|
||||
memberOf := getArrayAttribute(server.Config.Attr.MemberOf, result)
|
||||
|
||||
return memberOf, nil
|
||||
}
|
||||
|
||||
memberOf, err := server.requestMemberOf(search)
|
||||
memberOf, err := server.requestMemberOf(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memberOf, nil
|
||||
}
|
||||
|
||||
func appendIfNotEmpty(slice []string, values ...string) []string {
|
||||
for _, v := range values {
|
||||
if v != "" {
|
||||
slice = append(slice, v)
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func getLDAPAttr(name string, result *ldap.SearchResult) string {
|
||||
return getLDAPAttrN(name, result, 0)
|
||||
}
|
||||
|
||||
func getLDAPAttrN(name string, result *ldap.SearchResult, n int) string {
|
||||
if strings.ToLower(name) == "dn" {
|
||||
return result.Entries[n].DN
|
||||
}
|
||||
for _, attr := range result.Entries[n].Attributes {
|
||||
if attr.Name == name {
|
||||
if len(attr.Values) > 0 {
|
||||
return attr.Values[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getLDAPAttrArray(name string, result *ldap.SearchResult) []string {
|
||||
return getLDAPAttrArrayN(name, result, 0)
|
||||
}
|
||||
|
||||
func getLDAPAttrArrayN(name string, result *ldap.SearchResult, n int) []string {
|
||||
for _, attr := range result.Entries[n].Attributes {
|
||||
if attr.Name == name {
|
||||
return attr.Values
|
||||
}
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
@ -5,136 +5,87 @@ import (
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
func TestLDAPHelpers(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("isMemberOf()", t, func() {
|
||||
Convey("Wildcard", func() {
|
||||
result := isMemberOf([]string{}, "*")
|
||||
So(result, ShouldBeTrue)
|
||||
})
|
||||
|
||||
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"),
|
||||
}
|
||||
Convey("Should find one", func() {
|
||||
result := isMemberOf([]string{"one", "Two", "three"}, "two")
|
||||
So(result, ShouldBeTrue)
|
||||
})
|
||||
|
||||
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("Should not find one", func() {
|
||||
result := isMemberOf([]string{"one", "Two", "three"}, "twos")
|
||||
So(result, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("serverBind()", t, func() {
|
||||
Convey("Given bind dn and password configured", func() {
|
||||
connection := &MockConnection{}
|
||||
var actualUsername, actualPassword string
|
||||
connection.bindProvider = func(username, password string) error {
|
||||
actualUsername = username
|
||||
actualPassword = password
|
||||
return nil
|
||||
}
|
||||
server := &Server{
|
||||
Connection: connection,
|
||||
Config: &ServerConfig{
|
||||
BindDN: "o=users,dc=grafana,dc=org",
|
||||
BindPassword: "bindpwd",
|
||||
Convey("getAttribute()", t, func() {
|
||||
Convey("Should get username", func() {
|
||||
value := []string{"roelgerrits"}
|
||||
entry := &ldap.Entry{
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "username", Values: value,
|
||||
},
|
||||
},
|
||||
}
|
||||
err := server.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
|
||||
So(actualPassword, ShouldEqual, "bindpwd")
|
||||
|
||||
result := getAttribute("username", entry)
|
||||
|
||||
So(result, ShouldEqual, value[0])
|
||||
})
|
||||
|
||||
Convey("Given bind dn configured", func() {
|
||||
connection := &MockConnection{}
|
||||
unauthenticatedBindWasCalled := false
|
||||
var actualUsername string
|
||||
connection.unauthenticatedBindProvider = func(username string) error {
|
||||
unauthenticatedBindWasCalled = true
|
||||
actualUsername = username
|
||||
return nil
|
||||
}
|
||||
server := &Server{
|
||||
Connection: connection,
|
||||
Config: &ServerConfig{
|
||||
BindDN: "o=users,dc=grafana,dc=org",
|
||||
Convey("Should not get anything", func() {
|
||||
value := []string{"roelgerrits"}
|
||||
entry := &ldap.Entry{
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "killa", Values: value,
|
||||
},
|
||||
},
|
||||
}
|
||||
err := server.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
|
||||
|
||||
result := getAttribute("username", entry)
|
||||
|
||||
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() {
|
||||
connection := &MockConnection{}
|
||||
unauthenticatedBindWasCalled := false
|
||||
var actualUsername string
|
||||
connection.unauthenticatedBindProvider = func(username string) error {
|
||||
unauthenticatedBindWasCalled = true
|
||||
actualUsername = username
|
||||
return nil
|
||||
Convey("Should not get anything", func() {
|
||||
value := []string{"roelgerrits"}
|
||||
entry := &ldap.Entry{
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "username", Values: value,
|
||||
},
|
||||
},
|
||||
}
|
||||
server := &Server{
|
||||
Connection: connection,
|
||||
Config: &ServerConfig{},
|
||||
}
|
||||
err := server.serverBind()
|
||||
So(err, ShouldBeNil)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldBeEmpty)
|
||||
|
||||
result := getArrayAttribute("something", entry)
|
||||
|
||||
So(result, ShouldResemble, []string{})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1,88 +1,24 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"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) {
|
||||
defaultLogin := &models.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
}
|
||||
|
||||
Convey("Login()", t, func() {
|
||||
serverScenario("When user is log in and updated", func(sc *scenarioContext) {
|
||||
// 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) {
|
||||
Convey("Should get invalid credentials when auth fails", func() {
|
||||
connection := &MockConnection{}
|
||||
entry := ldap.Entry{}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
@ -95,25 +31,60 @@ func TestLDAPLogin(t *testing.T) {
|
||||
}
|
||||
server := &Server{
|
||||
Config: &ServerConfig{
|
||||
Attr: AttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
Connection: connection,
|
||||
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{}
|
||||
entry := ldap.Entry{
|
||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||
@ -143,107 +114,10 @@ func TestLDAPLogin(t *testing.T) {
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
resp, err := server.Login(scenario.loginUserQuery)
|
||||
resp, err := server.Login(defaultLogin)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
ldap "gopkg.in/ldap.v3"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
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("find one user", func() {
|
||||
Convey("Finds one user", func() {
|
||||
MockConnection := &MockConnection{}
|
||||
entry := ldap.Entry{
|
||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||
@ -49,10 +61,49 @@ func TestPublicAPI(t *testing.T) {
|
||||
// No empty attributes should be added to the search request
|
||||
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("Given bind dn and password configured", func() {
|
||||
Convey("Auth()", t, func() {
|
||||
Convey("Should ignore passsed username and password", func() {
|
||||
connection := &MockConnection{}
|
||||
var actualUsername, actualPassword string
|
||||
connection.bindProvider = func(username, password string) error {
|
||||
@ -63,14 +114,13 @@ func TestPublicAPI(t *testing.T) {
|
||||
server := &Server{
|
||||
Connection: connection,
|
||||
Config: &ServerConfig{
|
||||
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
||||
BindDN: "cn=admin,dc=grafana,dc=org",
|
||||
BindPassword: "bindpwd",
|
||||
},
|
||||
}
|
||||
err := server.InitialBind("user", "pwd")
|
||||
err := server.Auth("user", "pwd")
|
||||
So(err, ShouldBeNil)
|
||||
So(server.requireSecondBind, ShouldBeTrue)
|
||||
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
|
||||
So(actualUsername, ShouldEqual, "cn=admin,dc=grafana,dc=org")
|
||||
So(actualPassword, ShouldEqual, "bindpwd")
|
||||
})
|
||||
|
||||
@ -88,31 +138,29 @@ func TestPublicAPI(t *testing.T) {
|
||||
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
|
||||
},
|
||||
}
|
||||
err := server.InitialBind("user", "pwd")
|
||||
err := server.Auth("user", "pwd")
|
||||
So(err, ShouldBeNil)
|
||||
So(server.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() {
|
||||
Convey("Should handle an error", func() {
|
||||
connection := &MockConnection{}
|
||||
unauthenticatedBindWasCalled := false
|
||||
var actualUsername string
|
||||
connection.unauthenticatedBindProvider = func(username string) error {
|
||||
unauthenticatedBindWasCalled = true
|
||||
actualUsername = username
|
||||
return nil
|
||||
expected := &ldap.Error{
|
||||
ResultCode: uint16(25),
|
||||
}
|
||||
connection.bindProvider = func(username, password string) error {
|
||||
return expected
|
||||
}
|
||||
server := &Server{
|
||||
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")
|
||||
So(err, ShouldBeNil)
|
||||
So(server.requireSecondBind, ShouldBeTrue)
|
||||
So(unauthenticatedBindWasCalled, ShouldBeTrue)
|
||||
So(actualUsername, ShouldBeEmpty)
|
||||
err := server.Auth("user", "pwd")
|
||||
So(err, ShouldEqual, expected)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ type ServerConfig struct {
|
||||
Groups []*GroupToOrgRole `toml:"group_mappings"`
|
||||
}
|
||||
|
||||
// AttributeMap is a struct representation for LDAP "attributes" setting
|
||||
type AttributeMap struct {
|
||||
Username string `toml:"username"`
|
||||
Name string `toml:"name"`
|
||||
@ -50,14 +51,19 @@ type AttributeMap struct {
|
||||
MemberOf string `toml:"member_of"`
|
||||
}
|
||||
|
||||
// GroupToOrgRole is a struct representation of LDAP
|
||||
// config "group_mappings" setting
|
||||
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"`
|
||||
GroupDN string `toml:"group_dn"`
|
||||
OrgID int64 `toml:"org_id"`
|
||||
|
||||
// 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")
|
||||
|
||||
// loadingMutex locks the reading of the config so multiple requests for reloading are sequential.
|
||||
@ -82,6 +88,10 @@ func ReloadConfig() error {
|
||||
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
|
||||
// the config or it reads it and caches it first.
|
||||
func GetConfig() (*Config, error) {
|
||||
@ -129,8 +139,8 @@ func readConfig(configFile string) (*Config, error) {
|
||||
}
|
||||
|
||||
for _, groupMap := range server.Groups {
|
||||
if groupMap.OrgId == 0 {
|
||||
groupMap.OrgId = 1
|
||||
if groupMap.OrgID == 0 {
|
||||
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
|
||||
}
|
||||
func (mock *mockLDAP) InitialBind(string, string) error {
|
||||
func (mock *mockLDAP) Auth(string, string) error {
|
||||
return nil
|
||||
}
|
||||
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