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
+56 -16
View File
@@ -4,12 +4,12 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) {
cmd := m.CreateUserCommand{
func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) {
cmd := models.CreateUserCommand{
Login: form.Login,
Email: form.Email,
Password: form.Password,
@@ -38,7 +38,7 @@ func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) {
user := cmd.Result
result := m.UserIdDTO{
result := models.UserIdDTO{
Message: "User created",
Id: user.Id,
}
@@ -46,7 +46,7 @@ func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) {
c.JSON(200, result)
}
func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) {
func AdminUpdateUserPassword(c *models.ReqContext, form dtos.AdminUpdateUserPasswordForm) {
userID := c.ParamsInt64(":id")
if len(form.Password) < 4 {
@@ -54,7 +54,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
return
}
userQuery := m.GetUserByIdQuery{Id: userID}
userQuery := models.GetUserByIdQuery{Id: userID}
if err := bus.Dispatch(&userQuery); err != nil {
c.JsonApiErr(500, "Could not read user from database", err)
@@ -63,7 +63,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt)
cmd := m.ChangeUserPasswordCommand{
cmd := models.ChangeUserPasswordCommand{
UserId: userID,
NewPassword: passwordHashed,
}
@@ -77,17 +77,17 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
}
// PUT /api/admin/users/:id/permissions
func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
func AdminUpdateUserPermissions(c *models.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
userID := c.ParamsInt64(":id")
cmd := m.UpdateUserPermissionsCommand{
cmd := models.UpdateUserPermissionsCommand{
UserId: userID,
IsGrafanaAdmin: form.IsGrafanaAdmin,
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastGrafanaAdmin {
c.JsonApiErr(400, m.ErrLastGrafanaAdmin.Error(), nil)
if err == models.ErrLastGrafanaAdmin {
c.JsonApiErr(400, models.ErrLastGrafanaAdmin.Error(), nil)
return
}
@@ -98,10 +98,10 @@ func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermis
c.JsonOK("User permissions updated")
}
func AdminDeleteUser(c *m.ReqContext) {
func AdminDeleteUser(c *models.ReqContext) {
userID := c.ParamsInt64(":id")
cmd := m.DeleteUserCommand{UserId: userID}
cmd := models.DeleteUserCommand{UserId: userID}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to delete user", err)
@@ -111,8 +111,48 @@ func AdminDeleteUser(c *m.ReqContext) {
c.JsonOK("User deleted")
}
// POST /api/admin/users/:id/disable
func AdminDisableUser(c *models.ReqContext) {
userID := c.ParamsInt64(":id")
// External users shouldn't be disabled from API
authInfoQuery := &models.GetAuthInfoQuery{UserId: userID}
if err := bus.Dispatch(authInfoQuery); err != models.ErrUserNotFound {
c.JsonApiErr(500, "Could not disable external user", nil)
return
}
disableCmd := models.DisableUserCommand{UserId: userID, IsDisabled: true}
if err := bus.Dispatch(&disableCmd); err != nil {
c.JsonApiErr(500, "Failed to disable user", err)
return
}
c.JsonOK("User disabled")
}
// POST /api/admin/users/:id/enable
func AdminEnableUser(c *models.ReqContext) {
userID := c.ParamsInt64(":id")
// External users shouldn't be disabled from API
authInfoQuery := &models.GetAuthInfoQuery{UserId: userID}
if err := bus.Dispatch(authInfoQuery); err != models.ErrUserNotFound {
c.JsonApiErr(500, "Could not enable external user", nil)
return
}
disableCmd := models.DisableUserCommand{UserId: userID, IsDisabled: false}
if err := bus.Dispatch(&disableCmd); err != nil {
c.JsonApiErr(500, "Failed to enable user", err)
return
}
c.JsonOK("User enabled")
}
// POST /api/admin/users/:id/logout
func (server *HTTPServer) AdminLogoutUser(c *m.ReqContext) Response {
func (server *HTTPServer) AdminLogoutUser(c *models.ReqContext) Response {
userID := c.ParamsInt64(":id")
if c.UserId == userID {
@@ -123,13 +163,13 @@ func (server *HTTPServer) AdminLogoutUser(c *m.ReqContext) Response {
}
// GET /api/admin/users/:id/auth-tokens
func (server *HTTPServer) AdminGetUserAuthTokens(c *m.ReqContext) Response {
func (server *HTTPServer) AdminGetUserAuthTokens(c *models.ReqContext) Response {
userID := c.ParamsInt64(":id")
return server.getUserAuthTokensInternal(c, userID)
}
// POST /api/admin/users/:id/revoke-auth-token
func (server *HTTPServer) AdminRevokeUserAuthToken(c *m.ReqContext, cmd m.RevokeAuthTokenCmd) Response {
func (server *HTTPServer) AdminRevokeUserAuthToken(c *models.ReqContext, cmd models.RevokeAuthTokenCmd) Response {
userID := c.ParamsInt64(":id")
return server.revokeUserAuthTokenInternal(c, userID, cmd)
}
+53
View File
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
@@ -84,6 +85,36 @@ func TestAdminApiEndpoint(t *testing.T) {
So(userId, ShouldEqual, 200)
})
})
Convey("When a server admin attempts to disable/enable external user", t, func() {
userId := int64(0)
bus.AddHandler("test", func(cmd *m.GetAuthInfoQuery) error {
userId = cmd.UserId
return nil
})
adminDisableUserScenario("Should return Could not disable external user error", "disable", "/api/admin/users/42/disable", "/api/admin/users/:id/disable", func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 500)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("message").MustString(), ShouldEqual, "Could not disable external user")
So(userId, ShouldEqual, 42)
})
adminDisableUserScenario("Should return Could not enable external user error", "enable", "/api/admin/users/42/enable", "/api/admin/users/:id/enable", func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 500)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("message").MustString(), ShouldEqual, "Could not enable external user")
So(userId, ShouldEqual, 42)
})
})
}
func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
@@ -186,3 +217,25 @@ func adminGetUserAuthTokensScenario(desc string, url string, routePattern string
fn(sc)
})
}
func adminDisableUserScenario(desc string, action string, url string, routePattern string, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *m.ReqContext) {
sc.context = c
sc.context.UserId = TestUserID
if action == "enable" {
AdminEnableUser(c)
} else {
AdminDisableUser(c)
}
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
+2
View File
@@ -381,6 +381,8 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
adminRoute.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
adminRoute.Delete("/users/:id", AdminDeleteUser)
adminRoute.Post("/users/:id/disable", AdminDisableUser)
adminRoute.Post("/users/:id/enable", AdminEnableUser)
adminRoute.Get("/users/:id/quotas", Wrap(GetUserQuotas))
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
adminRoute.Get("/stats", AdminGetStats)
+4
View File
@@ -105,6 +105,10 @@ func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response
return Error(401, "Invalid username or password", err)
}
if err == login.ErrUserDisabled {
return Error(401, "User is disabled", err)
}
return Error(500, "Error while trying to authenticate user", err)
}