mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
19185bd0af
commit
dc9ec7dc91
@ -287,6 +287,9 @@ signout_redirect_url =
|
||||
# This setting is ignored if multiple OAuth providers are configured.
|
||||
oauth_auto_login = false
|
||||
|
||||
# limit of api_key seconds to live before expiration
|
||||
api_key_max_seconds_to_live = -1
|
||||
|
||||
#################################### Anonymous Auth ######################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
|
@ -63,6 +63,9 @@ login_maximum_lifetime_days = 30
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
token_rotation_interval_minutes = 10
|
||||
|
||||
# The maximum lifetime (seconds) an api key can be used. If it is set all the api keys should have limited lifetime that is lower than this value.
|
||||
api_key_max_seconds_to_live = -1
|
||||
```
|
||||
|
||||
### Anonymous authentication
|
||||
|
@ -82,7 +82,8 @@ Content-Type: application/json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "TestAdmin",
|
||||
"role": "Admin"
|
||||
"role": "Admin",
|
||||
"expiration": "2019-06-26T10:52:03+03:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
@ -101,7 +102,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"name": "mykey",
|
||||
"role": "Admin"
|
||||
"role": "Admin",
|
||||
"secondsToLive": 86400
|
||||
}
|
||||
```
|
||||
|
||||
@ -109,6 +111,12 @@ JSON Body schema:
|
||||
|
||||
- **name** – The key name
|
||||
- **role** – Sets the access level/Grafana Role for the key. Can be one of the following values: `Viewer`, `Editor` or `Admin`.
|
||||
- **secondsToLive** – Sets the key expiration in seconds. It is optional. If it is a positive number an expiration date for the key is set. If it is null, zero or is omitted completely (unless `api_key_max_seconds_to_live` configuration option is set) the key will never expire.
|
||||
|
||||
Error statuses:
|
||||
|
||||
- **400** – `api_key_max_seconds_to_live` is set but no `secondsToLive` is specified or `secondsToLive` is greater than this value.
|
||||
- **500** – The key was unable to be stored in the database.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
|
@ -46,6 +46,7 @@ export interface DateTimeDuration {
|
||||
hours: () => number;
|
||||
minutes: () => number;
|
||||
seconds: () => number;
|
||||
asSeconds: () => number;
|
||||
}
|
||||
|
||||
export interface DateTime extends Object {
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -12,9 +12,34 @@ import ApiKeysAddedModal from './ApiKeysAddedModal';
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { DeleteButton, Input } from '@grafana/ui';
|
||||
import { DeleteButton, EventsWithValidation, FormLabel, Input, ValidationEvents } from '@grafana/ui';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { store } from 'app/store/store';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
// Utils
|
||||
import { dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
|
||||
const timeRangeValidationEvents: ValidationEvents = {
|
||||
[EventsWithValidation.onBlur]: [
|
||||
{
|
||||
rule: value => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
kbn.interval_to_seconds(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
errorMessage: 'Not a valid duration',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@ -36,13 +61,18 @@ export interface State {
|
||||
enum ApiKeyStateProps {
|
||||
Name = 'name',
|
||||
Role = 'role',
|
||||
SecondsToLive = 'secondsToLive',
|
||||
}
|
||||
|
||||
const initialApiKeyState = {
|
||||
name: '',
|
||||
role: OrgRole.Viewer,
|
||||
secondsToLive: '',
|
||||
};
|
||||
|
||||
const tooltipText =
|
||||
'The api key life duration. For example 1d if your key is going to last for one day. All the supported units are: s,m,h,d,w,M,y';
|
||||
|
||||
export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -81,6 +111,9 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
});
|
||||
};
|
||||
|
||||
// make sure that secondsToLive is number or null
|
||||
const secondsToLive = this.state.newApiKey['secondsToLive'];
|
||||
this.state.newApiKey['secondsToLive'] = secondsToLive ? kbn.interval_to_seconds(secondsToLive) : null;
|
||||
this.props.addApiKey(this.state.newApiKey, openModal);
|
||||
this.setState((prevState: State) => {
|
||||
return {
|
||||
@ -130,6 +163,17 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
);
|
||||
}
|
||||
|
||||
formatDate(date, format?) {
|
||||
if (!date) {
|
||||
return 'No expiration date';
|
||||
}
|
||||
date = isDateTime(date) ? date : dateTime(date);
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
const timezone = getTimeZone(store.getState().user);
|
||||
|
||||
return timezone.isUtc ? date.utc().format(format) : date.format(format);
|
||||
}
|
||||
|
||||
renderAddApiKeyForm() {
|
||||
const { newApiKey, isAdding } = this.state;
|
||||
|
||||
@ -170,6 +214,17 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div className="gf-form max-width-21">
|
||||
<FormLabel tooltip={tooltipText}>Time to live</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="1d"
|
||||
validationEvents={timeRangeValidationEvents}
|
||||
value={newApiKey.secondsToLive}
|
||||
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.SecondsToLive)}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<button className="btn gf-form-btn btn-primary">Add</button>
|
||||
</div>
|
||||
@ -211,6 +266,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Expires</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
@ -221,6 +277,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
<tr key={key.id}>
|
||||
<td>{key.name}</td>
|
||||
<td>{key.role}</td>
|
||||
<td>{this.formatDate(key.expiration)}</td>
|
||||
<td>
|
||||
<DeleteButton onConfirm={() => this.onDeleteApiKey(key)} />
|
||||
</td>
|
||||
|
@ -7,6 +7,8 @@ export const getMultipleMockKeys = (numberOfKeys: number): ApiKey[] => {
|
||||
id: i,
|
||||
name: `test-${i}`,
|
||||
role: OrgRole.Viewer,
|
||||
secondsToLive: null,
|
||||
expiration: '2019-06-04',
|
||||
});
|
||||
}
|
||||
|
||||
@ -18,5 +20,7 @@ export const getMockKey = (): ApiKey => {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
role: OrgRole.Admin,
|
||||
secondsToLive: null,
|
||||
expiration: '2019-06-04',
|
||||
};
|
||||
};
|
||||
|
@ -130,6 +130,32 @@ exports[`Render should render CTA if there are no API keys 1`] = `
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form max-width-21"
|
||||
>
|
||||
<Component
|
||||
tooltip="The api key life duration. For example 1d if your key is going to last for one day. All the supported units are: s,m,h,d,w,M,y"
|
||||
>
|
||||
Time to live
|
||||
</Component>
|
||||
<Input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="1d"
|
||||
type="text"
|
||||
validationEvents={
|
||||
Object {
|
||||
"onBlur": Array [
|
||||
Object {
|
||||
"errorMessage": "Not a valid duration",
|
||||
"rule": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
|
@ -4,11 +4,14 @@ export interface ApiKey {
|
||||
id: number;
|
||||
name: string;
|
||||
role: OrgRole;
|
||||
secondsToLive: number;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
export interface NewApiKey {
|
||||
name: string;
|
||||
role: OrgRole;
|
||||
secondsToLive: number;
|
||||
}
|
||||
|
||||
export interface ApiKeysState {
|
||||
|
Loading…
Reference in New Issue
Block a user