mirror of
https://github.com/grafana/grafana.git
synced 2024-12-26 17:01:09 -06:00
feat(api): support list/revoke auth token in admin/current user api
This commit is contained in:
parent
8029e48588
commit
0cd5a6772d
@ -110,3 +110,26 @@ func AdminDeleteUser(c *m.ReqContext) {
|
|||||||
|
|
||||||
c.JsonOK("User deleted")
|
c.JsonOK("User deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /api/admin/users/:id/logout
|
||||||
|
func (server *HTTPServer) AdminLogoutUser(c *m.ReqContext) Response {
|
||||||
|
userID := c.ParamsInt64(":id")
|
||||||
|
|
||||||
|
if c.UserId == userID {
|
||||||
|
return Error(400, "You cannot logout yourself", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.logoutUserFromAllDevicesInternal(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/admin/users/:id/auth-tokens
|
||||||
|
func (server *HTTPServer) AdminGetUserAuthTokens(c *m.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 {
|
||||||
|
userID := c.ParamsInt64(":id")
|
||||||
|
return server.revokeUserAuthTokenInternal(c, userID, cmd)
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
@ -27,6 +28,62 @@ func TestAdminApiEndpoint(t *testing.T) {
|
|||||||
So(sc.resp.Code, ShouldEqual, 400)
|
So(sc.resp.Code, ShouldEqual, 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("When a server admin attempts to logout himself from all devices", t, func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
cmd.Result = &m.User{Id: TestUserID}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
adminLogoutUserScenario("Should not be allowed when calling POST on", "/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When a server admin attempts to logout a non-existing user from all devices", t, func() {
|
||||||
|
userId := int64(0)
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
userId = cmd.Id
|
||||||
|
return m.ErrUserNotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
adminLogoutUserScenario("Should return not found when calling POST on", "/api/admin/users/200/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 404)
|
||||||
|
So(userId, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When a server admin attempts to revoke an auth token for a non-existing user", t, func() {
|
||||||
|
userId := int64(0)
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
userId = cmd.Id
|
||||||
|
return m.ErrUserNotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
|
||||||
|
|
||||||
|
adminRevokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/admin/users/200/revoke-auth-token", "/api/admin/users/:id/revoke-auth-token", cmd, func(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 404)
|
||||||
|
So(userId, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When a server admin gets auth tokens for a non-existing user", t, func() {
|
||||||
|
userId := int64(0)
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
userId = cmd.Id
|
||||||
|
return m.ErrUserNotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
adminGetUserAuthTokensScenario("Should return not found when calling GET on", "/api/admin/users/200/auth-tokens", "/api/admin/users/:id/auth-tokens", func(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 404)
|
||||||
|
So(userId, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
|
func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
|
||||||
@ -48,3 +105,84 @@ func putAdminScenario(desc string, url string, routePattern string, role m.RoleT
|
|||||||
fn(sc)
|
fn(sc)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func adminLogoutUserScenario(desc string, url string, routePattern string, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
AuthTokenService: auth.NewFakeUserAuthTokenService(),
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext(url)
|
||||||
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = TestUserID
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = m.ROLE_ADMIN
|
||||||
|
|
||||||
|
return hs.AdminLogoutUser(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminRevokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd m.RevokeAuthTokenCmd, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
AuthTokenService: fakeAuthTokenService,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext(url)
|
||||||
|
sc.userAuthTokenService = fakeAuthTokenService
|
||||||
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = TestUserID
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = m.ROLE_ADMIN
|
||||||
|
|
||||||
|
return hs.AdminRevokeUserAuthToken(c, cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminGetUserAuthTokensScenario(desc string, url string, routePattern string, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
AuthTokenService: fakeAuthTokenService,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext(url)
|
||||||
|
sc.userAuthTokenService = fakeAuthTokenService
|
||||||
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = TestUserID
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = m.ROLE_ADMIN
|
||||||
|
|
||||||
|
return hs.AdminGetUserAuthTokens(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Get(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -133,6 +133,9 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
userRoute.Get("/preferences", Wrap(GetUserPreferences))
|
userRoute.Get("/preferences", Wrap(GetUserPreferences))
|
||||||
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
|
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
|
||||||
|
|
||||||
|
userRoute.Get("/auth-tokens", Wrap(hs.GetUserAuthTokens))
|
||||||
|
userRoute.Post("/revoke-auth-token", bind(m.RevokeAuthTokenCmd{}), Wrap(hs.RevokeUserAuthToken))
|
||||||
})
|
})
|
||||||
|
|
||||||
// users (admin permission required)
|
// users (admin permission required)
|
||||||
@ -375,6 +378,10 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
|
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
|
||||||
adminRoute.Get("/stats", AdminGetStats)
|
adminRoute.Get("/stats", AdminGetStats)
|
||||||
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
|
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
|
||||||
|
|
||||||
|
adminRoute.Post("/users/:id/logout", Wrap(hs.AdminLogoutUser))
|
||||||
|
adminRoute.Get("/users/:id/auth-tokens", Wrap(hs.AdminGetUserAuthTokens))
|
||||||
|
adminRoute.Post("/users/:id/revoke-auth-token", bind(m.RevokeAuthTokenCmd{}), Wrap(hs.AdminRevokeUserAuthToken))
|
||||||
}, reqGrafanaAdmin)
|
}, reqGrafanaAdmin)
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
@ -94,13 +95,14 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
|
|||||||
}
|
}
|
||||||
|
|
||||||
type scenarioContext struct {
|
type scenarioContext struct {
|
||||||
m *macaron.Macaron
|
m *macaron.Macaron
|
||||||
context *m.ReqContext
|
context *m.ReqContext
|
||||||
resp *httptest.ResponseRecorder
|
resp *httptest.ResponseRecorder
|
||||||
handlerFunc handlerFunc
|
handlerFunc handlerFunc
|
||||||
defaultHandler macaron.Handler
|
defaultHandler macaron.Handler
|
||||||
req *http.Request
|
req *http.Request
|
||||||
url string
|
url string
|
||||||
|
userAuthTokenService *auth.FakeUserAuthTokenService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *scenarioContext) exec() {
|
func (sc *scenarioContext) exec() {
|
||||||
|
12
pkg/api/dtos/user_token.go
Normal file
12
pkg/api/dtos/user_token.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package dtos
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type UserToken struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
ClientIp string `json:"clientIp"`
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
SeenAt time.Time `json:"seenAt"`
|
||||||
|
}
|
110
pkg/api/user_token.go
Normal file
110
pkg/api/user_token.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/user/auth-tokens
|
||||||
|
func (server *HTTPServer) GetUserAuthTokens(c *models.ReqContext) Response {
|
||||||
|
return server.getUserAuthTokensInternal(c, c.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/user/revoke-auth-token
|
||||||
|
func (server *HTTPServer) RevokeUserAuthToken(c *models.ReqContext, cmd models.RevokeAuthTokenCmd) Response {
|
||||||
|
return server.revokeUserAuthTokenInternal(c, c.UserId, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *HTTPServer) logoutUserFromAllDevicesInternal(userID int64) Response {
|
||||||
|
userQuery := models.GetUserByIdQuery{Id: userID}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&userQuery); err != nil {
|
||||||
|
if err == models.ErrUserNotFound {
|
||||||
|
return Error(404, "User not found", err)
|
||||||
|
}
|
||||||
|
return Error(500, "Could not read user from database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := server.AuthTokenService.RevokeAllUserTokens(userID)
|
||||||
|
if err != nil {
|
||||||
|
return Error(500, "Failed to logout user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON(200, util.DynMap{
|
||||||
|
"message": "User logged out",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int64) Response {
|
||||||
|
userQuery := models.GetUserByIdQuery{Id: userID}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&userQuery); err != nil {
|
||||||
|
if err == models.ErrUserNotFound {
|
||||||
|
return Error(404, "User not found", err)
|
||||||
|
}
|
||||||
|
return Error(500, "Failed to get user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens, err := server.AuthTokenService.GetUserTokens(userID)
|
||||||
|
if err != nil {
|
||||||
|
return Error(500, "Failed to get user auth tokens", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []*dtos.UserToken{}
|
||||||
|
for _, token := range tokens {
|
||||||
|
isActive := false
|
||||||
|
if c.UserToken != nil && c.UserToken.Id == token.Id {
|
||||||
|
isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, &dtos.UserToken{
|
||||||
|
Id: token.Id,
|
||||||
|
IsActive: isActive,
|
||||||
|
ClientIp: token.ClientIp,
|
||||||
|
UserAgent: token.UserAgent,
|
||||||
|
CreatedAt: time.Unix(token.CreatedAt, 0),
|
||||||
|
SeenAt: time.Unix(token.SeenAt, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON(200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd models.RevokeAuthTokenCmd) Response {
|
||||||
|
userQuery := models.GetUserByIdQuery{Id: userID}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&userQuery); err != nil {
|
||||||
|
if err == models.ErrUserNotFound {
|
||||||
|
return Error(404, "User not found", err)
|
||||||
|
}
|
||||||
|
return Error(500, "Failed to get user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := server.AuthTokenService.GetUserToken(userID, cmd.AuthTokenId)
|
||||||
|
if err != nil {
|
||||||
|
if err == models.ErrUserTokenNotFound {
|
||||||
|
return Error(404, "User auth token not found", err)
|
||||||
|
}
|
||||||
|
return Error(500, "Failed to get user auth token", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UserToken != nil && c.UserToken.Id == token.Id {
|
||||||
|
return Error(400, "Cannot revoke active user auth token", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.AuthTokenService.RevokeToken(token)
|
||||||
|
if err != nil {
|
||||||
|
if err == models.ErrUserTokenNotFound {
|
||||||
|
return Error(404, "User auth token not found", err)
|
||||||
|
}
|
||||||
|
return Error(500, "Failed to revoke user auth token", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON(200, util.DynMap{
|
||||||
|
"message": "User auth token revoked",
|
||||||
|
})
|
||||||
|
}
|
294
pkg/api/user_token_test.go
Normal file
294
pkg/api/user_token_test.go
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserTokenApiEndpoint(t *testing.T) {
|
||||||
|
Convey("When current user attempts to revoke an auth token for a non-existing user", t, func() {
|
||||||
|
userId := int64(0)
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
userId = cmd.Id
|
||||||
|
return m.ErrUserNotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
|
||||||
|
|
||||||
|
revokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/user/revoke-auth-token", "/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 404)
|
||||||
|
So(userId, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When current user gets auth tokens for a non-existing user", t, func() {
|
||||||
|
userId := int64(0)
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
userId = cmd.Id
|
||||||
|
return m.ErrUserNotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
getUserAuthTokensScenario("Should return not found when calling GET on", "/api/user/auth-tokens", "/api/user/auth-tokens", 200, func(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 404)
|
||||||
|
So(userId, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When logout an existing user from all devices", t, func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
cmd.Result = &m.User{Id: 200}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
logoutUserFromAllDevicesInternalScenario("Should be successful", 1, func(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When logout a non-existing user from all devices", t, func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
return m.ErrUserNotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
logoutUserFromAllDevicesInternalScenario("Should return not found", TestUserID, func(sc *scenarioContext) {
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When revoke an auth token for a user", t, func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
cmd.Result = &m.User{Id: 200}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
|
||||||
|
token := &m.UserToken{Id: 1}
|
||||||
|
|
||||||
|
revokeUserAuthTokenInternalScenario("Should be successful", cmd, 200, token, func(sc *scenarioContext) {
|
||||||
|
sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) {
|
||||||
|
return &m.UserToken{Id: 2}, nil
|
||||||
|
}
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When revoke the active auth token used by himself", t, func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
cmd.Result = &m.User{Id: TestUserID}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := m.RevokeAuthTokenCmd{AuthTokenId: 2}
|
||||||
|
token := &m.UserToken{Id: 2}
|
||||||
|
|
||||||
|
revokeUserAuthTokenInternalScenario("Should not be successful", cmd, TestUserID, token, func(sc *scenarioContext) {
|
||||||
|
sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
So(sc.resp.Code, ShouldEqual, 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When gets auth tokens for a user", t, func() {
|
||||||
|
bus.AddHandler("test", func(cmd *m.GetUserByIdQuery) error {
|
||||||
|
cmd.Result = &m.User{Id: TestUserID}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
currentToken := &m.UserToken{Id: 1}
|
||||||
|
|
||||||
|
getUserAuthTokensInternalScenario("Should be successful", currentToken, func(sc *scenarioContext) {
|
||||||
|
tokens := []*m.UserToken{
|
||||||
|
{
|
||||||
|
Id: 1,
|
||||||
|
ClientIp: "127.0.0.1",
|
||||||
|
UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
SeenAt: time.Now().Unix(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: 2,
|
||||||
|
ClientIp: "127.0.0.2",
|
||||||
|
UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
SeenAt: time.Now().Unix(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sc.userAuthTokenService.GetUserTokensProvider = func(userId int64) ([]*m.UserToken, error) {
|
||||||
|
return tokens, nil
|
||||||
|
}
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
result := sc.ToJSON()
|
||||||
|
So(result.MustArray(), ShouldHaveLength, 2)
|
||||||
|
|
||||||
|
resultOne := result.GetIndex(0)
|
||||||
|
So(resultOne.Get("id").MustInt64(), ShouldEqual, tokens[0].Id)
|
||||||
|
So(resultOne.Get("isActive").MustBool(), ShouldBeTrue)
|
||||||
|
So(resultOne.Get("clientIp").MustString(), ShouldEqual, "127.0.0.1")
|
||||||
|
So(resultOne.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36")
|
||||||
|
So(resultOne.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[0].CreatedAt, 0).Format(time.RFC3339))
|
||||||
|
So(resultOne.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[0].SeenAt, 0).Format(time.RFC3339))
|
||||||
|
|
||||||
|
resultTwo := result.GetIndex(1)
|
||||||
|
So(resultTwo.Get("id").MustInt64(), ShouldEqual, tokens[1].Id)
|
||||||
|
So(resultTwo.Get("isActive").MustBool(), ShouldBeFalse)
|
||||||
|
So(resultTwo.Get("clientIp").MustString(), ShouldEqual, "127.0.0.2")
|
||||||
|
So(resultTwo.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1")
|
||||||
|
So(resultTwo.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339))
|
||||||
|
So(resultTwo.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[1].SeenAt, 0).Format(time.RFC3339))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func revokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd m.RevokeAuthTokenCmd, userId int64, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
AuthTokenService: fakeAuthTokenService,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext(url)
|
||||||
|
sc.userAuthTokenService = fakeAuthTokenService
|
||||||
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = userId
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = m.ROLE_ADMIN
|
||||||
|
|
||||||
|
return hs.RevokeUserAuthToken(c, cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserAuthTokensScenario(desc string, url string, routePattern string, userId int64, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
AuthTokenService: fakeAuthTokenService,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext(url)
|
||||||
|
sc.userAuthTokenService = fakeAuthTokenService
|
||||||
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = userId
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = m.ROLE_ADMIN
|
||||||
|
|
||||||
|
return hs.GetUserAuthTokens(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Get(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func logoutUserFromAllDevicesInternalScenario(desc string, userId int64, fn scenarioFunc) {
|
||||||
|
Convey(desc, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
AuthTokenService: auth.NewFakeUserAuthTokenService(),
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext("/")
|
||||||
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = TestUserID
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = m.ROLE_ADMIN
|
||||||
|
|
||||||
|
return hs.logoutUserFromAllDevicesInternal(userId)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Post("/", sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func revokeUserAuthTokenInternalScenario(desc string, cmd m.RevokeAuthTokenCmd, userId int64, token *m.UserToken, fn scenarioFunc) {
|
||||||
|
Convey(desc, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
AuthTokenService: fakeAuthTokenService,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext("/")
|
||||||
|
sc.userAuthTokenService = fakeAuthTokenService
|
||||||
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = TestUserID
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = m.ROLE_ADMIN
|
||||||
|
sc.context.UserToken = token
|
||||||
|
|
||||||
|
return hs.revokeUserAuthTokenInternal(c, userId, cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Post("/", sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserAuthTokensInternalScenario(desc string, token *m.UserToken, fn scenarioFunc) {
|
||||||
|
Convey(desc, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||||
|
|
||||||
|
hs := HTTPServer{
|
||||||
|
Bus: bus.GetBus(),
|
||||||
|
AuthTokenService: fakeAuthTokenService,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext("/")
|
||||||
|
sc.userAuthTokenService = fakeAuthTokenService
|
||||||
|
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = TestUserID
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = m.ROLE_ADMIN
|
||||||
|
sc.context.UserToken = token
|
||||||
|
|
||||||
|
return hs.getUserAuthTokensInternal(c, TestUserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Get("/", sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
// Typed errors
|
// Typed errors
|
||||||
var (
|
var (
|
||||||
@ -23,6 +25,10 @@ type UserToken struct {
|
|||||||
UnhashedToken string
|
UnhashedToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RevokeAuthTokenCmd struct {
|
||||||
|
AuthTokenId int64 `json:"authTokenId"`
|
||||||
|
}
|
||||||
|
|
||||||
// UserTokenService are used for generating and validating user tokens
|
// UserTokenService are used for generating and validating user tokens
|
||||||
type UserTokenService interface {
|
type UserTokenService interface {
|
||||||
CreateToken(userId int64, clientIP, userAgent string) (*UserToken, error)
|
CreateToken(userId int64, clientIP, userAgent string) (*UserToken, error)
|
||||||
|
Loading…
Reference in New Issue
Block a user