diff --git a/pkg/models/apikey.go b/pkg/models/apikey.go index 9085189b777..462238a3d13 100644 --- a/pkg/models/apikey.go +++ b/pkg/models/apikey.go @@ -20,6 +20,7 @@ type ApiKey struct { Role RoleType Created time.Time Updated time.Time + LastUsedAt *time.Time `xorm:"last_used_at"` Expires *int64 ServiceAccountId *int64 } diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 35aed0664ba..7794fcfcaba 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -272,6 +272,12 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo return true } + // update api_key last used date + if err := h.SQLStore.UpdateAPIKeyLastUsedDate(reqContext.Req.Context(), apikey.Id); err != nil { + reqContext.JsonApiErr(http.StatusInternalServerError, InvalidAPIKey, errKey) + return true + } + if apikey.ServiceAccountId == nil || *apikey.ServiceAccountId < 1 { //There is no service account attached to the apikey //Use the old APIkey method. This provides backwards compatibility. reqContext.SignedInUser = &models.SignedInUser{} @@ -305,6 +311,7 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo reqContext.IsSignedIn = true reqContext.SignedInUser = querySignedInUser.Result + return true } diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index d154983b044..263e16b6e5f 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -24,6 +24,7 @@ type TokenDTO struct { Id int64 `json:"id"` Name string `json:"name"` Created *time.Time `json:"created"` + LastUsedAt *time.Time `json:"lastUsedAt"` Expiration *time.Time `json:"expiration"` SecondsUntilExpiration *float64 `json:"secondsUntilExpiration"` HasExpired bool `json:"hasExpired"` @@ -72,6 +73,7 @@ func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Respo Expiration: expiration, SecondsUntilExpiration: &secondsUntilExpiration, HasExpired: isExpired, + LastUsedAt: t.LastUsedAt, } } diff --git a/pkg/services/serviceaccounts/database/token_store.go b/pkg/services/serviceaccounts/database/token_store.go index e777f16078c..13bea20fcd0 100644 --- a/pkg/services/serviceaccounts/database/token_store.go +++ b/pkg/services/serviceaccounts/database/token_store.go @@ -55,6 +55,7 @@ func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, s Created: updated, Updated: updated, Expires: expires, + LastUsedAt: nil, ServiceAccountId: &serviceAccountId, } diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index aa303778a12..f9730cde6ae 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -163,3 +163,15 @@ func (ss *SQLStore) GetAPIKeyByHash(ctx context.Context, hash string) (*models.A return &apikey, err } + +// UpdateAPIKeyLastUsedDate updates the last used date of the API key to current time. +func (ss *SQLStore) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error { + now := timeNow() + return ss.WithDbSession(ctx, func(sess *DBSession) error { + if _, err := sess.Table("api_key").ID(tokenID).Cols("last_used_at").Update(&models.ApiKey{LastUsedAt: &now}); err != nil { + return err + } + + return nil + }) +} diff --git a/pkg/services/sqlstore/apikey_test.go b/pkg/services/sqlstore/apikey_test.go index 260f319ecda..64d75d37707 100644 --- a/pkg/services/sqlstore/apikey_test.go +++ b/pkg/services/sqlstore/apikey_test.go @@ -76,6 +76,24 @@ func TestIntegrationApiKeyDataAccess(t *testing.T) { assert.Equal(t, *query.Result.Expires, expected) }) + t.Run("Last Used At datetime update", func(t *testing.T) { + // expires in one hour + cmd := models.AddApiKeyCommand{OrgId: 1, Name: "last-update-at", Key: "asd3", SecondsToLive: 3600} + err := ss.AddAPIKey(context.Background(), &cmd) + require.NoError(t, err) + + assert.Nil(t, cmd.Result.LastUsedAt) + + err = ss.UpdateAPIKeyLastUsedDate(context.Background(), cmd.Result.Id) + require.NoError(t, err) + + query := models.GetApiKeyByNameQuery{KeyName: "last-update-at", OrgId: 1} + err = ss.GetApiKeyByName(context.Background(), &query) + assert.Nil(t, err) + + assert.NotNil(t, query.Result.LastUsedAt) + }) + 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} diff --git a/pkg/services/sqlstore/migrations/apikey_mig.go b/pkg/services/sqlstore/migrations/apikey_mig.go index 5e33b0e941d..a681256de24 100644 --- a/pkg/services/sqlstore/migrations/apikey_mig.go +++ b/pkg/services/sqlstore/migrations/apikey_mig.go @@ -91,4 +91,8 @@ func addApiKeyMigrations(mg *Migrator) { mg.AddMigration("set service account foreign key to nil if 0", NewRawSQLMigration( "UPDATE api_key SET service_account_id = NULL WHERE service_account_id = 0;")) + + mg.AddMigration("Add last_used_at to api_key table", NewAddColumnMigration(apiKeyV2, &Column{ + Name: "last_used_at", Type: DB_DateTime, Nullable: true, + })) } diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 55f8b1e7668..90d8206b2a8 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -543,6 +543,10 @@ func (m *SQLStoreMock) GetApiKeyByName(ctx context.Context, query *models.GetApi return m.ExpectedError } +func (m *SQLStoreMock) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error { + return nil +} + func (m *SQLStoreMock) UpdateTempUserStatus(ctx context.Context, cmd *models.UpdateTempUserStatusCommand) error { return m.ExpectedError } diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index 1dedae4f695..55bc7f85a3d 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -121,6 +121,7 @@ type Store interface { GetApiKeyById(ctx context.Context, query *models.GetApiKeyByIdQuery) error GetApiKeyByName(ctx context.Context, query *models.GetApiKeyByNameQuery) error GetAPIKeyByHash(ctx context.Context, hash string) (*models.ApiKey, error) + UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error UpdateTempUserStatus(ctx context.Context, cmd *models.UpdateTempUserStatusCommand) error CreateTempUser(ctx context.Context, cmd *models.CreateTempUserCommand) error UpdateTempUserWithEmailSent(ctx context.Context, cmd *models.UpdateTempUserWithEmailSentCommand) error diff --git a/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx b/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx index 580e465daa2..8f26776dc4d 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx @@ -23,6 +23,7 @@ export const ServiceAccountTokensTable = ({ tokens, timeZone, tokenActionsDisabl Name Expires Created + Last used at @@ -35,6 +36,7 @@ export const ServiceAccountTokensTable = ({ tokens, timeZone, tokenActionsDisabl {formatDate(timeZone, key.created)} + {formatLastUsedAtDate(timeZone, key.lastUsedAt)}