Merge pull request #15839 from grafana/15836_revoke_auth_tokens

Support list and revoke of user auth tokens in HTTP API
pull/16007/head
Carl Bergquist 6 years ago committed by GitHub
commit 23852b59c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 102
      docs/sources/http_api/admin.md
  2. 72
      docs/sources/http_api/user.md
  3. 23
      pkg/api/admin_users.go
  4. 138
      pkg/api/admin_users_test.go
  5. 7
      pkg/api/api.go
  6. 16
      pkg/api/common_test.go
  7. 12
      pkg/api/dtos/user_token.go
  8. 110
      pkg/api/user_token.go
  9. 294
      pkg/api/user_token_test.go
  10. 67
      pkg/middleware/middleware_test.go
  11. 4
      pkg/middleware/org_redirect_test.go
  12. 5
      pkg/middleware/quota_test.go
  13. 3
      pkg/middleware/recovery_test.go
  14. 11
      pkg/models/user_token.go
  15. 51
      pkg/services/auth/auth_token.go
  16. 41
      pkg/services/auth/auth_token_test.go
  17. 81
      pkg/services/auth/testing.go

@ -341,3 +341,105 @@ Content-Type: application/json
{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
```
## Auth tokens for User
`GET /api/admin/users/:id/auth-tokens`
Return a list of all auth tokens (devices) that the user currently have logged in from.
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
**Example Request**:
```http
GET /api/admin/users/1/auth-tokens HTTP/1.1
Accept: application/json
Content-Type: application/json
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 361,
"isActive": false,
"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": "2019-03-05T21:22:54+01:00",
"seenAt": "2019-03-06T19:41:06+01:00"
},
{
"id": 364,
"isActive": false,
"clientIp": "127.0.0.1",
"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": "2019-03-06T19:41:19+01:00",
"seenAt": "2019-03-06T19:41:21+01:00"
}
]
```
## Revoke auth token for User
`POST /api/admin/users/:id/revoke-auth-token`
Revokes the given auth token (device) for the user. User of issued auth token (device) will no longer be logged in
and will be required to authenticate again upon next activity.
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
**Example Request**:
```http
POST /api/admin/users/1/revoke-auth-token HTTP/1.1
Accept: application/json
Content-Type: application/json
{
"authTokenId": 364
}
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"message": "User auth token revoked"
}
```
## Logout User
`POST /api/admin/users/:id/logout`
Logout user revokes all auth tokens (devices) for the user. User of issued auth tokens (devices) will no longer be logged in
and will be required to authenticate again upon next activity.
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
**Example Request**:
```http
POST /api/admin/users/1/logout HTTP/1.1
Accept: application/json
Content-Type: application/json
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"message": "User auth token revoked"
}
```

@ -478,3 +478,75 @@ Content-Type: application/json
{"message":"Dashboard unstarred"}
```
## Auth tokens of the actual User
`GET /api/user/auth-tokens`
Return a list of all auth tokens (devices) that the actual user currently have logged in from.
**Example Request**:
```http
GET /api/user/auth-tokens HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 361,
"isActive": true,
"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": "2019-03-05T21:22:54+01:00",
"seenAt": "2019-03-06T19:41:06+01:00"
},
{
"id": 364,
"isActive": false,
"clientIp": "127.0.0.1",
"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": "2019-03-06T19:41:19+01:00",
"seenAt": "2019-03-06T19:41:21+01:00"
}
]
```
## Revoke an auth token of the actual User
`POST /api/user/revoke-auth-token`
Revokes the given auth token (device) for the actual user. User of issued auth token (device) will no longer be logged in
and will be required to authenticate again upon next activity.
**Example Request**:
```http
POST /api/user/revoke-auth-token HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"authTokenId": 364
}
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"message": "User auth token revoked"
}
```

@ -110,3 +110,26 @@ func AdminDeleteUser(c *m.ReqContext) {
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/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
. "github.com/smartystreets/goconvey/convey"
)
@ -27,6 +28,62 @@ func TestAdminApiEndpoint(t *testing.T) {
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) {
@ -48,3 +105,84 @@ func putAdminScenario(desc string, url string, routePattern string, role m.RoleT
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.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)
@ -375,6 +378,10 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
adminRoute.Get("/stats", AdminGetStats)
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)
// rendering

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"gopkg.in/macaron.v1"
. "github.com/smartystreets/goconvey/convey"
@ -94,13 +95,14 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
}
type scenarioContext struct {
m *macaron.Macaron
context *m.ReqContext
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
m *macaron.Macaron
context *m.ReqContext
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
userAuthTokenService *auth.FakeUserAuthTokenService
}
func (sc *scenarioContext) exec() {

@ -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"`
}

@ -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",
})
}

@ -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)
})
}

@ -11,6 +11,7 @@ import (
msession "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@ -155,7 +156,7 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: unhashedToken,
@ -184,14 +185,14 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
sc.userAuthTokenService.tryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) {
sc.userAuthTokenService.TryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) {
userToken.UnhashedToken = "rotated"
return true, nil
}
@ -226,7 +227,7 @@ func TestMiddlewareContext(t *testing.T) {
middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return nil, m.ErrUserTokenNotFound
}
@ -562,7 +563,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
}))
session.Init(&msession.Options{}, 0)
sc.userAuthTokenService = newFakeUserAuthTokenService()
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
session.StartSessionGC = func() {}
@ -595,7 +596,7 @@ type scenarioContext struct {
handlerFunc handlerFunc
defaultHandler macaron.Handler
url string
userAuthTokenService *fakeUserAuthTokenService
userAuthTokenService *auth.FakeUserAuthTokenService
req *http.Request
}
@ -676,57 +677,3 @@ func (sc *scenarioContext) exec() {
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *m.ReqContext)
type fakeUserAuthTokenService struct {
createTokenProvider func(userId int64, clientIP, userAgent string) (*m.UserToken, error)
tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error)
lookupTokenProvider func(unhashedToken string) (*m.UserToken, error)
revokeTokenProvider func(token *m.UserToken) error
activeAuthTokenCount func() (int64, error)
}
func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
return &fakeUserAuthTokenService{
createTokenProvider: func(userId int64, clientIP, userAgent string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
},
tryRotateTokenProvider: func(token *m.UserToken, clientIP, userAgent string) (bool, error) {
return false, nil
},
lookupTokenProvider: func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
},
revokeTokenProvider: func(token *m.UserToken) error {
return nil
},
activeAuthTokenCount: func() (int64, error) {
return 10, nil
},
}
}
func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*m.UserToken, error) {
return s.createTokenProvider(userId, clientIP, userAgent)
}
func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (*m.UserToken, error) {
return s.lookupTokenProvider(unhashedToken)
}
func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP, userAgent string) (bool, error) {
return s.tryRotateTokenProvider(token, clientIP, userAgent)
}
func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error {
return s.revokeTokenProvider(token)
}
func (s *fakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
return s.activeAuthTokenCount()
}

@ -24,7 +24,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
return nil
})
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 0,
UnhashedToken: "",
@ -50,7 +50,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
return nil
})
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",

@ -3,6 +3,7 @@ package middleware
import (
"testing"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/bus"
@ -36,7 +37,7 @@ func TestMiddlewareQuota(t *testing.T) {
},
}
fakeAuthTokenService := newFakeUserAuthTokenService()
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
qs := &quota.QuotaService{
AuthTokenService: fakeAuthTokenService,
}
@ -87,7 +88,7 @@ func TestMiddlewareQuota(t *testing.T) {
return nil
})
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
return &m.UserToken{
UserId: 12,
UnhashedToken: "",

@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
macaron "gopkg.in/macaron.v1"
@ -62,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.userAuthTokenService = newFakeUserAuthTokenService()
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
sc.m.Use(OrgRedirect())

@ -1,6 +1,8 @@
package models
import "errors"
import (
"errors"
)
// Typed errors
var (
@ -23,11 +25,18 @@ type UserToken struct {
UnhashedToken string
}
type RevokeAuthTokenCmd struct {
AuthTokenId int64 `json:"authTokenId"`
}
// UserTokenService are used for generating and validating user tokens
type UserTokenService interface {
CreateToken(userId int64, clientIP, userAgent string) (*UserToken, error)
LookupToken(unhashedToken string) (*UserToken, error)
TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
RevokeToken(token *UserToken) error
RevokeAllUserTokens(userId int64) error
ActiveTokenCount() (int64, error)
GetUserToken(userId, userTokenId int64) (*UserToken, error)
GetUserTokens(userId int64) ([]*UserToken, error)
}

@ -221,6 +221,57 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
return nil
}
func (s *UserAuthTokenService) RevokeAllUserTokens(userId int64) error {
sql := `DELETE from user_auth_token WHERE user_id = ?`
res, err := s.SQLStore.NewSession().Exec(sql, userId)
if err != nil {
return err
}
affected, err := res.RowsAffected()
if err != nil {
return err
}
s.log.Debug("all user tokens for user revoked", "userId", userId, "count", affected)
return nil
}
func (s *UserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) {
var token userAuthToken
exists, err := s.SQLStore.NewSession().Where("id = ? AND user_id = ?", userTokenId, userId).Get(&token)
if err != nil {
return nil, err
}
if !exists {
return nil, models.ErrUserTokenNotFound
}
var result models.UserToken
token.toUserToken(&result)
return &result, nil
}
func (s *UserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) {
var tokens []*userAuthToken
err := s.SQLStore.NewSession().Where("user_id = ? AND created_at > ? AND rotated_at > ?", userId, s.createdAfterParam(), s.rotatedAfterParam()).Find(&tokens)
if err != nil {
return nil, err
}
result := []*models.UserToken{}
for _, token := range tokens {
var userToken models.UserToken
token.toUserToken(&userToken)
result = append(result, &userToken)
}
return result, nil
}
func (s *UserAuthTokenService) createdAfterParam() int64 {
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
return getTime().Add(-tokenMaxLifetime).Unix()

@ -75,6 +75,47 @@ func TestUserAuthToken(t *testing.T) {
err = userAuthTokenService.RevokeToken(userToken)
So(err, ShouldEqual, models.ErrUserTokenNotFound)
})
Convey("When creating an additional token", func() {
userToken2, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(userToken2, ShouldNotBeNil)
Convey("Can get first user token", func() {
token, err := userAuthTokenService.GetUserToken(userID, userToken.Id)
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
So(token.Id, ShouldEqual, userToken.Id)
})
Convey("Can get second user token", func() {
token, err := userAuthTokenService.GetUserToken(userID, userToken2.Id)
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
So(token.Id, ShouldEqual, userToken2.Id)
})
Convey("Can get user tokens", func() {
tokens, err := userAuthTokenService.GetUserTokens(userID)
So(err, ShouldBeNil)
So(tokens, ShouldHaveLength, 2)
So(tokens[0].Id, ShouldEqual, userToken.Id)
So(tokens[1].Id, ShouldEqual, userToken2.Id)
})
Convey("Can revoke all user tokens", func() {
err := userAuthTokenService.RevokeAllUserTokens(userID)
So(err, ShouldBeNil)
model, err := ctx.getAuthTokenByID(userToken.Id)
So(err, ShouldBeNil)
So(model, ShouldBeNil)
model2, err := ctx.getAuthTokenByID(userToken2.Id)
So(err, ShouldBeNil)
So(model2, ShouldBeNil)
})
})
})
Convey("expires correctly", func() {

@ -0,0 +1,81 @@
package auth
import "github.com/grafana/grafana/pkg/models"
type FakeUserAuthTokenService struct {
CreateTokenProvider func(userId int64, clientIP, userAgent string) (*models.UserToken, error)
TryRotateTokenProvider func(token *models.UserToken, clientIP, userAgent string) (bool, error)
LookupTokenProvider func(unhashedToken string) (*models.UserToken, error)
RevokeTokenProvider func(token *models.UserToken) error
RevokeAllUserTokensProvider func(userId int64) error
ActiveAuthTokenCount func() (int64, error)
GetUserTokenProvider func(userId, userTokenId int64) (*models.UserToken, error)
GetUserTokensProvider func(userId int64) ([]*models.UserToken, error)
}
func NewFakeUserAuthTokenService() *FakeUserAuthTokenService {
return &FakeUserAuthTokenService{
CreateTokenProvider: func(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
},
TryRotateTokenProvider: func(token *models.UserToken, clientIP, userAgent string) (bool, error) {
return false, nil
},
LookupTokenProvider: func(unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
},
RevokeTokenProvider: func(token *models.UserToken) error {
return nil
},
RevokeAllUserTokensProvider: func(userId int64) error {
return nil
},
ActiveAuthTokenCount: func() (int64, error) {
return 10, nil
},
GetUserTokenProvider: func(userId, userTokenId int64) (*models.UserToken, error) {
return nil, nil
},
GetUserTokensProvider: func(userId int64) ([]*models.UserToken, error) {
return nil, nil
},
}
}
func (s *FakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
return s.CreateTokenProvider(userId, clientIP, userAgent)
}
func (s *FakeUserAuthTokenService) LookupToken(unhashedToken string) (*models.UserToken, error) {
return s.LookupTokenProvider(unhashedToken)
}
func (s *FakeUserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, userAgent string) (bool, error) {
return s.TryRotateTokenProvider(token, clientIP, userAgent)
}
func (s *FakeUserAuthTokenService) RevokeToken(token *models.UserToken) error {
return s.RevokeTokenProvider(token)
}
func (s *FakeUserAuthTokenService) RevokeAllUserTokens(userId int64) error {
return s.RevokeAllUserTokensProvider(userId)
}
func (s *FakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
return s.ActiveAuthTokenCount()
}
func (s *FakeUserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) {
return s.GetUserTokenProvider(userId, userTokenId)
}
func (s *FakeUserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) {
return s.GetUserTokensProvider(userId)
}
Loading…
Cancel
Save