Auth: Allow expiration of API keys (#17678)
* Modify backend to allow expiration of API Keys * Add middleware test for expired api keys * Modify frontend to enable expiration of API Keys * Fix frontend tests * Fix migration and add index for `expires` field * Add api key tests for database access * Substitude time.Now() by a mock for test usage * Front-end modifications * Change input label to `Time to live` * Change input behavior to comply with the other similar * Add tooltip * Modify AddApiKey api call response Expiration should be *time.Time instead of string * Present expiration date in the selected timezone * Use kbn for transforming intervals to seconds * Use `assert` library for tests * Frontend fixes Add checks for empty/undefined/null values * Change expires column from datetime to integer * Restrict api key duration input It should be interval not number * AddApiKey must complain if SecondsToLive is negative * Declare ErrInvalidApiKeyExpiration * Move configuration to auth section * Update docs * Eliminate alias for models in modified files * Omit expiration from api response if empty * Eliminate Goconvey from test file * Fix test Do not sleep, use mocked timeNow() instead * Remove index for expires from api_key table The index should be anyway on both org_id and expires fields. However this commit eliminates completely the index for now since not many rows are expected to be in this table. * Use getTimeZone function * Minor change in api key listing The frontend should display a message instead of empty string if the key does not expire.
This commit is contained in:
+31
-31
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func (hs *HTTPServer) registerRoutes() {
|
||||
@@ -105,7 +105,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index)
|
||||
|
||||
// api for dashboard snapshots
|
||||
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
|
||||
r.Post("/api/snapshots/", bind(models.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
|
||||
r.Get("/api/snapshot/shared-options/", GetSharingOptions)
|
||||
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
|
||||
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
|
||||
@@ -120,7 +120,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// user (signed in)
|
||||
apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
|
||||
userRoute.Get("/", Wrap(GetSignedInUser))
|
||||
userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
|
||||
userRoute.Put("/", bind(models.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
|
||||
userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
|
||||
userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
|
||||
userRoute.Get("/teams", Wrap(GetSignedInUserTeamList))
|
||||
@@ -128,7 +128,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
|
||||
userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))
|
||||
|
||||
userRoute.Put("/password", bind(m.ChangeUserPasswordCommand{}), Wrap(ChangeUserPassword))
|
||||
userRoute.Put("/password", bind(models.ChangeUserPasswordCommand{}), Wrap(ChangeUserPassword))
|
||||
userRoute.Get("/quotas", Wrap(GetUserQuotas))
|
||||
userRoute.Put("/helpflags/:id", Wrap(SetHelpFlag))
|
||||
// For dev purpose
|
||||
@@ -138,7 +138,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
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))
|
||||
userRoute.Post("/revoke-auth-token", bind(models.RevokeAuthTokenCmd{}), Wrap(hs.RevokeUserAuthToken))
|
||||
})
|
||||
|
||||
// users (admin permission required)
|
||||
@@ -150,18 +150,18 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
|
||||
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
||||
usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))
|
||||
usersRoute.Put("/:id", bind(m.UpdateUserCommand{}), Wrap(UpdateUser))
|
||||
usersRoute.Put("/:id", bind(models.UpdateUserCommand{}), Wrap(UpdateUser))
|
||||
usersRoute.Post("/:id/using/:orgId", Wrap(UpdateUserActiveOrg))
|
||||
}, reqGrafanaAdmin)
|
||||
|
||||
// team (admin permission required)
|
||||
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
|
||||
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(hs.CreateTeam))
|
||||
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(hs.UpdateTeam))
|
||||
teamsRoute.Post("/", bind(models.CreateTeamCommand{}), Wrap(hs.CreateTeam))
|
||||
teamsRoute.Put("/:teamId", bind(models.UpdateTeamCommand{}), Wrap(hs.UpdateTeam))
|
||||
teamsRoute.Delete("/:teamId", Wrap(hs.DeleteTeamByID))
|
||||
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
|
||||
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(hs.AddTeamMember))
|
||||
teamsRoute.Put("/:teamId/members/:userId", bind(m.UpdateTeamMemberCommand{}), Wrap(hs.UpdateTeamMember))
|
||||
teamsRoute.Post("/:teamId/members", bind(models.AddTeamMemberCommand{}), Wrap(hs.AddTeamMember))
|
||||
teamsRoute.Put("/:teamId/members/:userId", bind(models.UpdateTeamMemberCommand{}), Wrap(hs.UpdateTeamMember))
|
||||
teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember))
|
||||
teamsRoute.Get("/:teamId/preferences", Wrap(hs.GetTeamPreferences))
|
||||
teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(hs.UpdateTeamPreferences))
|
||||
@@ -183,8 +183,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
|
||||
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrgCurrent))
|
||||
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddressCurrent))
|
||||
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
|
||||
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
|
||||
orgRoute.Post("/users", quota("user"), bind(models.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
|
||||
orgRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
|
||||
orgRoute.Delete("/users/:userId", Wrap(RemoveOrgUserForCurrentOrg))
|
||||
|
||||
// invites
|
||||
@@ -203,7 +203,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
})
|
||||
|
||||
// create new org
|
||||
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), Wrap(CreateOrg))
|
||||
apiRoute.Post("/orgs", quota("org"), bind(models.CreateOrgCommand{}), Wrap(CreateOrg))
|
||||
|
||||
// search all orgs
|
||||
apiRoute.Get("/orgs", reqGrafanaAdmin, Wrap(SearchOrgs))
|
||||
@@ -215,11 +215,11 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddress))
|
||||
orgsRoute.Delete("/", Wrap(DeleteOrgByID))
|
||||
orgsRoute.Get("/users", Wrap(GetOrgUsers))
|
||||
orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), Wrap(AddOrgUser))
|
||||
orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUser))
|
||||
orgsRoute.Post("/users", bind(models.AddOrgUserCommand{}), Wrap(AddOrgUser))
|
||||
orgsRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), Wrap(UpdateOrgUser))
|
||||
orgsRoute.Delete("/users/:userId", Wrap(RemoveOrgUser))
|
||||
orgsRoute.Get("/quotas", Wrap(GetOrgQuotas))
|
||||
orgsRoute.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), Wrap(UpdateOrgQuota))
|
||||
orgsRoute.Put("/quotas/:target", bind(models.UpdateOrgQuotaCmd{}), Wrap(UpdateOrgQuota))
|
||||
}, reqGrafanaAdmin)
|
||||
|
||||
// orgs (admin routes)
|
||||
@@ -230,20 +230,20 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// auth api keys
|
||||
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
|
||||
keysRoute.Get("/", Wrap(GetAPIKeys))
|
||||
keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), Wrap(AddAPIKey))
|
||||
keysRoute.Post("/", quota("api_key"), bind(models.AddApiKeyCommand{}), Wrap(hs.AddAPIKey))
|
||||
keysRoute.Delete("/:id", Wrap(DeleteAPIKey))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// Preferences
|
||||
apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
|
||||
prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), Wrap(SetHomeDashboard))
|
||||
prefRoute.Post("/set-home-dash", bind(models.SavePreferencesCommand{}), Wrap(SetHomeDashboard))
|
||||
})
|
||||
|
||||
// Data sources
|
||||
apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) {
|
||||
datasourceRoute.Get("/", Wrap(GetDataSources))
|
||||
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
|
||||
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
|
||||
datasourceRoute.Post("/", quota("data_source"), bind(models.AddDataSourceCommand{}), Wrap(AddDataSource))
|
||||
datasourceRoute.Put("/:id", bind(models.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
|
||||
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
|
||||
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
|
||||
datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
|
||||
@@ -258,7 +258,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
|
||||
pluginRoute.Get("/:pluginId/dashboards/", Wrap(GetPluginDashboards))
|
||||
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
|
||||
pluginRoute.Post("/:pluginId/settings", bind(models.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
|
||||
@@ -269,11 +269,11 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||
folderRoute.Get("/", Wrap(GetFolders))
|
||||
folderRoute.Get("/id/:id", Wrap(GetFolderByID))
|
||||
folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(hs.CreateFolder))
|
||||
folderRoute.Post("/", bind(models.CreateFolderCommand{}), Wrap(hs.CreateFolder))
|
||||
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Get("/", Wrap(GetFolderByUID))
|
||||
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), Wrap(UpdateFolder))
|
||||
folderUidRoute.Put("/", bind(models.UpdateFolderCommand{}), Wrap(UpdateFolder))
|
||||
folderUidRoute.Delete("/", Wrap(DeleteFolder))
|
||||
|
||||
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
|
||||
@@ -293,7 +293,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
|
||||
|
||||
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(hs.PostDashboard))
|
||||
dashboardRoute.Post("/db", bind(models.SaveDashboardCommand{}), Wrap(hs.PostDashboard))
|
||||
dashboardRoute.Get("/home", Wrap(GetHomeDashboard))
|
||||
dashboardRoute.Get("/tags", GetDashboardTags)
|
||||
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard))
|
||||
@@ -322,8 +322,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
playlistRoute.Get("/:id/items", ValidateOrgPlaylist, Wrap(GetPlaylistItems))
|
||||
playlistRoute.Get("/:id/dashboards", ValidateOrgPlaylist, Wrap(GetPlaylistDashboards))
|
||||
playlistRoute.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, Wrap(DeletePlaylist))
|
||||
playlistRoute.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, Wrap(UpdatePlaylist))
|
||||
playlistRoute.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), Wrap(CreatePlaylist))
|
||||
playlistRoute.Put("/:id", reqEditorRole, bind(models.UpdatePlaylistCommand{}), ValidateOrgPlaylist, Wrap(UpdatePlaylist))
|
||||
playlistRoute.Post("/", reqEditorRole, bind(models.CreatePlaylistCommand{}), Wrap(CreatePlaylist))
|
||||
})
|
||||
|
||||
// Search
|
||||
@@ -348,12 +348,12 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
apiRoute.Group("/alert-notifications", func(alertNotifications routing.RouteRegister) {
|
||||
alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), Wrap(NotificationTest))
|
||||
alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), Wrap(CreateAlertNotification))
|
||||
alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), Wrap(UpdateAlertNotification))
|
||||
alertNotifications.Post("/", bind(models.CreateAlertNotificationCommand{}), Wrap(CreateAlertNotification))
|
||||
alertNotifications.Put("/:notificationId", bind(models.UpdateAlertNotificationCommand{}), Wrap(UpdateAlertNotification))
|
||||
alertNotifications.Get("/:notificationId", Wrap(GetAlertNotificationByID))
|
||||
alertNotifications.Delete("/:notificationId", Wrap(DeleteAlertNotification))
|
||||
alertNotifications.Get("/uid/:uid", Wrap(GetAlertNotificationByUID))
|
||||
alertNotifications.Put("/uid/:uid", bind(m.UpdateAlertNotificationWithUidCommand{}), Wrap(UpdateAlertNotificationByUID))
|
||||
alertNotifications.Put("/uid/:uid", bind(models.UpdateAlertNotificationWithUidCommand{}), Wrap(UpdateAlertNotificationByUID))
|
||||
alertNotifications.Delete("/uid/:uid", Wrap(DeleteAlertNotificationByUID))
|
||||
}, reqEditorRole)
|
||||
|
||||
@@ -384,13 +384,13 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
adminRoute.Post("/users/:id/disable", Wrap(hs.AdminDisableUser))
|
||||
adminRoute.Post("/users/:id/enable", Wrap(AdminEnableUser))
|
||||
adminRoute.Get("/users/:id/quotas", Wrap(GetUserQuotas))
|
||||
adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
|
||||
adminRoute.Put("/users/:id/quotas/:target", bind(models.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))
|
||||
adminRoute.Post("/users/:id/revoke-auth-token", bind(models.RevokeAuthTokenCmd{}), Wrap(hs.AdminRevokeUserAuthToken))
|
||||
|
||||
adminRoute.Post("/provisioning/dashboards/reload", Wrap(hs.AdminProvisioningReloadDasboards))
|
||||
adminRoute.Post("/provisioning/datasources/reload", Wrap(hs.AdminProvisioningReloadDatasources))
|
||||
|
||||
+29
-11
@@ -4,32 +4,39 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetAPIKeys(c *m.ReqContext) Response {
|
||||
query := m.GetApiKeysQuery{OrgId: c.OrgId}
|
||||
func GetAPIKeys(c *models.ReqContext) Response {
|
||||
query := models.GetApiKeysQuery{OrgId: c.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return Error(500, "Failed to list api keys", err)
|
||||
}
|
||||
|
||||
result := make([]*m.ApiKeyDTO, len(query.Result))
|
||||
result := make([]*models.ApiKeyDTO, len(query.Result))
|
||||
for i, t := range query.Result {
|
||||
result[i] = &m.ApiKeyDTO{
|
||||
Id: t.Id,
|
||||
Name: t.Name,
|
||||
Role: t.Role,
|
||||
var expiration *time.Time = nil
|
||||
if t.Expires != nil {
|
||||
v := time.Unix(*t.Expires, 0)
|
||||
expiration = &v
|
||||
}
|
||||
result[i] = &models.ApiKeyDTO{
|
||||
Id: t.Id,
|
||||
Name: t.Name,
|
||||
Role: t.Role,
|
||||
Expiration: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
func DeleteAPIKey(c *m.ReqContext) Response {
|
||||
func DeleteAPIKey(c *models.ReqContext) Response {
|
||||
id := c.ParamsInt64(":id")
|
||||
|
||||
cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
|
||||
cmd := &models.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
@@ -39,17 +46,28 @@ func DeleteAPIKey(c *m.ReqContext) Response {
|
||||
return Success("API key deleted")
|
||||
}
|
||||
|
||||
func AddAPIKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response {
|
||||
func (hs *HTTPServer) AddAPIKey(c *models.ReqContext, cmd models.AddApiKeyCommand) Response {
|
||||
if !cmd.Role.IsValid() {
|
||||
return Error(400, "Invalid role specified", nil)
|
||||
}
|
||||
|
||||
if hs.Cfg.ApiKeyMaxSecondsToLive != -1 {
|
||||
if cmd.SecondsToLive == 0 {
|
||||
return Error(400, "Number of seconds before expiration should be set", nil)
|
||||
}
|
||||
if cmd.SecondsToLive > hs.Cfg.ApiKeyMaxSecondsToLive {
|
||||
return Error(400, "Number of seconds before expiration is greater than the global limit", nil)
|
||||
}
|
||||
}
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
newKeyInfo := apikeygen.New(cmd.OrgId, cmd.Name)
|
||||
cmd.Key = newKeyInfo.HashedKey
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == models.ErrInvalidApiKeyExpiration {
|
||||
return Error(400, err.Error(), nil)
|
||||
}
|
||||
return Error(500, "Failed to add API key", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,26 +14,28 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var getTime = time.Now
|
||||
|
||||
var (
|
||||
ReqGrafanaAdmin = Auth(&AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
|
||||
ReqSignedIn = Auth(&AuthOptions{ReqSignedIn: true})
|
||||
ReqEditorRole = RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
|
||||
ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN)
|
||||
ReqEditorRole = RoleAuth(models.ROLE_EDITOR, models.ROLE_ADMIN)
|
||||
ReqOrgAdmin = RoleAuth(models.ROLE_ADMIN)
|
||||
)
|
||||
|
||||
func GetContextHandler(
|
||||
ats m.UserTokenService,
|
||||
ats models.UserTokenService,
|
||||
remoteCache *remotecache.RemoteCache,
|
||||
) macaron.Handler {
|
||||
return func(c *macaron.Context) {
|
||||
ctx := &m.ReqContext{
|
||||
ctx := &models.ReqContext{
|
||||
Context: c,
|
||||
SignedInUser: &m.SignedInUser{},
|
||||
SignedInUser: &models.SignedInUser{},
|
||||
IsSignedIn: false,
|
||||
AllowAnonymous: false,
|
||||
SkipCache: false,
|
||||
@@ -68,19 +70,19 @@ func GetContextHandler(
|
||||
// update last seen every 5min
|
||||
if ctx.ShouldUpdateLastSeenAt() {
|
||||
ctx.Logger.Debug("Updating last user_seen_at", "user_id", ctx.UserId)
|
||||
if err := bus.Dispatch(&m.UpdateUserLastSeenAtCommand{UserId: ctx.UserId}); err != nil {
|
||||
if err := bus.Dispatch(&models.UpdateUserLastSeenAtCommand{UserId: ctx.UserId}); err != nil {
|
||||
ctx.Logger.Error("Failed to update last_seen_at", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initContextWithAnonymousUser(ctx *m.ReqContext) bool {
|
||||
func initContextWithAnonymousUser(ctx *models.ReqContext) bool {
|
||||
if !setting.AnonymousEnabled {
|
||||
return false
|
||||
}
|
||||
|
||||
orgQuery := m.GetOrgByNameQuery{Name: setting.AnonymousOrgName}
|
||||
orgQuery := models.GetOrgByNameQuery{Name: setting.AnonymousOrgName}
|
||||
if err := bus.Dispatch(&orgQuery); err != nil {
|
||||
log.Error(3, "Anonymous access organization error: '%s': %s", setting.AnonymousOrgName, err)
|
||||
return false
|
||||
@@ -88,14 +90,14 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool {
|
||||
|
||||
ctx.IsSignedIn = false
|
||||
ctx.AllowAnonymous = true
|
||||
ctx.SignedInUser = &m.SignedInUser{IsAnonymous: true}
|
||||
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
|
||||
ctx.SignedInUser = &models.SignedInUser{IsAnonymous: true}
|
||||
ctx.OrgRole = models.RoleType(setting.AnonymousOrgRole)
|
||||
ctx.OrgId = orgQuery.Result.Id
|
||||
ctx.OrgName = orgQuery.Result.Name
|
||||
return true
|
||||
}
|
||||
|
||||
func initContextWithApiKey(ctx *m.ReqContext) bool {
|
||||
func initContextWithApiKey(ctx *models.ReqContext) bool {
|
||||
var keyString string
|
||||
if keyString = getApiKey(ctx); keyString == "" {
|
||||
return false
|
||||
@@ -109,7 +111,7 @@ func initContextWithApiKey(ctx *m.ReqContext) bool {
|
||||
}
|
||||
|
||||
// fetch key
|
||||
keyQuery := m.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId}
|
||||
keyQuery := models.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId}
|
||||
if err := bus.Dispatch(&keyQuery); err != nil {
|
||||
ctx.JsonApiErr(401, "Invalid API key", err)
|
||||
return true
|
||||
@@ -123,15 +125,21 @@ func initContextWithApiKey(ctx *m.ReqContext) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// check for expiration
|
||||
if apikey.Expires != nil && *apikey.Expires <= getTime().Unix() {
|
||||
ctx.JsonApiErr(401, "Expired API key", err)
|
||||
return true
|
||||
}
|
||||
|
||||
ctx.IsSignedIn = true
|
||||
ctx.SignedInUser = &m.SignedInUser{}
|
||||
ctx.SignedInUser = &models.SignedInUser{}
|
||||
ctx.OrgRole = apikey.Role
|
||||
ctx.ApiKeyId = apikey.Id
|
||||
ctx.OrgId = apikey.OrgId
|
||||
return true
|
||||
}
|
||||
|
||||
func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
||||
func initContextWithBasicAuth(ctx *models.ReqContext, orgId int64) bool {
|
||||
|
||||
if !setting.BasicAuthEnabled {
|
||||
return false
|
||||
@@ -148,7 +156,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
loginQuery := m.GetUserByLoginQuery{LoginOrEmail: username}
|
||||
loginQuery := models.GetUserByLoginQuery{LoginOrEmail: username}
|
||||
if err := bus.Dispatch(&loginQuery); err != nil {
|
||||
ctx.JsonApiErr(401, "Basic auth failed", err)
|
||||
return true
|
||||
@@ -156,13 +164,13 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
||||
|
||||
user := loginQuery.Result
|
||||
|
||||
loginUserQuery := m.LoginUserQuery{Username: username, Password: password, User: user}
|
||||
loginUserQuery := models.LoginUserQuery{Username: username, Password: password, User: user}
|
||||
if err := bus.Dispatch(&loginUserQuery); err != nil {
|
||||
ctx.JsonApiErr(401, "Invalid username or password", err)
|
||||
return true
|
||||
}
|
||||
|
||||
query := m.GetSignedInUserQuery{UserId: user.Id, OrgId: orgId}
|
||||
query := models.GetSignedInUserQuery{UserId: user.Id, OrgId: orgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
ctx.JsonApiErr(401, "Authentication error", err)
|
||||
return true
|
||||
@@ -173,7 +181,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext, orgID int64) bool {
|
||||
func initContextWithToken(authTokenService models.UserTokenService, ctx *models.ReqContext, orgID int64) bool {
|
||||
rawToken := ctx.GetCookie(setting.LoginCookieName)
|
||||
if rawToken == "" {
|
||||
return false
|
||||
@@ -186,7 +194,7 @@ func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext
|
||||
return false
|
||||
}
|
||||
|
||||
query := m.GetSignedInUserQuery{UserId: token.UserId, OrgId: orgID}
|
||||
query := models.GetSignedInUserQuery{UserId: token.UserId, OrgId: orgID}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
ctx.Logger.Error("failed to get user with id", "userId", token.UserId, "error", err)
|
||||
return false
|
||||
@@ -209,7 +217,7 @@ func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext
|
||||
return true
|
||||
}
|
||||
|
||||
func WriteSessionCookie(ctx *m.ReqContext, value string, maxLifetimeDays int) {
|
||||
func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetimeDays int) {
|
||||
if setting.Env == setting.DEV {
|
||||
ctx.Logger.Info("new token", "unhashed token", value)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -21,6 +21,19 @@ import (
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func mockGetTime() {
|
||||
var timeSeed int64
|
||||
getTime = func() time.Time {
|
||||
fakeNow := time.Unix(timeSeed, 0)
|
||||
timeSeed++
|
||||
return fakeNow
|
||||
}
|
||||
}
|
||||
|
||||
func resetGetTime() {
|
||||
getTime = time.Now
|
||||
}
|
||||
|
||||
func TestMiddleWareSecurityHeaders(t *testing.T) {
|
||||
setting.ERR_TEMPLATE_NAME = "error-template"
|
||||
|
||||
@@ -83,7 +96,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
})
|
||||
|
||||
middlewareScenario(t, "middleware should add Cache-Control header for requests with html response", func(sc *scenarioContext) {
|
||||
sc.handler(func(c *m.ReqContext) {
|
||||
sc.handler(func(c *models.ReqContext) {
|
||||
data := &dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{},
|
||||
Settings: map[string]interface{}{},
|
||||
@@ -125,20 +138,20 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
|
||||
middlewareScenario(t, "Using basic auth", func(sc *scenarioContext) {
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
|
||||
query.Result = &m.User{
|
||||
bus.AddHandler("test", func(query *models.GetUserByLoginQuery) error {
|
||||
query.Result = &models.User{
|
||||
Password: util.EncodePassword("myPass", "salt"),
|
||||
Salt: "salt",
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(loginUserQuery *m.LoginUserQuery) error {
|
||||
bus.AddHandler("test", func(loginUserQuery *models.LoginUserQuery) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -156,8 +169,8 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
middlewareScenario(t, "Valid api key", func(sc *scenarioContext) {
|
||||
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
|
||||
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
|
||||
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
|
||||
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -170,15 +183,15 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
Convey("Should init middleware context", func() {
|
||||
So(sc.context.IsSignedIn, ShouldEqual, true)
|
||||
So(sc.context.OrgId, ShouldEqual, 12)
|
||||
So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR)
|
||||
So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR)
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario(t, "Valid api key, but does not match db hash", func(sc *scenarioContext) {
|
||||
keyhash := "something_not_matching"
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
|
||||
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
|
||||
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
|
||||
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -190,11 +203,34 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario(t, "Valid api key, but expired", func(sc *scenarioContext) {
|
||||
mockGetTime()
|
||||
defer resetGetTime()
|
||||
|
||||
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
|
||||
|
||||
// api key expired one second before
|
||||
expires := getTime().Add(-1 * time.Second).Unix()
|
||||
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash,
|
||||
Expires: &expires}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReq("GET", "/").withValidApiKey().exec()
|
||||
|
||||
Convey("Should return 401", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 401)
|
||||
So(sc.respJson["message"], ShouldEqual, "Expired API key")
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario(t, "Valid api key via Basic auth", func(sc *scenarioContext) {
|
||||
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
|
||||
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
|
||||
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
|
||||
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -208,20 +244,20 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
Convey("Should init middleware context", func() {
|
||||
So(sc.context.IsSignedIn, ShouldEqual, true)
|
||||
So(sc.context.OrgId, ShouldEqual, 12)
|
||||
So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR)
|
||||
So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR)
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario(t, "Non-expired auth token in cookie which not are being rotated", func(sc *scenarioContext) {
|
||||
sc.withTokenSessionCookie("token")
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) {
|
||||
return &m.UserToken{
|
||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
|
||||
return &models.UserToken{
|
||||
UserId: 12,
|
||||
UnhashedToken: unhashedToken,
|
||||
}, nil
|
||||
@@ -244,19 +280,19 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
middlewareScenario(t, "Non-expired auth token in cookie which are being rotated", func(sc *scenarioContext) {
|
||||
sc.withTokenSessionCookie("token")
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) {
|
||||
return &m.UserToken{
|
||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
|
||||
return &models.UserToken{
|
||||
UserId: 12,
|
||||
UnhashedToken: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *m.UserToken, clientIP, userAgent string) (bool, error) {
|
||||
sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *models.UserToken, clientIP, userAgent string) (bool, error) {
|
||||
userToken.UnhashedToken = "rotated"
|
||||
return true, nil
|
||||
}
|
||||
@@ -291,8 +327,8 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
middlewareScenario(t, "Invalid/expired auth token in cookie", func(sc *scenarioContext) {
|
||||
sc.withTokenSessionCookie("token")
|
||||
|
||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) {
|
||||
return nil, m.ErrUserTokenNotFound
|
||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
|
||||
return nil, models.ErrUserTokenNotFound
|
||||
}
|
||||
|
||||
sc.fakeReq("GET", "/").exec()
|
||||
@@ -307,12 +343,12 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
middlewareScenario(t, "When anonymous access is enabled", func(sc *scenarioContext) {
|
||||
setting.AnonymousEnabled = true
|
||||
setting.AnonymousOrgName = "test"
|
||||
setting.AnonymousOrgRole = string(m.ROLE_EDITOR)
|
||||
setting.AnonymousOrgRole = string(models.ROLE_EDITOR)
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetOrgByNameQuery) error {
|
||||
bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error {
|
||||
So(query.Name, ShouldEqual, "test")
|
||||
|
||||
query.Result = &m.Org{Id: 2, Name: "test"}
|
||||
query.Result = &models.Org{Id: 2, Name: "test"}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -321,7 +357,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
Convey("should init context with org info", func() {
|
||||
So(sc.context.UserId, ShouldEqual, 0)
|
||||
So(sc.context.OrgId, ShouldEqual, 2)
|
||||
So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR)
|
||||
So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR)
|
||||
})
|
||||
|
||||
Convey("context signed in should be false", func() {
|
||||
@@ -339,8 +375,8 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
name := "markelog"
|
||||
|
||||
middlewareScenario(t, "should not sync the user if it's in the cache", func(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: query.UserId}
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 4, UserId: query.UserId}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -362,16 +398,16 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
setting.LDAPEnabled = false
|
||||
setting.AuthProxyAutoSignUp = true
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
if query.UserId > 0 {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
||||
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
|
||||
return nil
|
||||
}
|
||||
return m.ErrUserNotFound
|
||||
return models.ErrUserNotFound
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||
cmd.Result = &m.User{Id: 33}
|
||||
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||
cmd.Result = &models.User{Id: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -389,13 +425,13 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
middlewareScenario(t, "should get an existing user from header", func(sc *scenarioContext) {
|
||||
setting.LDAPEnabled = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||
cmd.Result = &m.User{Id: 12}
|
||||
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||
cmd.Result = &models.User{Id: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -414,13 +450,13 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
|
||||
setting.LDAPEnabled = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||
cmd.Result = &m.User{Id: 33}
|
||||
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||
cmd.Result = &models.User{Id: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -440,13 +476,13 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
setting.AuthProxyWhitelist = "8.8.8.8"
|
||||
setting.LDAPEnabled = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||
cmd.Result = &m.User{Id: 33}
|
||||
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||
cmd.Result = &models.User{Id: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -489,7 +525,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
|
||||
|
||||
sc.m.Use(OrgRedirect())
|
||||
|
||||
sc.defaultHandler = func(c *m.ReqContext) {
|
||||
sc.defaultHandler = func(c *models.ReqContext) {
|
||||
sc.context = c
|
||||
if sc.handlerFunc != nil {
|
||||
sc.handlerFunc(sc.context)
|
||||
@@ -504,7 +540,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
|
||||
|
||||
type scenarioContext struct {
|
||||
m *macaron.Macaron
|
||||
context *m.ReqContext
|
||||
context *models.ReqContext
|
||||
resp *httptest.ResponseRecorder
|
||||
apiKey string
|
||||
authHeader string
|
||||
@@ -587,4 +623,4 @@ func (sc *scenarioContext) exec() {
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
type handlerFunc func(c *m.ReqContext)
|
||||
type handlerFunc func(c *models.ReqContext)
|
||||
|
||||
+14
-9
@@ -6,6 +6,7 @@ import (
|
||||
)
|
||||
|
||||
var ErrInvalidApiKey = errors.New("Invalid API Key")
|
||||
var ErrInvalidApiKeyExpiration = errors.New("Negative value for SecondsToLive")
|
||||
|
||||
type ApiKey struct {
|
||||
Id int64
|
||||
@@ -15,15 +16,17 @@ type ApiKey struct {
|
||||
Role RoleType
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Expires *int64
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
type AddApiKeyCommand struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Role RoleType `json:"role" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
Key string `json:"-"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Role RoleType `json:"role" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
Key string `json:"-"`
|
||||
SecondsToLive int64 `json:"secondsToLive"`
|
||||
|
||||
Result *ApiKey `json:"-"`
|
||||
}
|
||||
@@ -45,8 +48,9 @@ type DeleteApiKeyCommand struct {
|
||||
// QUERIES
|
||||
|
||||
type GetApiKeysQuery struct {
|
||||
OrgId int64
|
||||
Result []*ApiKey
|
||||
OrgId int64
|
||||
IncludeInvalid bool
|
||||
Result []*ApiKey
|
||||
}
|
||||
|
||||
type GetApiKeyByNameQuery struct {
|
||||
@@ -64,7 +68,8 @@ type GetApiKeyByIdQuery struct {
|
||||
// DTO & Projections
|
||||
|
||||
type ApiKeyDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role RoleType `json:"role"`
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role RoleType `json:"role"`
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -16,14 +16,18 @@ func init() {
|
||||
bus.AddHandler("sql", AddApiKey)
|
||||
}
|
||||
|
||||
func GetApiKeys(query *m.GetApiKeysQuery) error {
|
||||
sess := x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name")
|
||||
func GetApiKeys(query *models.GetApiKeysQuery) error {
|
||||
sess := x.Limit(100, 0).Where("org_id=? and ( expires IS NULL or expires >= ?)",
|
||||
query.OrgId, timeNow().Unix()).Asc("name")
|
||||
if query.IncludeInvalid {
|
||||
sess = x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name")
|
||||
}
|
||||
|
||||
query.Result = make([]*m.ApiKey, 0)
|
||||
query.Result = make([]*models.ApiKey, 0)
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
|
||||
func DeleteApiKeyCtx(ctx context.Context, cmd *m.DeleteApiKeyCommand) error {
|
||||
func DeleteApiKeyCtx(ctx context.Context, cmd *models.DeleteApiKeyCommand) error {
|
||||
return withDbSession(ctx, func(sess *DBSession) error {
|
||||
var rawSql = "DELETE FROM api_key WHERE id=? and org_id=?"
|
||||
_, err := sess.Exec(rawSql, cmd.Id, cmd.OrgId)
|
||||
@@ -31,15 +35,24 @@ func DeleteApiKeyCtx(ctx context.Context, cmd *m.DeleteApiKeyCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func AddApiKey(cmd *m.AddApiKeyCommand) error {
|
||||
func AddApiKey(cmd *models.AddApiKeyCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
t := m.ApiKey{
|
||||
updated := timeNow()
|
||||
var expires *int64 = nil
|
||||
if cmd.SecondsToLive > 0 {
|
||||
v := updated.Add(time.Second * time.Duration(cmd.SecondsToLive)).Unix()
|
||||
expires = &v
|
||||
} else if cmd.SecondsToLive < 0 {
|
||||
return models.ErrInvalidApiKeyExpiration
|
||||
}
|
||||
t := models.ApiKey{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Role: cmd.Role,
|
||||
Key: cmd.Key,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
Created: updated,
|
||||
Updated: updated,
|
||||
Expires: expires,
|
||||
}
|
||||
|
||||
if _, err := sess.Insert(&t); err != nil {
|
||||
@@ -50,28 +63,28 @@ func AddApiKey(cmd *m.AddApiKeyCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
|
||||
var apikey m.ApiKey
|
||||
func GetApiKeyById(query *models.GetApiKeyByIdQuery) error {
|
||||
var apikey models.ApiKey
|
||||
has, err := x.Id(query.ApiKeyId).Get(&apikey)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return m.ErrInvalidApiKey
|
||||
return models.ErrInvalidApiKey
|
||||
}
|
||||
|
||||
query.Result = &apikey
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
|
||||
var apikey m.ApiKey
|
||||
func GetApiKeyByName(query *models.GetApiKeyByNameQuery) error {
|
||||
var apikey models.ApiKey
|
||||
has, err := x.Where("org_id=? AND name=?", query.OrgId, query.KeyName).Get(&apikey)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return m.ErrInvalidApiKey
|
||||
return models.ErrInvalidApiKey
|
||||
}
|
||||
|
||||
query.Result = &apikey
|
||||
|
||||
@@ -1,31 +1,117 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestApiKeyDataAccess(t *testing.T) {
|
||||
mockTimeNow()
|
||||
defer resetTimeNow()
|
||||
|
||||
Convey("Testing API Key data access", t, func() {
|
||||
t.Run("Testing API Key data access", func(t *testing.T) {
|
||||
InitTestDB(t)
|
||||
|
||||
Convey("Given saved api key", func() {
|
||||
cmd := m.AddApiKeyCommand{OrgId: 1, Name: "hello", Key: "asd"}
|
||||
t.Run("Given saved api key", func(t *testing.T) {
|
||||
cmd := models.AddApiKeyCommand{OrgId: 1, Name: "hello", Key: "asd"}
|
||||
err := AddApiKey(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
Convey("Should be able to get key by name", func() {
|
||||
query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
|
||||
t.Run("Should be able to get key by name", func(t *testing.T) {
|
||||
query := models.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
|
||||
err = GetApiKeyByName(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldNotBeNil)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, query.Result)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("Add non expiring key", func(t *testing.T) {
|
||||
cmd := models.AddApiKeyCommand{OrgId: 1, Name: "non-expiring", Key: "asd1", SecondsToLive: 0}
|
||||
err := AddApiKey(&cmd)
|
||||
assert.Nil(t, err)
|
||||
|
||||
query := models.GetApiKeyByNameQuery{KeyName: "non-expiring", OrgId: 1}
|
||||
err = GetApiKeyByName(&query)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Nil(t, query.Result.Expires)
|
||||
})
|
||||
|
||||
t.Run("Add an expiring key", func(t *testing.T) {
|
||||
//expires in one hour
|
||||
cmd := models.AddApiKeyCommand{OrgId: 1, Name: "expiring-in-an-hour", Key: "asd2", SecondsToLive: 3600}
|
||||
err := AddApiKey(&cmd)
|
||||
assert.Nil(t, err)
|
||||
|
||||
query := models.GetApiKeyByNameQuery{KeyName: "expiring-in-an-hour", OrgId: 1}
|
||||
err = GetApiKeyByName(&query)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.True(t, *query.Result.Expires >= timeNow().Unix())
|
||||
|
||||
// timeNow() has been called twice since creation; once by AddApiKey and once by GetApiKeyByName
|
||||
// therefore two seconds should be subtracted by next value retuned by timeNow()
|
||||
// that equals the number by which timeSeed has been advanced
|
||||
then := timeNow().Add(-2 * time.Second)
|
||||
expected := then.Add(1 * time.Hour).UTC().Unix()
|
||||
assert.Equal(t, *query.Result.Expires, expected)
|
||||
})
|
||||
|
||||
t.Run("Add a key with negative lifespan", func(t *testing.T) {
|
||||
//expires in one day
|
||||
cmd := models.AddApiKeyCommand{OrgId: 1, Name: "key-with-negative-lifespan", Key: "asd3", SecondsToLive: -3600}
|
||||
err := AddApiKey(&cmd)
|
||||
assert.EqualError(t, err, models.ErrInvalidApiKeyExpiration.Error())
|
||||
|
||||
query := models.GetApiKeyByNameQuery{KeyName: "key-with-negative-lifespan", OrgId: 1}
|
||||
err = GetApiKeyByName(&query)
|
||||
assert.EqualError(t, err, "Invalid API Key")
|
||||
})
|
||||
|
||||
t.Run("Add keys", func(t *testing.T) {
|
||||
//never expires
|
||||
cmd := models.AddApiKeyCommand{OrgId: 1, Name: "key1", Key: "key1", SecondsToLive: 0}
|
||||
err := AddApiKey(&cmd)
|
||||
assert.Nil(t, err)
|
||||
|
||||
//expires in 1s
|
||||
cmd = models.AddApiKeyCommand{OrgId: 1, Name: "key2", Key: "key2", SecondsToLive: 1}
|
||||
err = AddApiKey(&cmd)
|
||||
assert.Nil(t, err)
|
||||
|
||||
//expires in one hour
|
||||
cmd = models.AddApiKeyCommand{OrgId: 1, Name: "key3", Key: "key3", SecondsToLive: 3600}
|
||||
err = AddApiKey(&cmd)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// advance mocked getTime by 1s
|
||||
timeNow()
|
||||
|
||||
query := models.GetApiKeysQuery{OrgId: 1, IncludeInvalid: false}
|
||||
err = GetApiKeys(&query)
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, k := range query.Result {
|
||||
if k.Name == "key2" {
|
||||
t.Fatalf("key2 should not be there")
|
||||
}
|
||||
}
|
||||
|
||||
query = models.GetApiKeysQuery{OrgId: 1, IncludeInvalid: true}
|
||||
err = GetApiKeys(&query)
|
||||
assert.Nil(t, err)
|
||||
|
||||
found := false
|
||||
for _, k := range query.Result {
|
||||
if k.Name == "key2" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,4 +78,8 @@ func addApiKeyMigrations(mg *Migrator) {
|
||||
{Name: "key", Type: DB_Varchar, Length: 190, Nullable: false},
|
||||
{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
}))
|
||||
|
||||
mg.AddMigration("Add expires to api_key table", NewAddColumnMigration(apiKeyV2, &Column{
|
||||
Name: "expires", Type: DB_BigInt, Nullable: true,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -259,6 +259,8 @@ type Cfg struct {
|
||||
RemoteCacheOptions *RemoteCacheOptions
|
||||
|
||||
EditorsCanAdmin bool
|
||||
|
||||
ApiKeyMaxSecondsToLive int64
|
||||
}
|
||||
|
||||
type CommandLineArgs struct {
|
||||
@@ -795,6 +797,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
|
||||
LoginMaxLifetimeDays = auth.Key("login_maximum_lifetime_days").MustInt(30)
|
||||
cfg.LoginMaxLifetimeDays = LoginMaxLifetimeDays
|
||||
cfg.ApiKeyMaxSecondsToLive = auth.Key("api_key_max_seconds_to_live").MustInt64(-1)
|
||||
|
||||
cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10)
|
||||
if cfg.TokenRotationIntervalMinutes < 2 {
|
||||
|
||||
Reference in New Issue
Block a user