Users: Disable users removed from LDAP (#16820)

* Users: add is_disabled column

* Users: disable users removed from LDAP

* Auth: return ErrInvalidCredentials for failed LDAP auth

* User: return isDisabled flag in user search api

* User: mark disabled users at the server admin page

* Chore: refactor according to review

* Auth: prevent disabled user from login

* Auth: re-enable user when it found in ldap

* User: add api endpoint for disabling user

* User: use separate endpoints to disable/enable user

* User: disallow disabling external users

* User: able do disable users from admin UI

* Chore: refactor based on review

* Chore: use more clear error check when disabling user

* Fix login tests

* Tests for disabling user during the LDAP login

* Tests for disable user API

* Tests for login with disabled user

* Remove disable user UI stub

* Sync with latest LDAP refactoring
This commit is contained in:
Alexander Zobnin
2019-05-21 14:52:49 +03:00
committed by GitHub
parent 8d1909c56d
commit 2d03815770
17 changed files with 428 additions and 72 deletions

View File

@ -10,6 +10,7 @@ import (
"gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
@ -48,6 +49,7 @@ var (
// ErrInvalidCredentials is returned if username and password do not match
ErrInvalidCredentials = errors.New("Invalid Username or Password")
ErrLDAPUserNotFound = errors.New("LDAP user not found")
)
var dial = func(network, addr string) (IConnection, error) {
@ -142,6 +144,7 @@ func (server *Server) Login(query *models.LoginUserQuery) (
// If we couldn't find the user -
// we should show incorrect credentials err
if len(users) == 0 {
server.disableExternalUser(query.Username)
return nil, ErrInvalidCredentials
}
@ -263,6 +266,34 @@ func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error {
return nil
}
// disableExternalUser marks external user as disabled in Grafana db
func (server *Server) disableExternalUser(username string) error {
// Check if external user exist in Grafana
userQuery := &models.GetExternalUserInfoByLoginQuery{
LoginOrEmail: username,
}
if err := bus.Dispatch(userQuery); err != nil {
return err
}
userInfo := userQuery.Result
if !userInfo.IsDisabled {
server.log.Debug("Disabling external user", "user", userQuery.Result.Login)
// Mark user as disabled in grafana db
disableUserCmd := &models.DisableUserCommand{
UserId: userQuery.Result.UserId,
IsDisabled: true,
}
if err := bus.Dispatch(disableUserCmd); err != nil {
server.log.Debug("Error disabling external user", "user", userQuery.Result.Login, "message", err.Error())
return err
}
}
return nil
}
// getSearchRequest returns LDAP search request for users
func (server *Server) getSearchRequest(
base string,
@ -305,7 +336,7 @@ func (server *Server) getSearchRequest(
// buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo
func (server *Server) buildGrafanaUser(user *UserInfo) *models.ExternalUserInfo {
extUser := &models.ExternalUserInfo{
AuthModule: "ldap",
AuthModule: models.AuthModuleLDAP,
AuthId: user.DN,
Name: strings.TrimSpace(
fmt.Sprintf("%s %s", user.FirstName, user.LastName),

View File

@ -148,5 +148,102 @@ func TestLDAPLogin(t *testing.T) {
So(err, ShouldBeNil)
So(resp.Login, ShouldEqual, "markelog")
})
authScenario("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
}
auth := &Server{
config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
log: log.New("test-logger"),
}
_, err := auth.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)
})
})
authScenario("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
}
auth := &Server{
config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
log: log.New("test-logger"),
}
_, err := auth.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)
})
})
authScenario("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
}
auth := &Server{
config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
log: log.New("test-logger"),
}
extUser, _ := auth.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)
})
})
})
}

View File

@ -115,6 +115,18 @@ func authScenario(desc string, fn scenarioFunc) {
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
@ -145,16 +157,19 @@ func authScenario(desc string, fn scenarioFunc) {
}
type scenarioContext struct {
loginUserQuery *models.LoginUserQuery
getUserByAuthInfoQuery *models.GetUserByAuthInfoQuery
getUserOrgListQuery *models.GetUserOrgListQuery
createUserCmd *models.CreateUserCommand
addOrgUserCmd *models.AddOrgUserCommand
updateOrgUserCmd *models.UpdateOrgUserCommand
removeOrgUserCmd *models.RemoveOrgUserCommand
updateUserCmd *models.UpdateUserCommand
setUsingOrgCmd *models.SetUsingOrgCommand
updateUserPermissionsCmd *models.UpdateUserPermissionsCommand
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) {
@ -177,4 +192,15 @@ func (sc *scenarioContext) userOrgsQueryReturns(orgs []*models.UserOrgDTO) {
})
}
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)