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.
|
# This setting is ignored if multiple OAuth providers are configured.
|
||||||
oauth_auto_login = false
|
oauth_auto_login = false
|
||||||
|
|
||||||
|
# limit of api_key seconds to live before expiration
|
||||||
|
api_key_max_seconds_to_live = -1
|
||||||
|
|
||||||
#################################### Anonymous Auth ######################
|
#################################### Anonymous Auth ######################
|
||||||
[auth.anonymous]
|
[auth.anonymous]
|
||||||
# enable anonymous access
|
# 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.
|
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||||
token_rotation_interval_minutes = 10
|
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
|
### Anonymous authentication
|
||||||
|
@ -82,7 +82,8 @@ Content-Type: application/json
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "TestAdmin",
|
"name": "TestAdmin",
|
||||||
"role": "Admin"
|
"role": "Admin",
|
||||||
|
"expiration": "2019-06-26T10:52:03+03:00"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@ -101,7 +102,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
|||||||
|
|
||||||
{
|
{
|
||||||
"name": "mykey",
|
"name": "mykey",
|
||||||
"role": "Admin"
|
"role": "Admin",
|
||||||
|
"secondsToLive": 86400
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -109,6 +111,12 @@ JSON Body schema:
|
|||||||
|
|
||||||
- **name** – The key name
|
- **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`.
|
- **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**:
|
**Example Response**:
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ export interface DateTimeDuration {
|
|||||||
hours: () => number;
|
hours: () => number;
|
||||||
minutes: () => number;
|
minutes: () => number;
|
||||||
seconds: () => number;
|
seconds: () => number;
|
||||||
|
asSeconds: () => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DateTime extends Object {
|
export interface DateTime extends Object {
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (hs *HTTPServer) registerRoutes() {
|
func (hs *HTTPServer) registerRoutes() {
|
||||||
@ -105,7 +105,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index)
|
r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index)
|
||||||
|
|
||||||
// api for dashboard snapshots
|
// 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/snapshot/shared-options/", GetSharingOptions)
|
||||||
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
|
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
|
||||||
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
|
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
|
||||||
@ -120,7 +120,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
// user (signed in)
|
// user (signed in)
|
||||||
apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
|
apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
|
||||||
userRoute.Get("/", Wrap(GetSignedInUser))
|
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.Post("/using/:id", Wrap(UserSetUsingOrg))
|
||||||
userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
|
userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
|
||||||
userRoute.Get("/teams", Wrap(GetSignedInUserTeamList))
|
userRoute.Get("/teams", Wrap(GetSignedInUserTeamList))
|
||||||
@ -128,7 +128,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
|
userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
|
||||||
userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))
|
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.Get("/quotas", Wrap(GetUserQuotas))
|
||||||
userRoute.Put("/helpflags/:id", Wrap(SetHelpFlag))
|
userRoute.Put("/helpflags/:id", Wrap(SetHelpFlag))
|
||||||
// For dev purpose
|
// For dev purpose
|
||||||
@ -138,7 +138,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
|
userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
|
||||||
|
|
||||||
userRoute.Get("/auth-tokens", Wrap(hs.GetUserAuthTokens))
|
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)
|
// users (admin permission required)
|
||||||
@ -150,18 +150,18 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
|
usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
|
||||||
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
||||||
usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))
|
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))
|
usersRoute.Post("/:id/using/:orgId", Wrap(UpdateUserActiveOrg))
|
||||||
}, reqGrafanaAdmin)
|
}, reqGrafanaAdmin)
|
||||||
|
|
||||||
// team (admin permission required)
|
// team (admin permission required)
|
||||||
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
|
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
|
||||||
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(hs.CreateTeam))
|
teamsRoute.Post("/", bind(models.CreateTeamCommand{}), Wrap(hs.CreateTeam))
|
||||||
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(hs.UpdateTeam))
|
teamsRoute.Put("/:teamId", bind(models.UpdateTeamCommand{}), Wrap(hs.UpdateTeam))
|
||||||
teamsRoute.Delete("/:teamId", Wrap(hs.DeleteTeamByID))
|
teamsRoute.Delete("/:teamId", Wrap(hs.DeleteTeamByID))
|
||||||
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
|
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
|
||||||
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(hs.AddTeamMember))
|
teamsRoute.Post("/:teamId/members", bind(models.AddTeamMemberCommand{}), Wrap(hs.AddTeamMember))
|
||||||
teamsRoute.Put("/:teamId/members/:userId", bind(m.UpdateTeamMemberCommand{}), Wrap(hs.UpdateTeamMember))
|
teamsRoute.Put("/:teamId/members/:userId", bind(models.UpdateTeamMemberCommand{}), Wrap(hs.UpdateTeamMember))
|
||||||
teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember))
|
teamsRoute.Delete("/:teamId/members/:userId", Wrap(hs.RemoveTeamMember))
|
||||||
teamsRoute.Get("/:teamId/preferences", Wrap(hs.GetTeamPreferences))
|
teamsRoute.Get("/:teamId/preferences", Wrap(hs.GetTeamPreferences))
|
||||||
teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(hs.UpdateTeamPreferences))
|
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) {
|
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
|
||||||
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrgCurrent))
|
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrgCurrent))
|
||||||
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddressCurrent))
|
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddressCurrent))
|
||||||
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
|
orgRoute.Post("/users", quota("user"), bind(models.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
|
||||||
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
|
orgRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
|
||||||
orgRoute.Delete("/users/:userId", Wrap(RemoveOrgUserForCurrentOrg))
|
orgRoute.Delete("/users/:userId", Wrap(RemoveOrgUserForCurrentOrg))
|
||||||
|
|
||||||
// invites
|
// invites
|
||||||
@ -203,7 +203,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// create new org
|
// 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
|
// search all orgs
|
||||||
apiRoute.Get("/orgs", reqGrafanaAdmin, Wrap(SearchOrgs))
|
apiRoute.Get("/orgs", reqGrafanaAdmin, Wrap(SearchOrgs))
|
||||||
@ -215,11 +215,11 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddress))
|
orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddress))
|
||||||
orgsRoute.Delete("/", Wrap(DeleteOrgByID))
|
orgsRoute.Delete("/", Wrap(DeleteOrgByID))
|
||||||
orgsRoute.Get("/users", Wrap(GetOrgUsers))
|
orgsRoute.Get("/users", Wrap(GetOrgUsers))
|
||||||
orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), Wrap(AddOrgUser))
|
orgsRoute.Post("/users", bind(models.AddOrgUserCommand{}), Wrap(AddOrgUser))
|
||||||
orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUser))
|
orgsRoute.Patch("/users/:userId", bind(models.UpdateOrgUserCommand{}), Wrap(UpdateOrgUser))
|
||||||
orgsRoute.Delete("/users/:userId", Wrap(RemoveOrgUser))
|
orgsRoute.Delete("/users/:userId", Wrap(RemoveOrgUser))
|
||||||
orgsRoute.Get("/quotas", Wrap(GetOrgQuotas))
|
orgsRoute.Get("/quotas", Wrap(GetOrgQuotas))
|
||||||
orgsRoute.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), Wrap(UpdateOrgQuota))
|
orgsRoute.Put("/quotas/:target", bind(models.UpdateOrgQuotaCmd{}), Wrap(UpdateOrgQuota))
|
||||||
}, reqGrafanaAdmin)
|
}, reqGrafanaAdmin)
|
||||||
|
|
||||||
// orgs (admin routes)
|
// orgs (admin routes)
|
||||||
@ -230,20 +230,20 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
// auth api keys
|
// auth api keys
|
||||||
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
|
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
|
||||||
keysRoute.Get("/", Wrap(GetAPIKeys))
|
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))
|
keysRoute.Delete("/:id", Wrap(DeleteAPIKey))
|
||||||
}, reqOrgAdmin)
|
}, reqOrgAdmin)
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
|
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
|
// Data sources
|
||||||
apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) {
|
apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) {
|
||||||
datasourceRoute.Get("/", Wrap(GetDataSources))
|
datasourceRoute.Get("/", Wrap(GetDataSources))
|
||||||
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
|
datasourceRoute.Post("/", quota("data_source"), bind(models.AddDataSourceCommand{}), Wrap(AddDataSource))
|
||||||
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
|
datasourceRoute.Put("/:id", bind(models.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
|
||||||
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
|
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
|
||||||
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
|
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
|
||||||
datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
|
datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
|
||||||
@ -258,7 +258,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
|
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
|
||||||
pluginRoute.Get("/:pluginId/dashboards/", Wrap(GetPluginDashboards))
|
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)
|
}, reqOrgAdmin)
|
||||||
|
|
||||||
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
|
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
|
||||||
@ -269,11 +269,11 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||||
folderRoute.Get("/", Wrap(GetFolders))
|
folderRoute.Get("/", Wrap(GetFolders))
|
||||||
folderRoute.Get("/id/:id", Wrap(GetFolderByID))
|
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) {
|
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||||
folderUidRoute.Get("/", Wrap(GetFolderByUID))
|
folderUidRoute.Get("/", Wrap(GetFolderByUID))
|
||||||
folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), Wrap(UpdateFolder))
|
folderUidRoute.Put("/", bind(models.UpdateFolderCommand{}), Wrap(UpdateFolder))
|
||||||
folderUidRoute.Delete("/", Wrap(DeleteFolder))
|
folderUidRoute.Delete("/", Wrap(DeleteFolder))
|
||||||
|
|
||||||
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
|
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("/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("/home", Wrap(GetHomeDashboard))
|
||||||
dashboardRoute.Get("/tags", GetDashboardTags)
|
dashboardRoute.Get("/tags", GetDashboardTags)
|
||||||
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard))
|
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/items", ValidateOrgPlaylist, Wrap(GetPlaylistItems))
|
||||||
playlistRoute.Get("/:id/dashboards", ValidateOrgPlaylist, Wrap(GetPlaylistDashboards))
|
playlistRoute.Get("/:id/dashboards", ValidateOrgPlaylist, Wrap(GetPlaylistDashboards))
|
||||||
playlistRoute.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, Wrap(DeletePlaylist))
|
playlistRoute.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, Wrap(DeletePlaylist))
|
||||||
playlistRoute.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, Wrap(UpdatePlaylist))
|
playlistRoute.Put("/:id", reqEditorRole, bind(models.UpdatePlaylistCommand{}), ValidateOrgPlaylist, Wrap(UpdatePlaylist))
|
||||||
playlistRoute.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), Wrap(CreatePlaylist))
|
playlistRoute.Post("/", reqEditorRole, bind(models.CreatePlaylistCommand{}), Wrap(CreatePlaylist))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
@ -348,12 +348,12 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
apiRoute.Group("/alert-notifications", func(alertNotifications routing.RouteRegister) {
|
apiRoute.Group("/alert-notifications", func(alertNotifications routing.RouteRegister) {
|
||||||
alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), Wrap(NotificationTest))
|
alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), Wrap(NotificationTest))
|
||||||
alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), Wrap(CreateAlertNotification))
|
alertNotifications.Post("/", bind(models.CreateAlertNotificationCommand{}), Wrap(CreateAlertNotification))
|
||||||
alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), Wrap(UpdateAlertNotification))
|
alertNotifications.Put("/:notificationId", bind(models.UpdateAlertNotificationCommand{}), Wrap(UpdateAlertNotification))
|
||||||
alertNotifications.Get("/:notificationId", Wrap(GetAlertNotificationByID))
|
alertNotifications.Get("/:notificationId", Wrap(GetAlertNotificationByID))
|
||||||
alertNotifications.Delete("/:notificationId", Wrap(DeleteAlertNotification))
|
alertNotifications.Delete("/:notificationId", Wrap(DeleteAlertNotification))
|
||||||
alertNotifications.Get("/uid/:uid", Wrap(GetAlertNotificationByUID))
|
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))
|
alertNotifications.Delete("/uid/:uid", Wrap(DeleteAlertNotificationByUID))
|
||||||
}, reqEditorRole)
|
}, reqEditorRole)
|
||||||
|
|
||||||
@ -384,13 +384,13 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
adminRoute.Post("/users/:id/disable", Wrap(hs.AdminDisableUser))
|
adminRoute.Post("/users/:id/disable", Wrap(hs.AdminDisableUser))
|
||||||
adminRoute.Post("/users/:id/enable", Wrap(AdminEnableUser))
|
adminRoute.Post("/users/:id/enable", Wrap(AdminEnableUser))
|
||||||
adminRoute.Get("/users/:id/quotas", Wrap(GetUserQuotas))
|
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.Get("/stats", AdminGetStats)
|
||||||
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
|
adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
|
||||||
|
|
||||||
adminRoute.Post("/users/:id/logout", Wrap(hs.AdminLogoutUser))
|
adminRoute.Post("/users/:id/logout", Wrap(hs.AdminLogoutUser))
|
||||||
adminRoute.Get("/users/:id/auth-tokens", Wrap(hs.AdminGetUserAuthTokens))
|
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/dashboards/reload", Wrap(hs.AdminProvisioningReloadDasboards))
|
||||||
adminRoute.Post("/provisioning/datasources/reload", Wrap(hs.AdminProvisioningReloadDatasources))
|
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/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
"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 {
|
func GetAPIKeys(c *models.ReqContext) Response {
|
||||||
query := m.GetApiKeysQuery{OrgId: c.OrgId}
|
query := models.GetApiKeysQuery{OrgId: c.OrgId}
|
||||||
|
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
return Error(500, "Failed to list api keys", err)
|
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 {
|
for i, t := range query.Result {
|
||||||
result[i] = &m.ApiKeyDTO{
|
var expiration *time.Time = nil
|
||||||
|
if t.Expires != nil {
|
||||||
|
v := time.Unix(*t.Expires, 0)
|
||||||
|
expiration = &v
|
||||||
|
}
|
||||||
|
result[i] = &models.ApiKeyDTO{
|
||||||
Id: t.Id,
|
Id: t.Id,
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Role: t.Role,
|
Role: t.Role,
|
||||||
|
Expiration: expiration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON(200, result)
|
return JSON(200, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteAPIKey(c *m.ReqContext) Response {
|
func DeleteAPIKey(c *models.ReqContext) Response {
|
||||||
id := c.ParamsInt64(":id")
|
id := c.ParamsInt64(":id")
|
||||||
|
|
||||||
cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
|
cmd := &models.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
|
||||||
|
|
||||||
err := bus.Dispatch(cmd)
|
err := bus.Dispatch(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -39,17 +46,28 @@ func DeleteAPIKey(c *m.ReqContext) Response {
|
|||||||
return Success("API key deleted")
|
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() {
|
if !cmd.Role.IsValid() {
|
||||||
return Error(400, "Invalid role specified", nil)
|
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
|
cmd.OrgId = c.OrgId
|
||||||
|
|
||||||
newKeyInfo := apikeygen.New(cmd.OrgId, cmd.Name)
|
newKeyInfo := apikeygen.New(cmd.OrgId, cmd.Name)
|
||||||
cmd.Key = newKeyInfo.HashedKey
|
cmd.Key = newKeyInfo.HashedKey
|
||||||
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
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)
|
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/components/apikeygen"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
"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/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var getTime = time.Now
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ReqGrafanaAdmin = Auth(&AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
|
ReqGrafanaAdmin = Auth(&AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
|
||||||
ReqSignedIn = Auth(&AuthOptions{ReqSignedIn: true})
|
ReqSignedIn = Auth(&AuthOptions{ReqSignedIn: true})
|
||||||
ReqEditorRole = RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
|
ReqEditorRole = RoleAuth(models.ROLE_EDITOR, models.ROLE_ADMIN)
|
||||||
ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN)
|
ReqOrgAdmin = RoleAuth(models.ROLE_ADMIN)
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetContextHandler(
|
func GetContextHandler(
|
||||||
ats m.UserTokenService,
|
ats models.UserTokenService,
|
||||||
remoteCache *remotecache.RemoteCache,
|
remoteCache *remotecache.RemoteCache,
|
||||||
) macaron.Handler {
|
) macaron.Handler {
|
||||||
return func(c *macaron.Context) {
|
return func(c *macaron.Context) {
|
||||||
ctx := &m.ReqContext{
|
ctx := &models.ReqContext{
|
||||||
Context: c,
|
Context: c,
|
||||||
SignedInUser: &m.SignedInUser{},
|
SignedInUser: &models.SignedInUser{},
|
||||||
IsSignedIn: false,
|
IsSignedIn: false,
|
||||||
AllowAnonymous: false,
|
AllowAnonymous: false,
|
||||||
SkipCache: false,
|
SkipCache: false,
|
||||||
@ -68,19 +70,19 @@ func GetContextHandler(
|
|||||||
// update last seen every 5min
|
// update last seen every 5min
|
||||||
if ctx.ShouldUpdateLastSeenAt() {
|
if ctx.ShouldUpdateLastSeenAt() {
|
||||||
ctx.Logger.Debug("Updating last user_seen_at", "user_id", ctx.UserId)
|
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)
|
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 {
|
if !setting.AnonymousEnabled {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
orgQuery := m.GetOrgByNameQuery{Name: setting.AnonymousOrgName}
|
orgQuery := models.GetOrgByNameQuery{Name: setting.AnonymousOrgName}
|
||||||
if err := bus.Dispatch(&orgQuery); err != nil {
|
if err := bus.Dispatch(&orgQuery); err != nil {
|
||||||
log.Error(3, "Anonymous access organization error: '%s': %s", setting.AnonymousOrgName, err)
|
log.Error(3, "Anonymous access organization error: '%s': %s", setting.AnonymousOrgName, err)
|
||||||
return false
|
return false
|
||||||
@ -88,14 +90,14 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool {
|
|||||||
|
|
||||||
ctx.IsSignedIn = false
|
ctx.IsSignedIn = false
|
||||||
ctx.AllowAnonymous = true
|
ctx.AllowAnonymous = true
|
||||||
ctx.SignedInUser = &m.SignedInUser{IsAnonymous: true}
|
ctx.SignedInUser = &models.SignedInUser{IsAnonymous: true}
|
||||||
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
|
ctx.OrgRole = models.RoleType(setting.AnonymousOrgRole)
|
||||||
ctx.OrgId = orgQuery.Result.Id
|
ctx.OrgId = orgQuery.Result.Id
|
||||||
ctx.OrgName = orgQuery.Result.Name
|
ctx.OrgName = orgQuery.Result.Name
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func initContextWithApiKey(ctx *m.ReqContext) bool {
|
func initContextWithApiKey(ctx *models.ReqContext) bool {
|
||||||
var keyString string
|
var keyString string
|
||||||
if keyString = getApiKey(ctx); keyString == "" {
|
if keyString = getApiKey(ctx); keyString == "" {
|
||||||
return false
|
return false
|
||||||
@ -109,7 +111,7 @@ func initContextWithApiKey(ctx *m.ReqContext) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch key
|
// 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 {
|
if err := bus.Dispatch(&keyQuery); err != nil {
|
||||||
ctx.JsonApiErr(401, "Invalid API key", err)
|
ctx.JsonApiErr(401, "Invalid API key", err)
|
||||||
return true
|
return true
|
||||||
@ -123,15 +125,21 @@ func initContextWithApiKey(ctx *m.ReqContext) bool {
|
|||||||
return true
|
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.IsSignedIn = true
|
||||||
ctx.SignedInUser = &m.SignedInUser{}
|
ctx.SignedInUser = &models.SignedInUser{}
|
||||||
ctx.OrgRole = apikey.Role
|
ctx.OrgRole = apikey.Role
|
||||||
ctx.ApiKeyId = apikey.Id
|
ctx.ApiKeyId = apikey.Id
|
||||||
ctx.OrgId = apikey.OrgId
|
ctx.OrgId = apikey.OrgId
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
func initContextWithBasicAuth(ctx *models.ReqContext, orgId int64) bool {
|
||||||
|
|
||||||
if !setting.BasicAuthEnabled {
|
if !setting.BasicAuthEnabled {
|
||||||
return false
|
return false
|
||||||
@ -148,7 +156,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
loginQuery := m.GetUserByLoginQuery{LoginOrEmail: username}
|
loginQuery := models.GetUserByLoginQuery{LoginOrEmail: username}
|
||||||
if err := bus.Dispatch(&loginQuery); err != nil {
|
if err := bus.Dispatch(&loginQuery); err != nil {
|
||||||
ctx.JsonApiErr(401, "Basic auth failed", err)
|
ctx.JsonApiErr(401, "Basic auth failed", err)
|
||||||
return true
|
return true
|
||||||
@ -156,13 +164,13 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
|||||||
|
|
||||||
user := loginQuery.Result
|
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 {
|
if err := bus.Dispatch(&loginUserQuery); err != nil {
|
||||||
ctx.JsonApiErr(401, "Invalid username or password", err)
|
ctx.JsonApiErr(401, "Invalid username or password", err)
|
||||||
return true
|
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 {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
ctx.JsonApiErr(401, "Authentication error", err)
|
ctx.JsonApiErr(401, "Authentication error", err)
|
||||||
return true
|
return true
|
||||||
@ -173,7 +181,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
|||||||
return true
|
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)
|
rawToken := ctx.GetCookie(setting.LoginCookieName)
|
||||||
if rawToken == "" {
|
if rawToken == "" {
|
||||||
return false
|
return false
|
||||||
@ -186,7 +194,7 @@ func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext
|
|||||||
return false
|
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 {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
ctx.Logger.Error("failed to get user with id", "userId", token.UserId, "error", err)
|
ctx.Logger.Error("failed to get user with id", "userId", token.UserId, "error", err)
|
||||||
return false
|
return false
|
||||||
@ -209,7 +217,7 @@ func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext
|
|||||||
return true
|
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 {
|
if setting.Env == setting.DEV {
|
||||||
ctx.Logger.Info("new token", "unhashed token", value)
|
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/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
"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/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@ -21,6 +21,19 @@ import (
|
|||||||
"gopkg.in/macaron.v1"
|
"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) {
|
func TestMiddleWareSecurityHeaders(t *testing.T) {
|
||||||
setting.ERR_TEMPLATE_NAME = "error-template"
|
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) {
|
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{
|
data := &dtos.IndexViewData{
|
||||||
User: &dtos.CurrentUser{},
|
User: &dtos.CurrentUser{},
|
||||||
Settings: map[string]interface{}{},
|
Settings: map[string]interface{}{},
|
||||||
@ -125,20 +138,20 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
|
|
||||||
middlewareScenario(t, "Using basic auth", func(sc *scenarioContext) {
|
middlewareScenario(t, "Using basic auth", func(sc *scenarioContext) {
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
|
bus.AddHandler("test", func(query *models.GetUserByLoginQuery) error {
|
||||||
query.Result = &m.User{
|
query.Result = &models.User{
|
||||||
Password: util.EncodePassword("myPass", "salt"),
|
Password: util.EncodePassword("myPass", "salt"),
|
||||||
Salt: "salt",
|
Salt: "salt",
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(loginUserQuery *m.LoginUserQuery) error {
|
bus.AddHandler("test", func(loginUserQuery *models.LoginUserQuery) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -156,8 +169,8 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
middlewareScenario(t, "Valid api key", func(sc *scenarioContext) {
|
middlewareScenario(t, "Valid api key", func(sc *scenarioContext) {
|
||||||
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
|
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
|
||||||
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
|
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -170,15 +183,15 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
Convey("Should init middleware context", func() {
|
Convey("Should init middleware context", func() {
|
||||||
So(sc.context.IsSignedIn, ShouldEqual, true)
|
So(sc.context.IsSignedIn, ShouldEqual, true)
|
||||||
So(sc.context.OrgId, ShouldEqual, 12)
|
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) {
|
middlewareScenario(t, "Valid api key, but does not match db hash", func(sc *scenarioContext) {
|
||||||
keyhash := "something_not_matching"
|
keyhash := "something_not_matching"
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
|
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
|
||||||
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
|
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
|
||||||
return nil
|
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) {
|
middlewareScenario(t, "Valid api key via Basic auth", func(sc *scenarioContext) {
|
||||||
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
|
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
|
||||||
query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
|
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -208,20 +244,20 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
Convey("Should init middleware context", func() {
|
Convey("Should init middleware context", func() {
|
||||||
So(sc.context.IsSignedIn, ShouldEqual, true)
|
So(sc.context.IsSignedIn, ShouldEqual, true)
|
||||||
So(sc.context.OrgId, ShouldEqual, 12)
|
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) {
|
middlewareScenario(t, "Non-expired auth token in cookie which not are being rotated", func(sc *scenarioContext) {
|
||||||
sc.withTokenSessionCookie("token")
|
sc.withTokenSessionCookie("token")
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) {
|
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
|
||||||
return &m.UserToken{
|
return &models.UserToken{
|
||||||
UserId: 12,
|
UserId: 12,
|
||||||
UnhashedToken: unhashedToken,
|
UnhashedToken: unhashedToken,
|
||||||
}, nil
|
}, 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) {
|
middlewareScenario(t, "Non-expired auth token in cookie which are being rotated", func(sc *scenarioContext) {
|
||||||
sc.withTokenSessionCookie("token")
|
sc.withTokenSessionCookie("token")
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) {
|
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
|
||||||
return &m.UserToken{
|
return &models.UserToken{
|
||||||
UserId: 12,
|
UserId: 12,
|
||||||
UnhashedToken: "",
|
UnhashedToken: "",
|
||||||
}, nil
|
}, 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"
|
userToken.UnhashedToken = "rotated"
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
@ -291,8 +327,8 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
middlewareScenario(t, "Invalid/expired auth token in cookie", func(sc *scenarioContext) {
|
middlewareScenario(t, "Invalid/expired auth token in cookie", func(sc *scenarioContext) {
|
||||||
sc.withTokenSessionCookie("token")
|
sc.withTokenSessionCookie("token")
|
||||||
|
|
||||||
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) {
|
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
|
||||||
return nil, m.ErrUserTokenNotFound
|
return nil, models.ErrUserTokenNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
sc.fakeReq("GET", "/").exec()
|
sc.fakeReq("GET", "/").exec()
|
||||||
@ -307,12 +343,12 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
middlewareScenario(t, "When anonymous access is enabled", func(sc *scenarioContext) {
|
middlewareScenario(t, "When anonymous access is enabled", func(sc *scenarioContext) {
|
||||||
setting.AnonymousEnabled = true
|
setting.AnonymousEnabled = true
|
||||||
setting.AnonymousOrgName = "test"
|
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")
|
So(query.Name, ShouldEqual, "test")
|
||||||
|
|
||||||
query.Result = &m.Org{Id: 2, Name: "test"}
|
query.Result = &models.Org{Id: 2, Name: "test"}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -321,7 +357,7 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
Convey("should init context with org info", func() {
|
Convey("should init context with org info", func() {
|
||||||
So(sc.context.UserId, ShouldEqual, 0)
|
So(sc.context.UserId, ShouldEqual, 0)
|
||||||
So(sc.context.OrgId, ShouldEqual, 2)
|
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() {
|
Convey("context signed in should be false", func() {
|
||||||
@ -339,8 +375,8 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
name := "markelog"
|
name := "markelog"
|
||||||
|
|
||||||
middlewareScenario(t, "should not sync the user if it's in the cache", func(sc *scenarioContext) {
|
middlewareScenario(t, "should not sync the user if it's in the cache", func(sc *scenarioContext) {
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: query.UserId}
|
query.Result = &models.SignedInUser{OrgId: 4, UserId: query.UserId}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -362,16 +398,16 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
setting.LDAPEnabled = false
|
setting.LDAPEnabled = false
|
||||||
setting.AuthProxyAutoSignUp = true
|
setting.AuthProxyAutoSignUp = true
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||||
if query.UserId > 0 {
|
if query.UserId > 0 {
|
||||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return m.ErrUserNotFound
|
return models.ErrUserNotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||||
cmd.Result = &m.User{Id: 33}
|
cmd.Result = &models.User{Id: 33}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -389,13 +425,13 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
middlewareScenario(t, "should get an existing user from header", func(sc *scenarioContext) {
|
middlewareScenario(t, "should get an existing user from header", func(sc *scenarioContext) {
|
||||||
setting.LDAPEnabled = false
|
setting.LDAPEnabled = false
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||||
cmd.Result = &m.User{Id: 12}
|
cmd.Result = &models.User{Id: 12}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -414,13 +450,13 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
|
setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
|
||||||
setting.LDAPEnabled = false
|
setting.LDAPEnabled = false
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||||
cmd.Result = &m.User{Id: 33}
|
cmd.Result = &models.User{Id: 33}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -440,13 +476,13 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
setting.AuthProxyWhitelist = "8.8.8.8"
|
setting.AuthProxyWhitelist = "8.8.8.8"
|
||||||
setting.LDAPEnabled = false
|
setting.LDAPEnabled = false
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||||
cmd.Result = &m.User{Id: 33}
|
cmd.Result = &models.User{Id: 33}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -489,7 +525,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
|
|||||||
|
|
||||||
sc.m.Use(OrgRedirect())
|
sc.m.Use(OrgRedirect())
|
||||||
|
|
||||||
sc.defaultHandler = func(c *m.ReqContext) {
|
sc.defaultHandler = func(c *models.ReqContext) {
|
||||||
sc.context = c
|
sc.context = c
|
||||||
if sc.handlerFunc != nil {
|
if sc.handlerFunc != nil {
|
||||||
sc.handlerFunc(sc.context)
|
sc.handlerFunc(sc.context)
|
||||||
@ -504,7 +540,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
|
|||||||
|
|
||||||
type scenarioContext struct {
|
type scenarioContext struct {
|
||||||
m *macaron.Macaron
|
m *macaron.Macaron
|
||||||
context *m.ReqContext
|
context *models.ReqContext
|
||||||
resp *httptest.ResponseRecorder
|
resp *httptest.ResponseRecorder
|
||||||
apiKey string
|
apiKey string
|
||||||
authHeader string
|
authHeader string
|
||||||
@ -587,4 +623,4 @@ func (sc *scenarioContext) exec() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type scenarioFunc func(c *scenarioContext)
|
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 ErrInvalidApiKey = errors.New("Invalid API Key")
|
||||||
|
var ErrInvalidApiKeyExpiration = errors.New("Negative value for SecondsToLive")
|
||||||
|
|
||||||
type ApiKey struct {
|
type ApiKey struct {
|
||||||
Id int64
|
Id int64
|
||||||
@ -15,6 +16,7 @@ type ApiKey struct {
|
|||||||
Role RoleType
|
Role RoleType
|
||||||
Created time.Time
|
Created time.Time
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
|
Expires *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------
|
// ---------------------
|
||||||
@ -24,6 +26,7 @@ type AddApiKeyCommand struct {
|
|||||||
Role RoleType `json:"role" binding:"Required"`
|
Role RoleType `json:"role" binding:"Required"`
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Key string `json:"-"`
|
Key string `json:"-"`
|
||||||
|
SecondsToLive int64 `json:"secondsToLive"`
|
||||||
|
|
||||||
Result *ApiKey `json:"-"`
|
Result *ApiKey `json:"-"`
|
||||||
}
|
}
|
||||||
@ -46,6 +49,7 @@ type DeleteApiKeyCommand struct {
|
|||||||
|
|
||||||
type GetApiKeysQuery struct {
|
type GetApiKeysQuery struct {
|
||||||
OrgId int64
|
OrgId int64
|
||||||
|
IncludeInvalid bool
|
||||||
Result []*ApiKey
|
Result []*ApiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,4 +71,5 @@ type ApiKeyDTO struct {
|
|||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Role RoleType `json:"role"`
|
Role RoleType `json:"role"`
|
||||||
|
Expiration *time.Time `json:"expiration,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -16,14 +16,18 @@ func init() {
|
|||||||
bus.AddHandler("sql", AddApiKey)
|
bus.AddHandler("sql", AddApiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetApiKeys(query *m.GetApiKeysQuery) error {
|
func GetApiKeys(query *models.GetApiKeysQuery) error {
|
||||||
sess := x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name")
|
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)
|
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 {
|
return withDbSession(ctx, func(sess *DBSession) error {
|
||||||
var rawSql = "DELETE FROM api_key WHERE id=? and org_id=?"
|
var rawSql = "DELETE FROM api_key WHERE id=? and org_id=?"
|
||||||
_, err := sess.Exec(rawSql, cmd.Id, cmd.OrgId)
|
_, 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 {
|
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,
|
OrgId: cmd.OrgId,
|
||||||
Name: cmd.Name,
|
Name: cmd.Name,
|
||||||
Role: cmd.Role,
|
Role: cmd.Role,
|
||||||
Key: cmd.Key,
|
Key: cmd.Key,
|
||||||
Created: time.Now(),
|
Created: updated,
|
||||||
Updated: time.Now(),
|
Updated: updated,
|
||||||
|
Expires: expires,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := sess.Insert(&t); err != nil {
|
if _, err := sess.Insert(&t); err != nil {
|
||||||
@ -50,28 +63,28 @@ func AddApiKey(cmd *m.AddApiKeyCommand) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
|
func GetApiKeyById(query *models.GetApiKeyByIdQuery) error {
|
||||||
var apikey m.ApiKey
|
var apikey models.ApiKey
|
||||||
has, err := x.Id(query.ApiKeyId).Get(&apikey)
|
has, err := x.Id(query.ApiKeyId).Get(&apikey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return m.ErrInvalidApiKey
|
return models.ErrInvalidApiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
query.Result = &apikey
|
query.Result = &apikey
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
|
func GetApiKeyByName(query *models.GetApiKeyByNameQuery) error {
|
||||||
var apikey m.ApiKey
|
var apikey models.ApiKey
|
||||||
has, err := x.Where("org_id=? AND name=?", query.OrgId, query.KeyName).Get(&apikey)
|
has, err := x.Where("org_id=? AND name=?", query.OrgId, query.KeyName).Get(&apikey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return m.ErrInvalidApiKey
|
return models.ErrInvalidApiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
query.Result = &apikey
|
query.Result = &apikey
|
||||||
|
@ -1,31 +1,117 @@
|
|||||||
package sqlstore
|
package sqlstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
|
||||||
|
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestApiKeyDataAccess(t *testing.T) {
|
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)
|
InitTestDB(t)
|
||||||
|
|
||||||
Convey("Given saved api key", func() {
|
t.Run("Given saved api key", func(t *testing.T) {
|
||||||
cmd := m.AddApiKeyCommand{OrgId: 1, Name: "hello", Key: "asd"}
|
cmd := models.AddApiKeyCommand{OrgId: 1, Name: "hello", Key: "asd"}
|
||||||
err := AddApiKey(&cmd)
|
err := AddApiKey(&cmd)
|
||||||
So(err, ShouldBeNil)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
Convey("Should be able to get key by name", func() {
|
t.Run("Should be able to get key by name", func(t *testing.T) {
|
||||||
query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
|
query := models.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
|
||||||
err = GetApiKeyByName(&query)
|
err = GetApiKeyByName(&query)
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
assert.Nil(t, err)
|
||||||
So(query.Result, ShouldNotBeNil)
|
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: "key", Type: DB_Varchar, Length: 190, Nullable: false},
|
||||||
{Name: "role", Type: DB_NVarchar, Length: 255, 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
|
RemoteCacheOptions *RemoteCacheOptions
|
||||||
|
|
||||||
EditorsCanAdmin bool
|
EditorsCanAdmin bool
|
||||||
|
|
||||||
|
ApiKeyMaxSecondsToLive int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
@ -795,6 +797,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
|
|
||||||
LoginMaxLifetimeDays = auth.Key("login_maximum_lifetime_days").MustInt(30)
|
LoginMaxLifetimeDays = auth.Key("login_maximum_lifetime_days").MustInt(30)
|
||||||
cfg.LoginMaxLifetimeDays = LoginMaxLifetimeDays
|
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)
|
cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10)
|
||||||
if cfg.TokenRotationIntervalMinutes < 2 {
|
if cfg.TokenRotationIntervalMinutes < 2 {
|
||||||
|
@ -12,9 +12,34 @@ import ApiKeysAddedModal from './ApiKeysAddedModal';
|
|||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
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 { NavModel } from '@grafana/data';
|
||||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
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 {
|
export interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
@ -36,13 +61,18 @@ export interface State {
|
|||||||
enum ApiKeyStateProps {
|
enum ApiKeyStateProps {
|
||||||
Name = 'name',
|
Name = 'name',
|
||||||
Role = 'role',
|
Role = 'role',
|
||||||
|
SecondsToLive = 'secondsToLive',
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialApiKeyState = {
|
const initialApiKeyState = {
|
||||||
name: '',
|
name: '',
|
||||||
role: OrgRole.Viewer,
|
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> {
|
export class ApiKeysPage extends PureComponent<Props, any> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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.props.addApiKey(this.state.newApiKey, openModal);
|
||||||
this.setState((prevState: State) => {
|
this.setState((prevState: State) => {
|
||||||
return {
|
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() {
|
renderAddApiKeyForm() {
|
||||||
const { newApiKey, isAdding } = this.state;
|
const { newApiKey, isAdding } = this.state;
|
||||||
|
|
||||||
@ -170,6 +214,17 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
|||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div className="gf-form">
|
||||||
<button className="btn gf-form-btn btn-primary">Add</button>
|
<button className="btn gf-form-btn btn-primary">Add</button>
|
||||||
</div>
|
</div>
|
||||||
@ -211,6 +266,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
|
<th>Expires</th>
|
||||||
<th style={{ width: '34px' }} />
|
<th style={{ width: '34px' }} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -221,6 +277,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
|||||||
<tr key={key.id}>
|
<tr key={key.id}>
|
||||||
<td>{key.name}</td>
|
<td>{key.name}</td>
|
||||||
<td>{key.role}</td>
|
<td>{key.role}</td>
|
||||||
|
<td>{this.formatDate(key.expiration)}</td>
|
||||||
<td>
|
<td>
|
||||||
<DeleteButton onConfirm={() => this.onDeleteApiKey(key)} />
|
<DeleteButton onConfirm={() => this.onDeleteApiKey(key)} />
|
||||||
</td>
|
</td>
|
||||||
|
@ -7,6 +7,8 @@ export const getMultipleMockKeys = (numberOfKeys: number): ApiKey[] => {
|
|||||||
id: i,
|
id: i,
|
||||||
name: `test-${i}`,
|
name: `test-${i}`,
|
||||||
role: OrgRole.Viewer,
|
role: OrgRole.Viewer,
|
||||||
|
secondsToLive: null,
|
||||||
|
expiration: '2019-06-04',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,5 +20,7 @@ export const getMockKey = (): ApiKey => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'test',
|
name: 'test',
|
||||||
role: OrgRole.Admin,
|
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>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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
|
<div
|
||||||
className="gf-form"
|
className="gf-form"
|
||||||
>
|
>
|
||||||
|
@ -4,11 +4,14 @@ export interface ApiKey {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
role: OrgRole;
|
role: OrgRole;
|
||||||
|
secondsToLive: number;
|
||||||
|
expiration: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NewApiKey {
|
export interface NewApiKey {
|
||||||
name: string;
|
name: string;
|
||||||
role: OrgRole;
|
role: OrgRole;
|
||||||
|
secondsToLive: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiKeysState {
|
export interface ApiKeysState {
|
||||||
|
Loading…
Reference in New Issue
Block a user