ServiceAccounts: Add Service Account Token last used at date (#51446)

* ServiceAccounts Add api key last used at

* ServiceAccounts: LastUpdateAt tests
This commit is contained in:
Jguer 2022-06-28 14:42:40 +00:00 committed by GitHub
parent daf0e3cb4e
commit 6d0261263c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 60 additions and 0 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -55,6 +55,7 @@ func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, s
Created: updated,
Updated: updated,
Expires: expires,
LastUsedAt: nil,
ServiceAccountId: &serviceAccountId,
}

View File

@ -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
})
}

View File

@ -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}

View File

@ -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,
}))
}

View File

@ -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
}

View File

@ -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

View File

@ -23,6 +23,7 @@ export const ServiceAccountTokensTable = ({ tokens, timeZone, tokenActionsDisabl
<th>Name</th>
<th>Expires</th>
<th>Created</th>
<th>Last used at</th>
<th />
</tr>
</thead>
@ -35,6 +36,7 @@ export const ServiceAccountTokensTable = ({ tokens, timeZone, tokenActionsDisabl
<TokenExpiration timeZone={timeZone} token={key} />
</td>
<td>{formatDate(timeZone, key.created)}</td>
<td>{formatLastUsedAtDate(timeZone, key.lastUsedAt)}</td>
<td>
<DeleteButton
aria-label={`Delete service account token ${key.name}`}
@ -51,6 +53,13 @@ export const ServiceAccountTokensTable = ({ tokens, timeZone, tokenActionsDisabl
);
};
function formatLastUsedAtDate(timeZone: TimeZone, lastUsedAt?: string): string {
if (!lastUsedAt) {
return 'Never';
}
return dateTimeFormat(lastUsedAt, { timeZone });
}
function formatDate(timeZone: TimeZone, expiration?: string): string {
if (!expiration) {
return 'No expiration date';

View File

@ -11,6 +11,7 @@ export interface ApiKey extends WithAccessControlMetadata {
secondsUntilExpiration?: number;
hasExpired?: boolean;
created?: string;
lastUsedAt?: string;
}
export interface NewApiKey {