mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 06:22:13 +08:00
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:
@ -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),
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user