mirror of https://github.com/grafana/grafana.git
Merge pull request #15839 from grafana/15836_revoke_auth_tokens
Support list and revoke of user auth tokens in HTTP API
This commit is contained in:
commit
23852b59c9
|
|
@ -341,3 +341,105 @@ Content-Type: application/json
|
||||||
|
|
||||||
{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
|
{"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"}
|
{"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")
|
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() {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
msession "github.com/go-macaron/session"
|
||||||
"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/grafana/grafana/pkg/services/session"
|
"github.com/grafana/grafana/pkg/services/session"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
|
@ -155,7 +156,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
||||||
return &m.UserToken{
|
return &m.UserToken{
|
||||||
UserId: 12,
|
UserId: 12,
|
||||||
UnhashedToken: unhashedToken,
|
UnhashedToken: unhashedToken,
|
||||||
|
|
@ -184,14 +185,14 @@ func TestMiddlewareContext(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
||||||
return &m.UserToken{
|
return &m.UserToken{
|
||||||
UserId: 12,
|
UserId: 12,
|
||||||
UnhashedToken: "",
|
UnhashedToken: "",
|
||||||
}, nil
|
}, 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"
|
userToken.UnhashedToken = "rotated"
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +227,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||||
middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) {
|
middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) {
|
||||||
sc.withTokenSessionCookie("token")
|
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
|
return nil, m.ErrUserTokenNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -562,7 +563,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
session.Init(&msession.Options{}, 0)
|
session.Init(&msession.Options{}, 0)
|
||||||
sc.userAuthTokenService = newFakeUserAuthTokenService()
|
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
|
||||||
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
|
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
|
||||||
// mock out gc goroutine
|
// mock out gc goroutine
|
||||||
session.StartSessionGC = func() {}
|
session.StartSessionGC = func() {}
|
||||||
|
|
@ -595,7 +596,7 @@ type scenarioContext struct {
|
||||||
handlerFunc handlerFunc
|
handlerFunc handlerFunc
|
||||||
defaultHandler macaron.Handler
|
defaultHandler macaron.Handler
|
||||||
url string
|
url string
|
||||||
userAuthTokenService *fakeUserAuthTokenService
|
userAuthTokenService *auth.FakeUserAuthTokenService
|
||||||
|
|
||||||
req *http.Request
|
req *http.Request
|
||||||
}
|
}
|
||||||
|
|
@ -676,57 +677,3 @@ func (sc *scenarioContext) exec() {
|
||||||
|
|
||||||
type scenarioFunc func(c *scenarioContext)
|
type scenarioFunc func(c *scenarioContext)
|
||||||
type handlerFunc func(c *m.ReqContext)
|
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
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
||||||
return &m.UserToken{
|
return &m.UserToken{
|
||||||
UserId: 0,
|
UserId: 0,
|
||||||
UnhashedToken: "",
|
UnhashedToken: "",
|
||||||
|
|
@ -50,7 +50,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
||||||
return &m.UserToken{
|
return &m.UserToken{
|
||||||
UserId: 12,
|
UserId: 12,
|
||||||
UnhashedToken: "",
|
UnhashedToken: "",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package middleware
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/quota"
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
|
@ -36,7 +37,7 @@ func TestMiddlewareQuota(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fakeAuthTokenService := newFakeUserAuthTokenService()
|
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||||
qs := "a.QuotaService{
|
qs := "a.QuotaService{
|
||||||
AuthTokenService: fakeAuthTokenService,
|
AuthTokenService: fakeAuthTokenService,
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +88,7 @@ func TestMiddlewareQuota(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) {
|
||||||
return &m.UserToken{
|
return &m.UserToken{
|
||||||
UserId: 12,
|
UserId: 12,
|
||||||
UnhashedToken: "",
|
UnhashedToken: "",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"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/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
macaron "gopkg.in/macaron.v1"
|
macaron "gopkg.in/macaron.v1"
|
||||||
|
|
@ -62,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
|
||||||
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
sc.userAuthTokenService = newFakeUserAuthTokenService()
|
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
|
||||||
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
|
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
|
||||||
// mock out gc goroutine
|
// mock out gc goroutine
|
||||||
sc.m.Use(OrgRedirect())
|
sc.m.Use(OrgRedirect())
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
// Typed errors
|
// Typed errors
|
||||||
var (
|
var (
|
||||||
|
|
@ -23,11 +25,18 @@ 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)
|
||||||
LookupToken(unhashedToken string) (*UserToken, error)
|
LookupToken(unhashedToken string) (*UserToken, error)
|
||||||
TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
|
TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
|
||||||
RevokeToken(token *UserToken) error
|
RevokeToken(token *UserToken) error
|
||||||
|
RevokeAllUserTokens(userId int64) error
|
||||||
ActiveTokenCount() (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
|
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 {
|
func (s *UserAuthTokenService) createdAfterParam() int64 {
|
||||||
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
|
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
|
||||||
return getTime().Add(-tokenMaxLifetime).Unix()
|
return getTime().Add(-tokenMaxLifetime).Unix()
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,47 @@ func TestUserAuthToken(t *testing.T) {
|
||||||
err = userAuthTokenService.RevokeToken(userToken)
|
err = userAuthTokenService.RevokeToken(userToken)
|
||||||
So(err, ShouldEqual, models.ErrUserTokenNotFound)
|
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() {
|
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…
Reference in New Issue