Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api-description/web-api.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8768,6 +8768,9 @@ definitions:
type: string
environmentName:
type: string
lastUsedAt:
type: string
format: int64
accountAPIKeyRole:
type: string
enum:
Expand Down Expand Up @@ -9335,6 +9338,8 @@ definitions:
required:
- changeType
- team
accountUpdateAPIKeyLastUsedAtResponse:
type: object
accountUpdateAPIKeyRequest:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE api_key ADD COLUMN last_used_at bigint DEFAULT 0;
3 changes: 2 additions & 1 deletion migration/mysql/atlas.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
h1:sbwKrn+UjmZchqTeIaRqq00fxno8oq6vba/UVm6GN8A=
h1:jLGSNaAAAnRJ8v7ZLH7JHqjFe8HPv3wXCLetHRB/RXg=
20240626022133_initialization.sql h1:reSmqMhqnsrdIdPU2ezv/PXSL0COlRFX4gQA4U3/wMo=
20240708065726_update_audit_log_table.sql h1:fi8Xxw4WfSlHDyvq2Ni/8JUiZW8z/0qWWyWm6jFdUy8=
20240815043128_update_auto_ops_rule_table.sql h1:IKSW9W/XO6SWAYl5WPLJSg6KdsfcZ3rfQhIrf7aOnYc=
Expand Down Expand Up @@ -32,3 +32,4 @@ h1:sbwKrn+UjmZchqTeIaRqq00fxno8oq6vba/UVm6GN8A=
20250411093510_update_audit_log_table.sql h1:KDbGMLbeKhtUc833pAvKOiw9JdM4rXSUVc34TOywvc8=
20250618071930_create_team_table.sql h1:Gc0+XyVk0zM+wWdWfqtkox2WQEnzftNX3FR7qpEAioU=
20250711131250_update_indexes.sql h1:ghjrhD9mDtCHaHL7HbiAkTjmRHp6EZ3eerVjb8cSZCA=
20251028072327_update_last_used_at_api_key_table.sql h1:cuC7b8RV6QPv2LKij2Ld1cqkH8tpQB85Y+J8fHbi+38=
59 changes: 59 additions & 0 deletions pkg/account/api/api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -773,3 +773,62 @@ func (s *AccountService) UpdateAPIKey(

return &proto.UpdateAPIKeyResponse{}, nil
}

func (s *AccountService) UpdateAPIKeyLastUsedAt(
ctx context.Context,
req *proto.UpdateAPIKeyLastUsedAtRequest,
) (*proto.UpdateAPIKeyLastUsedAtResponse, error) {
// No need to check roles since this is only allowed to be called internally by other services.
err := validateUpdateAPIKeyLastUsedAtRequest(req)
if err != nil {
s.logger.Error("Invalid UpdateAPIKeyLastUsedAt request",
log.FieldsFromIncomingContext(ctx).AddFields(
zap.Error(err),
zap.String("apiKeyId", req.ApiKeyId),
zap.Int64("lastUsedAt", req.LastUsedAt),
)...)
return nil, err
}

err = s.mysqlClient.RunInTransactionV2(ctx, func(contextWithTx context.Context, _ mysql.Transaction) error {
envAPIKey, err := s.accountStorage.GetEnvironmentAPIKey(contextWithTx, req.ApiKeyId)
if err != nil {
return err
}
apiKey := &domain.APIKey{
APIKey: envAPIKey.ApiKey,
}
err = apiKey.SetUsedAt(req.LastUsedAt)
if err != nil {
return err
}
return s.accountStorage.UpdateAPIKey(contextWithTx, apiKey, envAPIKey.Environment.Id)
})
if err != nil {
if errors.Is(err, v2as.ErrAPIKeyNotFound) {
return nil, statusNotFound.Err()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statusNotFound.Err() is an account not found error, not api key. This could be confusing when debugging.
Please add a new one statusAPIKeyNotFound.

err.NewErrorNotFound(
	err.AccountPackageName,
	"api key not found",
	"api_key_id",
)

Reference: https://github.com/bucketeer-io/bucketeer/blob/main/pkg/account/api/error.go#L41

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code, the storage interface is already formatting the not found error, so we just need to use this: return nil, api.NewGRPCStatus(err).Err()

Reference: https://github.com/bucketeer-io/bucketeer/blob/main/pkg/account/storage/v2/api_key.go#L31C2-L31C19

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the GetAPIKey, not the Update interface. In the end, we need to implement it in the storage interface, the not-found check.

Reference:
https://github.com/bucketeer-io/bucketeer/blob/main/pkg/account/storage/v2/api_key.go#L94-L103

}
s.logger.Error(
"Failed to update api key last used at",
log.FieldsFromIncomingContext(ctx).AddFields(
zap.Error(err),
zap.String("id", req.ApiKeyId),
)...,
)
return nil, api.NewGRPCStatus(err).Err()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an inconsistency here.
When the error is not found, the localizer logic is used. Since this API is for internal use only, we don't need to use the localizer. It's also deprecated.
WDYT?

https://github.com/bucketeer-io/bucketeer/pull/2175/files#diff-1236610833a324b8af6d6273437bdeb2fb35012b5ae3b3374760927a8427c204R813-R815

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, by passing the err without formatting it, an unknown error will be generated.
https://github.com/bucketeer-io/bucketeer/blob/main/pkg/api/api/grpc_status.go#L41

Since we already have the statusInternal, let's use it.

Reference: https://github.com/bucketeer-io/bucketeer/blob/main/pkg/account/storage/v2/api_key.go#L94-L103

}
return &proto.UpdateAPIKeyLastUsedAtResponse{}, nil
}

func validateUpdateAPIKeyLastUsedAtRequest(
req *proto.UpdateAPIKeyLastUsedAtRequest,
) error {
if req.ApiKeyId == "" {
return statusMissingAPIKeyID.Err()
}

if req.LastUsedAt <= 0 {
return statusInvalidLastUsedAt.Err()
}
return nil
}
1 change: 1 addition & 0 deletions pkg/account/api/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ var (
statusSearchFilterIDIsEmpty = api.NewGRPCStatus(pkgErr.NewErrorInvalidArgEmpty(pkgErr.AccountPackageName, "search filter ID is empty", "search_filter_ID"))
statusSearchFilterIDNotFound = api.NewGRPCStatus(pkgErr.NewErrorNotFound(pkgErr.AccountPackageName, "search filter not found", "search_filter"))
statusInvalidListAPIKeyRequest = api.NewGRPCStatus(pkgErr.NewErrorInvalidArgEmpty(pkgErr.AccountPackageName, "invalid list api key request", "list_api_key_request"))
statusInvalidLastUsedAt = api.NewGRPCStatus(pkgErr.NewErrorInvalidArgNotMatchFormat(pkgErr.AccountPackageName, "last used at is invalid", "last_used_at"))
)
20 changes: 20 additions & 0 deletions pkg/account/client/mock/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions pkg/account/domain/api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,19 @@ import (
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
"github.com/jinzhu/copier"

pkgErr "github.com/bucketeer-io/bucketeer/v2/pkg/error"
proto "github.com/bucketeer-io/bucketeer/v2/proto/account"
)

const keyBytes = 32

var (
ErrLastUsedAtNotUpdated = pkgErr.NewErrorFailedPrecondition(
pkgErr.AccountPackageName, "last used at not updated")
ErrInvalidLastUsedAt = pkgErr.NewErrorInvalidArgNotMatchFormat(
pkgErr.AccountPackageName, "invalid last used at", "last_used_at")
)

type APIKey struct {
*proto.APIKey
}
Expand Down Expand Up @@ -115,3 +123,14 @@ func (a *APIKey) Update(
updated.UpdatedAt = time.Now().Unix()
return updated, nil
}

func (a *APIKey) SetUsedAt(lastUsedAt int64) error {
if lastUsedAt <= 0 {
return ErrInvalidLastUsedAt
}
if a.LastUsedAt >= lastUsedAt {
return ErrLastUsedAtNotUpdated
}
a.LastUsedAt = lastUsedAt
return nil
}
6 changes: 6 additions & 0 deletions pkg/account/storage/v2/api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func (s *accountStorage) UpdateAPIKey(ctx context.Context, k *domain.APIKey, env
k.Maintainer,
k.Description,
k.UpdatedAt,
k.LastUsedAt,
k.Id,
environmentID,
)
Expand Down Expand Up @@ -122,6 +123,7 @@ func (s *accountStorage) GetAPIKey(ctx context.Context, id, environmentID string
&apiKey.Description,
&apiKey.ApiKey,
&apiKey.Maintainer,
&apiKey.LastUsedAt,
)
if err != nil {
if errors.Is(err, mysql.ErrNoRows) {
Expand Down Expand Up @@ -155,6 +157,7 @@ func (s *accountStorage) GetAPIKeyByAPIKey(
&apiKeyDB.Description,
&apiKeyDB.ApiKey,
&apiKeyDB.Maintainer,
&apiKeyDB.LastUsedAt,
)
if err != nil {
if errors.Is(err, mysql.ErrNoRows) {
Expand Down Expand Up @@ -191,6 +194,7 @@ func (s *accountStorage) ListAllEnvironmentAPIKeys(
&apiKeyDB.Description,
&apiKeyDB.ApiKey,
&apiKeyDB.Maintainer,
&apiKeyDB.LastUsedAt,

// Environment columns
&envDB.Id,
Expand Down Expand Up @@ -246,6 +250,7 @@ func (s *accountStorage) GetEnvironmentAPIKey(
&apiKeyDB.Description,
&apiKeyDB.ApiKey,
&apiKeyDB.Maintainer,
&apiKeyDB.LastUsedAt,

// Environment columns
&envDB.Id,
Expand Down Expand Up @@ -306,6 +311,7 @@ func (s *accountStorage) ListAPIKeys(
&apiKey.EnvironmentName,
&apiKey.ApiKey,
&apiKey.Maintainer,
&apiKey.LastUsedAt,
)
if err != nil {
return nil, 0, 0, err
Expand Down
5 changes: 3 additions & 2 deletions pkg/account/storage/v2/api_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func TestUpdateAPIKey(t *testing.T) {
createdAt := int64(2)
updatedAt := int64(3)
maintainer := "demo@bucketeer.io"
lastUsedAt := int64(16000000000)

patterns := []struct {
desc string
Expand Down Expand Up @@ -170,11 +171,11 @@ func TestUpdateAPIKey(t *testing.T) {
s.qe.(*mock.MockQueryExecer).EXPECT().ExecContext(
gomock.Any(),
gomock.Any(),
name, int32(role), disabled, maintainer, description, updatedAt, id, environmentId,
name, int32(role), disabled, maintainer, description, updatedAt, lastUsedAt, id, environmentId,
).Return(result, nil)
},
input: &domain.APIKey{
APIKey: &proto.APIKey{Id: id, Name: name, Role: role, Disabled: disabled, Maintainer: maintainer, Description: description, CreatedAt: createdAt, UpdatedAt: updatedAt},
APIKey: &proto.APIKey{Id: id, Name: name, Role: role, Disabled: disabled, Maintainer: maintainer, Description: description, LastUsedAt: lastUsedAt, CreatedAt: createdAt, UpdatedAt: updatedAt},
},
environmentId: environmentId,
expectedErr: nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ SELECT
ak.description AS api_key_description,
ak.api_key AS api_key_key,
ak.maintainer AS api_key_maintainer,
ak.last_used_at AS api_key_last_used_at,

env.id AS environment_id,
env.name AS environment_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ SELECT
api_key.description,
environment_v2.name as environment_name,
api_key.api_key,
api_key.maintainer
api_key.maintainer,
api_key.last_used_at
FROM
api_key
LEFT JOIN environment_v2 ON api_key.environment_id = environment_v2.id
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ SELECT
updated_at,
description,
api_key,
maintainer
maintainer,
last_used_at
FROM
api_key
WHERE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ SELECT
updated_at,
description,
api_key,
maintainer
maintainer,
last_used_at
FROM
api_key
WHERE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ SELECT
ak.description AS api_key_description,
ak.api_key AS api_key_key,
ak.maintainer AS api_key_maintainer,
ak.last_used_at AS api_key_last_used_at,

env.id AS environment_id,
env.name AS environment_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ UPDATE api_key SET
disabled = ?,
maintainer = ?,
description = ?,
updated_at = ?
updated_at = ?,
last_used_at = ?
WHERE
id = ? AND
environment_id = ?
Loading