From ed1ad34f9bcc7136f7f0d0d8c0ca26ef2f445431 Mon Sep 17 00:00:00 2001 From: Jo Date: Mon, 28 Oct 2024 10:49:48 +0100 Subject: [PATCH] ServiceAccounts: Allow use of UIDs for Service Account routes (#95401) --- .../ossaccesscontrol/service_account.go | 5 +- pkg/services/serviceaccounts/api/api.go | 52 ++++++++++--- pkg/services/serviceaccounts/api/token.go | 10 ++- .../serviceaccounts/database/stats_test.go | 4 + .../serviceaccounts/database/store.go | 32 ++++++-- .../serviceaccounts/database/store_test.go | 73 +++++++++++++++---- .../serviceaccounts/database/token_store.go | 2 +- .../serviceaccounts/extsvcaccounts/service.go | 2 +- .../serviceaccounts/manager/service.go | 21 ++++-- .../serviceaccounts/manager/service_test.go | 2 +- pkg/services/serviceaccounts/manager/store.go | 2 +- pkg/services/serviceaccounts/models.go | 10 +++ pkg/services/serviceaccounts/proxy/service.go | 14 ++-- .../serviceaccounts/proxy/service_test.go | 2 +- .../serviceaccounts/retriever/retriever.go | 6 +- .../serviceaccounts/serviceaccounts.go | 2 +- pkg/services/serviceaccounts/tests/fakes.go | 2 +- pkg/services/serviceaccounts/tests/mocks.go | 16 ++-- public/api-enterprise-spec.json | 8 ++ public/api-merged.json | 8 ++ .../ServiceAccountCreatePage.tsx | 10 ++- .../ServiceAccountPage.test.tsx | 3 +- .../serviceaccounts/ServiceAccountPage.tsx | 13 ++-- .../serviceaccounts/ServiceAccountTable.tsx | 2 +- .../ServiceAccountsListPage.test.tsx | 3 +- .../ServiceAccountsListPage.tsx | 4 +- .../features/serviceaccounts/state/actions.ts | 10 +-- .../state/actionsServiceAccountPage.ts | 28 +++---- public/app/types/serviceaccount.ts | 2 + public/openapi3.json | 8 ++ 30 files changed, 254 insertions(+), 102 deletions(-) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/service_account.go b/pkg/services/accesscontrol/ossaccesscontrol/service_account.go index 7925a564690..76a128c8193 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/service_account.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/service_account.go @@ -51,7 +51,10 @@ func ProvideServiceAccountPermissions( if err != nil { return err } - _, err = serviceAccountRetrieverService.RetrieveServiceAccount(ctx, orgID, id) + _, err = serviceAccountRetrieverService.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{ + OrgID: orgID, + ID: id, + }) return err }, Assignments: resourcepermissions.Assignments{ diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index e5e5a2c4022..cfd850aa645 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -31,6 +31,30 @@ type ServiceAccountsAPI struct { isExternalSAEnabled bool } +func MiddlewareServiceAccountUIDResolver(saService serviceaccounts.Service, paramName string) web.Handler { + return func(c *contextmodel.ReqContext) { + // Get service account id from request + saUID := web.Params(c.Req)[paramName] + // if saID is empty or is an integer, we assume it's a service account id and we don't need to resolve it + _, err := strconv.ParseInt(saUID, 10, 64) + if saUID == "" || err == nil { + return + } + + serviceAccount, err := saService.RetrieveServiceAccount(c.Req.Context(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: c.SignedInUser.GetOrgID(), + UID: saUID, + }) + if err == nil { + gotParams := web.Params(c.Req) + gotParams[paramName] = strconv.FormatInt(serviceAccount.Id, 10) + web.SetURLParams(c.Req, gotParams) + } else { + c.JsonApiErr(http.StatusNotFound, "Not found", nil) + } + } +} + func NewServiceAccountsAPI( cfg *setting.Cfg, service serviceaccounts.Service, @@ -55,15 +79,16 @@ func NewServiceAccountsAPI( func (api *ServiceAccountsAPI) RegisterAPIEndpoints() { auth := accesscontrol.Middleware(api.accesscontrol) + saUIDResolver := MiddlewareServiceAccountUIDResolver(api.service, ":serviceAccountId") api.RouterRegister.Group("/api/serviceaccounts", func(serviceAccountsRoute routing.RouteRegister) { serviceAccountsRoute.Get("/search", auth(accesscontrol.EvalPermission(serviceaccounts.ActionRead)), routing.Wrap(api.SearchOrgServiceAccountsWithPaging)) serviceAccountsRoute.Post("/", auth(accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.CreateServiceAccount)) - serviceAccountsRoute.Get("/:serviceAccountId", auth(accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeID)), routing.Wrap(api.RetrieveServiceAccount)) - serviceAccountsRoute.Patch("/:serviceAccountId", auth(accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.UpdateServiceAccount)) - serviceAccountsRoute.Delete("/:serviceAccountId", auth(accesscontrol.EvalPermission(serviceaccounts.ActionDelete, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteServiceAccount)) - serviceAccountsRoute.Get("/:serviceAccountId/tokens", auth(accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeID)), routing.Wrap(api.ListTokens)) - serviceAccountsRoute.Post("/:serviceAccountId/tokens", auth(accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.CreateToken)) - serviceAccountsRoute.Delete("/:serviceAccountId/tokens/:tokenId", auth(accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteToken)) + serviceAccountsRoute.Get("/:serviceAccountId", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeID)), routing.Wrap(api.RetrieveServiceAccount)) + serviceAccountsRoute.Patch("/:serviceAccountId", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.UpdateServiceAccount)) + serviceAccountsRoute.Delete("/:serviceAccountId", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionDelete, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteServiceAccount)) + serviceAccountsRoute.Get("/:serviceAccountId/tokens", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeID)), routing.Wrap(api.ListTokens)) + serviceAccountsRoute.Post("/:serviceAccountId/tokens", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.CreateToken)) + serviceAccountsRoute.Delete("/:serviceAccountId/tokens/:tokenId", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteToken)) serviceAccountsRoute.Post("/migrate", auth(accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.MigrateApiKeysToServiceAccounts)) serviceAccountsRoute.Post("/migrate/:keyId", auth(accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.ConvertToServiceAccount)) }, requestmeta.SetOwner(requestmeta.TeamAuth)) @@ -125,12 +150,15 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *contextmodel.ReqContext) // 404: notFoundError // 500: internalServerError func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *contextmodel.ReqContext) response.Response { - scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) + saID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } - serviceAccount, err := api.service.RetrieveServiceAccount(ctx.Req.Context(), ctx.SignedInUser.GetOrgID(), scopeID) + serviceAccount, err := api.service.RetrieveServiceAccount(ctx.Req.Context(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: ctx.SignedInUser.GetOrgID(), + ID: saID, + }) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "Failed to retrieve service account", err) } @@ -167,7 +195,7 @@ func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *contextmodel.ReqConte // 404: notFoundError // 500: internalServerError func (api *ServiceAccountsAPI) UpdateServiceAccount(c *contextmodel.ReqContext) response.Response { - scopeID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) + saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) } @@ -181,7 +209,7 @@ func (api *ServiceAccountsAPI) UpdateServiceAccount(c *contextmodel.ReqContext) return response.ErrOrFallback(http.StatusInternalServerError, "failed to update service account", err) } - resp, err := api.service.UpdateServiceAccount(c.Req.Context(), c.SignedInUser.GetOrgID(), scopeID, &cmd) + resp, err := api.service.UpdateServiceAccount(c.Req.Context(), c.SignedInUser.GetOrgID(), saID, &cmd) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "Failed update service account", err) } @@ -223,11 +251,11 @@ func (api *ServiceAccountsAPI) validateRole(r *org.RoleType, orgRole org.RoleTyp // 403: forbiddenError // 500: internalServerError func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *contextmodel.ReqContext) response.Response { - scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) + saID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service account ID is invalid", err) } - err = api.service.DeleteServiceAccount(ctx.Req.Context(), ctx.SignedInUser.GetOrgID(), scopeID) + err = api.service.DeleteServiceAccount(ctx.Req.Context(), ctx.SignedInUser.GetOrgID(), saID) if err != nil { return response.Error(http.StatusInternalServerError, "Service account deletion error", err) } diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index 851b61ed6d7..d9ad97690db 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -132,7 +132,10 @@ func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response. } // confirm service account exists - if _, err = api.service.RetrieveServiceAccount(c.Req.Context(), c.SignedInUser.GetOrgID(), saID); err != nil { + if _, err = api.service.RetrieveServiceAccount(c.Req.Context(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: c.SignedInUser.GetOrgID(), + ID: saID, + }); err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "Failed to retrieve service account", err) } @@ -205,7 +208,10 @@ func (api *ServiceAccountsAPI) DeleteToken(c *contextmodel.ReqContext) response. } // confirm service account exists - if _, err := api.service.RetrieveServiceAccount(c.Req.Context(), c.SignedInUser.GetOrgID(), saID); err != nil { + if _, err := api.service.RetrieveServiceAccount(c.Req.Context(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: c.SignedInUser.GetOrgID(), + ID: saID, + }); err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "Failed to retrieve service account", err) } diff --git a/pkg/services/serviceaccounts/database/stats_test.go b/pkg/services/serviceaccounts/database/stats_test.go index 68441f9548c..ae158a63d64 100644 --- a/pkg/services/serviceaccounts/database/stats_test.go +++ b/pkg/services/serviceaccounts/database/stats_test.go @@ -14,6 +14,10 @@ import ( ) func TestIntegrationStore_UsageStats(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + saToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true} db, store := setupTestDatabase(t) sa := tests.SetupUserServiceAccount(t, db, store.cfg, saToCreate) diff --git a/pkg/services/serviceaccounts/database/store.go b/pkg/services/serviceaccounts/database/store.go index 003a41a24aa..499916a06f6 100644 --- a/pkg/services/serviceaccounts/database/store.go +++ b/pkg/services/serviceaccounts/database/store.go @@ -3,6 +3,7 @@ package database //nolint:goimports import ( "context" + "errors" "fmt" "strings" "time" @@ -81,6 +82,7 @@ func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, org return &serviceaccounts.ServiceAccountDTO{ Id: newSA.ID, + UID: newSA.UID, Name: newSA.Name, Login: newSA.Login, OrgId: newSA.OrgID, @@ -100,7 +102,7 @@ func (s *ServiceAccountsStoreImpl) UpdateServiceAccount( err := s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { var err error - updatedUser, err = s.RetrieveServiceAccount(ctx, orgId, serviceAccountId) + updatedUser, err = s.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{OrgID: orgId, ID: serviceAccountId}) if err != nil { return err } @@ -192,8 +194,15 @@ func (s *ServiceAccountsStoreImpl) EnableServiceAccount(ctx context.Context, org }) } -// RetrieveServiceAccount returns a service account by its ID -func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgId, serviceAccountId int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { +// RetrieveServiceAccount returns a service account by its ID or UID +func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { + if query.ID == 0 && query.UID == "" { + return nil, errors.New("either ID or UID must be provided") + } + if query.OrgID == 0 { + return nil, errors.New("OrgID must be provided") + } + serviceAccount := &serviceaccounts.ServiceAccountProfileDTO{} err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { @@ -205,10 +214,17 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o whereParams := make([]any, 0) whereConditions = append(whereConditions, "org_user.org_id = ?") - whereParams = append(whereParams, orgId) + whereParams = append(whereParams, query.OrgID) - whereConditions = append(whereConditions, "org_user.user_id = ?") - whereParams = append(whereParams, serviceAccountId) + if query.ID != 0 { + whereConditions = append(whereConditions, "org_user.user_id = ?") + whereParams = append(whereParams, query.ID) + } + + if query.UID != "" { + whereConditions = append(whereConditions, "user.uid = ?") + whereParams = append(whereParams, query.UID) + } whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %s", @@ -223,6 +239,7 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o "org_user.role", "user.email", "user.name", + "user.uid", "user.login", "user.created", "user.updated", @@ -232,7 +249,7 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o if ok, err := sess.Get(serviceAccount); err != nil { return err } else if !ok { - return serviceaccounts.ErrServiceAccountNotFound.Errorf("service account with id %d not found", serviceAccountId) + return serviceaccounts.ErrServiceAccountNotFound.Errorf("service account with id %d or uid %s not found", query.ID, query.UID) } return nil @@ -377,6 +394,7 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context, "user.email", "user.name", "user.login", + "user.uid", "user.last_seen_at", "user.is_disabled", ) diff --git a/pkg/services/serviceaccounts/database/store_test.go b/pkg/services/serviceaccounts/database/store_test.go index 5804a3fe8e6..a304cfc60f7 100644 --- a/pkg/services/serviceaccounts/database/store_test.go +++ b/pkg/services/serviceaccounts/database/store_test.go @@ -28,7 +28,11 @@ func TestMain(m *testing.M) { } // Service Account should not create an org on its own -func TestStore_CreateServiceAccountOrgNonExistant(t *testing.T) { +func TestIntegrationStore_CreateServiceAccountOrgNonExistant(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + _, store := setupTestDatabase(t) serviceAccountName := "new Service Account" t.Run("create service account", func(t *testing.T) { @@ -67,7 +71,10 @@ func TestStore_CreateServiceAccount(t *testing.T) { assert.Equal(t, serviceAccountName, saDTO.Name) assert.Equal(t, 0, int(saDTO.Tokens)) - retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) + retrieved, err := store.RetrieveServiceAccount(context.Background(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: serviceAccountOrgId, + ID: saDTO.Id, + }) require.NoError(t, err) assert.Equal(t, serviceAccountName, retrieved.Name) assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) @@ -98,7 +105,10 @@ func TestStore_CreateServiceAccount(t *testing.T) { assert.Equal(t, serviceAccountName, saDTO.Name) assert.Equal(t, 0, int(saDTO.Tokens)) - retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) + retrieved, err := store.RetrieveServiceAccount(context.Background(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: serviceAccountOrgId, + ID: saDTO.Id, + }) require.NoError(t, err) assert.Equal(t, serviceAccountName, retrieved.Name) assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) @@ -133,7 +143,10 @@ func TestStore_CreateServiceAccount(t *testing.T) { assert.Equal(t, serviceAccountName, saDTO.Name) assert.Equal(t, 0, int(saDTO.Tokens)) - retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) + retrieved, err := store.RetrieveServiceAccount(context.Background(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: serviceAccountOrgId, + ID: saDTO.Id, + }) require.NoError(t, err) assert.Equal(t, serviceAccountName, retrieved.Name) assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) @@ -156,7 +169,11 @@ func TestStore_CreateServiceAccount(t *testing.T) { }) } -func TestStore_CreateServiceAccountRoleNone(t *testing.T) { +func TestIntegrationStore_CreateServiceAccountRoleNone(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + _, store := setupTestDatabase(t) orgQuery := &org.CreateOrgCommand{Name: orgimpl.MainOrgName} orgResult, err := store.orgService.CreateWithMember(context.Background(), orgQuery) @@ -176,7 +193,10 @@ func TestStore_CreateServiceAccountRoleNone(t *testing.T) { assert.Equal(t, serviceAccountName, saDTO.Name) assert.Equal(t, 0, int(saDTO.Tokens)) - retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) + retrieved, err := store.RetrieveServiceAccount(context.Background(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: serviceAccountOrgId, + ID: saDTO.Id, + }) require.NoError(t, err) assert.Equal(t, serviceAccountName, retrieved.Name) assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) @@ -188,7 +208,10 @@ func TestStore_CreateServiceAccountRoleNone(t *testing.T) { assert.Equal(t, saDTO.Role, string(org.RoleNone)) } -func TestStore_DeleteServiceAccount(t *testing.T) { +func TestIntegrationStore_DeleteServiceAccount(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } cases := []struct { desc string user tests.TestUser @@ -237,7 +260,10 @@ func setupTestDatabase(t *testing.T) (db.DB, *ServiceAccountsStoreImpl) { return db, ProvideServiceAccountsStore(cfg, db, apiKeyService, kvStore, userSvc, orgService) } -func TestStore_RetrieveServiceAccount(t *testing.T) { +func TestIntegrationStore_RetrieveServiceAccount(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } cases := []struct { desc string user tests.TestUser @@ -259,7 +285,10 @@ func TestStore_RetrieveServiceAccount(t *testing.T) { t.Run(c.desc, func(t *testing.T) { db, store := setupTestDatabase(t) user := tests.SetupUserServiceAccount(t, db, store.cfg, c.user) - dto, err := store.RetrieveServiceAccount(context.Background(), user.OrgID, user.ID) + dto, err := store.RetrieveServiceAccount(context.Background(), &serviceaccounts.GetServiceAccountQuery{ + OrgID: user.OrgID, + ID: user.ID, + }) if c.expectedErr != nil { require.ErrorIs(t, err, c.expectedErr) } else { @@ -271,7 +300,10 @@ func TestStore_RetrieveServiceAccount(t *testing.T) { } } -func TestStore_MigrateApiKeys(t *testing.T) { +func TestIntegrationStore_MigrateApiKeys(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } cases := []struct { desc string key tests.TestApiKey @@ -331,7 +363,10 @@ func TestStore_MigrateApiKeys(t *testing.T) { } } -func TestStore_MigrateAllApiKeys(t *testing.T) { +func TestIntegrationStore_MigrateAllApiKeys(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } cases := []struct { desc string keys []tests.TestApiKey @@ -448,7 +483,11 @@ func TestStore_MigrateAllApiKeys(t *testing.T) { }) } } -func TestServiceAccountsStoreImpl_SearchOrgServiceAccounts(t *testing.T) { +func TestIntegrationServiceAccountsStoreImpl_SearchOrgServiceAccounts(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + initUsers := []tests.TestUser{ {Name: "satest-1", Role: string(org.RoleViewer), Login: "sa-1-satest-1", IsServiceAccount: true}, {Name: "usertest-2", Role: string(org.RoleEditor), Login: "usertest-2", IsServiceAccount: false}, @@ -575,7 +614,11 @@ func TestServiceAccountsStoreImpl_SearchOrgServiceAccounts(t *testing.T) { } } -func TestServiceAccountsStoreImpl_EnableServiceAccounts(t *testing.T) { +func TestIntegrationServiceAccountsStoreImpl_EnableServiceAccounts(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + ctx := context.Background() initUsers := []tests.TestUser{ @@ -588,9 +631,9 @@ func TestServiceAccountsStoreImpl_EnableServiceAccounts(t *testing.T) { orgID := tests.SetupUsersServiceAccounts(t, db, store.cfg, initUsers) fetchStates := func() map[int64]bool { - sa1, err := store.RetrieveServiceAccount(ctx, orgID, 1) + sa1, err := store.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{OrgID: orgID, ID: 1}) require.NoError(t, err) - sa2, err := store.RetrieveServiceAccount(ctx, orgID, 2) + sa2, err := store.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{OrgID: orgID, ID: 2}) require.NoError(t, err) user, err := store.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: 3}) require.NoError(t, err) diff --git a/pkg/services/serviceaccounts/database/token_store.go b/pkg/services/serviceaccounts/database/token_store.go index 173f7a27177..d1145a1730d 100644 --- a/pkg/services/serviceaccounts/database/token_store.go +++ b/pkg/services/serviceaccounts/database/token_store.go @@ -45,7 +45,7 @@ func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, s var apiKey *apikey.APIKey return apiKey, s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - if _, err := s.RetrieveServiceAccount(ctx, cmd.OrgId, serviceAccountId); err != nil { + if _, err := s.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{OrgID: cmd.OrgId, ID: serviceAccountId}); err != nil { return err } diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service.go b/pkg/services/serviceaccounts/extsvcaccounts/service.go index bd4b7ff9cf3..d51df9964ca 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service.go @@ -94,7 +94,7 @@ func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, org ctx, span := esa.tracer.Start(ctx, "ExtSvcAccountsService.RetrieveExtSvcAccount") defer span.End() - svcAcc, err := esa.saSvc.RetrieveServiceAccount(ctx, orgID, saID) + svcAcc, err := esa.saSvc.RetrieveServiceAccount(ctx, &sa.GetServiceAccountQuery{OrgID: orgID, ID: saID}) if err != nil { return nil, err } diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 0a0a74bc2c6..c42136e7718 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -193,14 +193,16 @@ func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, orgI return serviceAccount, nil } -func (sa *ServiceAccountsService) RetrieveServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - if err := validOrgID(orgID); err != nil { +func (sa *ServiceAccountsService) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { + if err := validOrgID(query.OrgID); err != nil { return nil, err } - if err := validServiceAccountID(serviceAccountID); err != nil { - return nil, err + if err := validServiceAccountID(query.ID); err != nil { + if err := validServiceAccountUID(query.UID); err != nil { + return nil, fmt.Errorf("invalid service account ID %d and UID %s has been specified", query.ID, query.UID) + } } - return sa.store.RetrieveServiceAccount(ctx, orgID, serviceAccountID) + return sa.store.RetrieveServiceAccount(ctx, query) } func (sa *ServiceAccountsService) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { @@ -303,12 +305,21 @@ func validOrgID(orgID int64) error { } return nil } + func validServiceAccountID(serviceaccountID int64) error { if serviceaccountID == 0 { return serviceaccounts.ErrServiceAccountInvalidID.Errorf("invalid service account ID 0 has been specified") } return nil } + +func validServiceAccountUID(saUID string) error { + if saUID == "" { + return serviceaccounts.ErrServiceAccountInvalidID.Errorf("invalid service account UID has been specified") + } + return nil +} + func validServiceAccountTokenID(tokenID int64) error { if tokenID == 0 { return serviceaccounts.ErrServiceAccountInvalidTokenID.Errorf("invalid service account token ID 0 has been specified") diff --git a/pkg/services/serviceaccounts/manager/service_test.go b/pkg/services/serviceaccounts/manager/service_test.go index 4115b9b3e25..43614415dc9 100644 --- a/pkg/services/serviceaccounts/manager/service_test.go +++ b/pkg/services/serviceaccounts/manager/service_test.go @@ -35,7 +35,7 @@ func newServiceAccountStoreFake() *FakeServiceAccountStore { } // CreateServiceAccount is a fake creating a service account. -func (f *FakeServiceAccountStore) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { +func (f *FakeServiceAccountStore) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { return f.ExpectedServiceAccountProfileDTO, f.ExpectedError } diff --git a/pkg/services/serviceaccounts/manager/store.go b/pkg/services/serviceaccounts/manager/store.go index b64e67d7522..63235bc4fb2 100644 --- a/pkg/services/serviceaccounts/manager/store.go +++ b/pkg/services/serviceaccounts/manager/store.go @@ -17,7 +17,7 @@ type store interface { ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) - RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) + RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) RevokeServiceAccountToken(ctx context.Context, orgId, serviceAccountId, tokenId int64) error SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 775ba2457f0..50f0914b403 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -73,6 +73,8 @@ type UpdateServiceAccountForm struct { // swagger: model type ServiceAccountDTO struct { Id int64 `json:"id" xorm:"user_id"` + // example: fe1xejlha91xce + UID string `json:"uid" xorm:"uid"` // example: grafana Name string `json:"name" xorm:"name"` // example: sa-grafana @@ -98,6 +100,12 @@ type GetSATokensQuery struct { ServiceAccountID *int64 // optional filtering by service account ID } +type GetServiceAccountQuery struct { + OrgID int64 `json:"orgId"` + ID int64 `json:"id"` + UID string `json:"uid"` +} + type AddServiceAccountTokenCommand struct { Name string `json:"name" binding:"Required"` OrgId int64 `json:"-"` @@ -135,6 +143,8 @@ type SearchOrgServiceAccountsResult struct { type ServiceAccountProfileDTO struct { // example: 2 Id int64 `json:"id" xorm:"user_id"` + // example: fe1xejlha91xce + UID string `json:"uid" xorm:"uid"` // example: test Name string `json:"name" xorm:"name"` // example: sa-grafana diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index de4153597b3..93917a58369 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -51,7 +51,7 @@ var _ serviceaccounts.Service = (*ServiceAccountsProxy)(nil) func (s *ServiceAccountsProxy) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { if s.isProxyEnabled { - sa, err := s.proxiedService.RetrieveServiceAccount(ctx, cmd.OrgId, serviceAccountID) + sa, err := s.proxiedService.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{ID: serviceAccountID, OrgID: cmd.OrgId}) if err != nil { return nil, err } @@ -77,7 +77,7 @@ func (s *ServiceAccountsProxy) CreateServiceAccount(ctx context.Context, orgID i func (s *ServiceAccountsProxy) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { if s.isProxyEnabled { - sa, err := s.proxiedService.RetrieveServiceAccount(ctx, orgID, serviceAccountID) + sa, err := s.proxiedService.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{ID: serviceAccountID, OrgID: orgID}) if err != nil { return err } @@ -92,7 +92,7 @@ func (s *ServiceAccountsProxy) DeleteServiceAccount(ctx context.Context, orgID, func (s *ServiceAccountsProxy) DeleteServiceAccountToken(ctx context.Context, orgID int64, serviceAccountID int64, tokenID int64) error { if s.isProxyEnabled { - sa, err := s.proxiedService.RetrieveServiceAccount(ctx, orgID, serviceAccountID) + sa, err := s.proxiedService.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{OrgID: orgID, ID: serviceAccountID}) if err != nil { return err } @@ -107,7 +107,7 @@ func (s *ServiceAccountsProxy) DeleteServiceAccountToken(ctx context.Context, or func (s *ServiceAccountsProxy) EnableServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, enable bool) error { if s.isProxyEnabled { - sa, err := s.proxiedService.RetrieveServiceAccount(ctx, orgID, serviceAccountID) + sa, err := s.proxiedService.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{OrgID: orgID, ID: serviceAccountID}) if err != nil { return err } @@ -131,8 +131,8 @@ func (s *ServiceAccountsProxy) MigrateApiKeysToServiceAccounts(ctx context.Conte return s.proxiedService.MigrateApiKeysToServiceAccounts(ctx, orgID) } -func (s *ServiceAccountsProxy) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - sa, err := s.proxiedService.RetrieveServiceAccount(ctx, orgID, serviceAccountID) +func (s *ServiceAccountsProxy) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { + sa, err := s.proxiedService.RetrieveServiceAccount(ctx, query) if err != nil { return nil, err } @@ -155,7 +155,7 @@ func (s *ServiceAccountsProxy) UpdateServiceAccount(ctx context.Context, orgID, s.log.Error("Invalid service account name", "name", *saForm.Name) return nil, extsvcaccounts.ErrInvalidName } - sa, err := s.proxiedService.RetrieveServiceAccount(ctx, orgID, serviceAccountID) + sa, err := s.proxiedService.RetrieveServiceAccount(ctx, &serviceaccounts.GetServiceAccountQuery{OrgID: orgID, ID: serviceAccountID}) if err != nil { return nil, err } diff --git a/pkg/services/serviceaccounts/proxy/service_test.go b/pkg/services/serviceaccounts/proxy/service_test.go index 53e79746b4f..4ea7fc5e598 100644 --- a/pkg/services/serviceaccounts/proxy/service_test.go +++ b/pkg/services/serviceaccounts/proxy/service_test.go @@ -154,7 +154,7 @@ func TestProvideServiceAccount_crudServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { serviceMock.ExpectedServiceAccountProfile = tc.expectedServiceAccount - sa, err := svc.RetrieveServiceAccount(context.Background(), autoAssignOrgID, testServiceAccountId) + sa, err := svc.RetrieveServiceAccount(context.Background(), &sa.GetServiceAccountQuery{OrgID: autoAssignOrgID, ID: testServiceAccountId}) assert.NoError(t, err, tc.description) assert.Equal(t, tc.expectedIsExternal, sa.IsExternal, tc.description) }) diff --git a/pkg/services/serviceaccounts/retriever/retriever.go b/pkg/services/serviceaccounts/retriever/retriever.go index f2d41622db4..a3f8d57e755 100644 --- a/pkg/services/serviceaccounts/retriever/retriever.go +++ b/pkg/services/serviceaccounts/retriever/retriever.go @@ -17,7 +17,7 @@ import ( // At the time of writing, this service is only used by the service accounts permissions service // to avoid cyclic dependency between the ServiceAccountService and the ServiceAccountPermissionsService type ServiceAccountRetriever interface { - RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) + RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) } // ServiceAccountRetriever is the service that manages service accounts. @@ -47,6 +47,6 @@ func ProvideService( } } -func (s *Service) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - return s.store.RetrieveServiceAccount(ctx, orgID, serviceAccountID) +func (s *Service) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { + return s.store.RetrieveServiceAccount(ctx, query) } diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index 046e6df2657..daee2197a75 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -17,7 +17,7 @@ do not have a password. type Service interface { CreateServiceAccount(ctx context.Context, orgID int64, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error - RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error) + RetrieveServiceAccount(ctx context.Context, query *GetServiceAccountQuery) (*ServiceAccountProfileDTO, error) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) SearchOrgServiceAccounts(ctx context.Context, query *SearchOrgServiceAccountsQuery) (*SearchOrgServiceAccountsResult, error) EnableServiceAccount(ctx context.Context, orgID, serviceAccountID int64, enable bool) error diff --git a/pkg/services/serviceaccounts/tests/fakes.go b/pkg/services/serviceaccounts/tests/fakes.go index df067ad5ae1..6e2a4427338 100644 --- a/pkg/services/serviceaccounts/tests/fakes.go +++ b/pkg/services/serviceaccounts/tests/fakes.go @@ -37,7 +37,7 @@ func (f *FakeServiceAccountService) EnableServiceAccount(ctx context.Context, or return f.ExpectedErr } -func (f *FakeServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID, id int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { +func (f *FakeServiceAccountService) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { return f.ExpectedServiceAccountProfile, f.ExpectedErr } diff --git a/pkg/services/serviceaccounts/tests/mocks.go b/pkg/services/serviceaccounts/tests/mocks.go index 5cd884fcf8d..500b6f535fc 100644 --- a/pkg/services/serviceaccounts/tests/mocks.go +++ b/pkg/services/serviceaccounts/tests/mocks.go @@ -178,24 +178,24 @@ func (_m *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context } // RetrieveServiceAccount provides a mock function with given fields: ctx, orgID, serviceAccountID -func (_m *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) { - ret := _m.Called(ctx, orgID, serviceAccountID) +func (_m *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { + ret := _m.Called(ctx, query) var r0 *serviceaccounts.ServiceAccountProfileDTO var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*serviceaccounts.ServiceAccountProfileDTO, error)); ok { - return rf(ctx, orgID, serviceAccountID) + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error)); ok { + return rf(ctx, query) } - if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *serviceaccounts.ServiceAccountProfileDTO); ok { - r0 = rf(ctx, orgID, serviceAccountID) + if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.GetServiceAccountQuery) *serviceaccounts.ServiceAccountProfileDTO); ok { + r0 = rf(ctx, query) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*serviceaccounts.ServiceAccountProfileDTO) } } - if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok { - r1 = rf(ctx, orgID, serviceAccountID) + if rf, ok := ret.Get(1).(func(context.Context, *serviceaccounts.GetServiceAccountQuery) error); ok { + r1 = rf(ctx, query) } else { r1 = ret.Error(1) } diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 4c0dcb2ce34..e25315267c5 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -7421,6 +7421,10 @@ "type": "integer", "format": "int64", "example": 0 + }, + "uid": { + "type": "string", + "example": "fe1xejlha91xce" } } }, @@ -7487,6 +7491,10 @@ "type": "integer", "format": "int64" }, + "uid": { + "type": "string", + "example": "fe1xejlha91xce" + }, "updatedAt": { "type": "string", "format": "date-time", diff --git a/public/api-merged.json b/public/api-merged.json index 393760e29c1..874b6708dec 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -20434,6 +20434,10 @@ "type": "integer", "format": "int64", "example": 0 + }, + "uid": { + "type": "string", + "example": "fe1xejlha91xce" } } }, @@ -20500,6 +20504,10 @@ "type": "integer", "format": "int64" }, + "uid": { + "type": "string", + "example": "fe1xejlha91xce" + }, "updatedAt": { "type": "string", "format": "date-time", diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index 83732d0e733..fa24596b328 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -22,11 +22,12 @@ const createServiceAccount = async (sa: ServiceAccountDTO) => { return result; }; -const updateServiceAccount = async (id: number, sa: ServiceAccountDTO) => - getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa); +const updateServiceAccount = async (uid: string, sa: ServiceAccountDTO) => + getBackendSrv().patch(`/api/serviceaccounts/${uid}`, sa); const defaultServiceAccount = { id: 0, + uid: '', orgId: contextSrv.user.orgId, role: contextSrv.licensedAccessControlEnabled() ? OrgRole.None : OrgRole.Viewer, tokens: 0, @@ -81,6 +82,7 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { const newAccount: ServiceAccountCreateApiResponse = { avatarUrl: response.avatarUrl, id: response.id, + uid: response.uid, isDisabled: response.isDisabled, login: response.login, name: response.name, @@ -88,7 +90,7 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { role: response.role, tokens: response.tokens, }; - await updateServiceAccount(response.id, data); + await updateServiceAccount(newAccount.uid, data); if ( contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && @@ -99,7 +101,7 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { } catch (e) { console.error(e); // TODO: handle error } - locationService.push(`/org/serviceaccounts/${response.id}`); + locationService.push(`/org/serviceaccounts/${response.uid}`); }, [serviceAccount.role, pendingRoles] ); diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx index fe3e5cb42f8..ee61c2af792 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx @@ -61,6 +61,7 @@ const setup = (propOverrides: Partial) => { const getDefaultServiceAccount = (): ServiceAccountDTO => ({ id: 42, + uid: 'aaaaa', name: 'Data source scavenger', login: 'sa-data-source-scavenger', orgId: 1, @@ -164,6 +165,6 @@ describe('ServiceAccountPage tests', () => { await userEvent.click(screen.getByLabelText(/Delete service account token/)); await user.click(screen.getByRole('button', { name: /^Delete$/ })); - expect(deleteServiceAccountTokenMock).toHaveBeenCalledWith(42, 142); + expect(deleteServiceAccountTokenMock).toHaveBeenCalledWith('aaaaa', 142); }); }); diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index d68bdb53233..755e16dd5c0 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -68,7 +68,6 @@ export const ServiceAccountPageUnconnected = ({ const [isDisableModalOpen, setIsDisableModalOpen] = useState(false); const { id = '' } = useParams(); - const serviceAccountId = parseInt(id, 10); const tokenActionsDisabled = serviceAccount.isDisabled || serviceAccount.isExternal || @@ -87,12 +86,12 @@ export const ServiceAccountPageUnconnected = ({ }; useEffect(() => { - loadServiceAccount(serviceAccountId); - loadServiceAccountTokens(serviceAccountId); + loadServiceAccount(id); + loadServiceAccountTokens(id); if (contextSrv.licensedAccessControlEnabled()) { fetchACOptions(); } - }, [loadServiceAccount, loadServiceAccountTokens, serviceAccountId]); + }, [loadServiceAccount, loadServiceAccountTokens, id]); const onProfileChange = (serviceAccount: ServiceAccountDTO) => { updateServiceAccount(serviceAccount); @@ -107,7 +106,7 @@ export const ServiceAccountPageUnconnected = ({ }; const handleServiceAccountDelete = () => { - deleteServiceAccount(serviceAccount.id); + deleteServiceAccount(serviceAccount.uid); }; const handleServiceAccountDisable = () => { @@ -120,11 +119,11 @@ export const ServiceAccountPageUnconnected = ({ }; const onDeleteServiceAccountToken = (key: ApiKey) => { - deleteServiceAccountToken(serviceAccount?.id, key.id!); + deleteServiceAccountToken(serviceAccount?.uid, key.id!); }; const onCreateToken = (token: ServiceAccountToken) => { - createServiceAccountToken(serviceAccount?.id, token, setNewToken); + createServiceAccountToken(serviceAccount?.uid, token, setNewToken); }; const onTokenModalClose = () => { diff --git a/public/app/features/serviceaccounts/ServiceAccountTable.tsx b/public/app/features/serviceaccounts/ServiceAccountTable.tsx index ede46556e73..cd9655a43c6 100644 --- a/public/app/features/serviceaccounts/ServiceAccountTable.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountTable.tsx @@ -122,7 +122,7 @@ const getCellContent = ( if (isLoading) { return columnName === 'avatarUrl' ? : ; } - const href = `/org/serviceaccounts/${original.id}`; + const href = `/org/serviceaccounts/${original.uid}`; const ariaLabel = `Edit service account's ${name} details`; switch (columnName) { case 'avatarUrl': diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx index 5276cd2026d..f5f563321af 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx @@ -65,6 +65,7 @@ const setup = (propOverrides: Partial) => { const getDefaultServiceAccount: () => ServiceAccountDTO = () => ({ id: 42, + uid: 'aaaaa', name: 'Data source scavenger', login: 'sa-data-source-scavenger', orgId: 1, @@ -155,6 +156,6 @@ describe('ServiceAccountsListPage tests', () => { await user.click(screen.getByLabelText(`Delete service account ${getDefaultServiceAccount().name}`)); await user.click(screen.getByRole('button', { name: 'Delete' })); - expect(deleteServiceAccountMock).toHaveBeenCalledWith(42); + expect(deleteServiceAccountMock).toHaveBeenCalledWith('aaaaa'); }); }); diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index ffd3f39ba59..3c4bab9d3ac 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -112,7 +112,7 @@ export const ServiceAccountsListPageUnconnected = ({ const onServiceAccountRemove = async () => { if (currentServiceAccount) { - deleteServiceAccount(currentServiceAccount.id); + deleteServiceAccount(currentServiceAccount.uid); } onRemoveModalClose(); }; @@ -140,7 +140,7 @@ export const ServiceAccountsListPageUnconnected = ({ const onTokenCreate = async (token: ServiceAccountToken) => { if (currentServiceAccount) { - createServiceAccountToken(currentServiceAccount.id, token, setNewToken); + createServiceAccountToken(currentServiceAccount.uid, token, setNewToken); } }; diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 1668c273359..06591867aa4 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -84,27 +84,27 @@ const fetchServiceAccountsWithDebounce = debounce((dispatch) => dispatch(fetchSe export function updateServiceAccount(serviceAccount: ServiceAccountDTO): ThunkResult { return async (dispatch) => { - await getBackendSrv().patch(`${BASE_URL}/${serviceAccount.id}?accesscontrol=true`, { + await getBackendSrv().patch(`${BASE_URL}/${serviceAccount.uid}?accesscontrol=true`, { ...serviceAccount, }); dispatch(fetchServiceAccounts()); }; } -export function deleteServiceAccount(serviceAccountId: number): ThunkResult { +export function deleteServiceAccount(serviceAccountUid: string): ThunkResult { return async (dispatch) => { - await getBackendSrv().delete(`${BASE_URL}/${serviceAccountId}`); + await getBackendSrv().delete(`${BASE_URL}/${serviceAccountUid}`); dispatch(fetchServiceAccounts()); }; } export function createServiceAccountToken( - saID: number, + saUid: string, token: ServiceAccountToken, onTokenCreated: (key: string) => void ): ThunkResult { return async (dispatch) => { - const result = await getBackendSrv().post(`${BASE_URL}/${saID}/tokens`, token); + const result = await getBackendSrv().post(`${BASE_URL}/${saUid}/tokens`, token); onTokenCreated(result.key); dispatch(fetchServiceAccounts()); }; diff --git a/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts b/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts index 99b8701372c..4644d3ae136 100644 --- a/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts +++ b/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts @@ -13,11 +13,11 @@ import { const BASE_URL = `/api/serviceaccounts`; -export function loadServiceAccount(saID: number): ThunkResult { +export function loadServiceAccount(saUid: string): ThunkResult { return async (dispatch) => { dispatch(serviceAccountFetchBegin()); try { - const response = await getBackendSrv().get(`${BASE_URL}/${saID}`, accessControlQueryParam()); + const response = await getBackendSrv().get(`${BASE_URL}/${saUid}`, accessControlQueryParam()); dispatch(serviceAccountLoaded(response)); } catch (error) { console.error(error); @@ -29,43 +29,43 @@ export function loadServiceAccount(saID: number): ThunkResult { export function updateServiceAccount(serviceAccount: ServiceAccountDTO): ThunkResult { return async (dispatch) => { - await getBackendSrv().patch(`${BASE_URL}/${serviceAccount.id}?accesscontrol=true`, { + await getBackendSrv().patch(`${BASE_URL}/${serviceAccount.uid}?accesscontrol=true`, { ...serviceAccount, }); - dispatch(loadServiceAccount(serviceAccount.id)); + dispatch(loadServiceAccount(serviceAccount.uid)); }; } -export function deleteServiceAccount(serviceAccountId: number): ThunkResult { +export function deleteServiceAccount(serviceAccountUid: string): ThunkResult { return async () => { - await getBackendSrv().delete(`${BASE_URL}/${serviceAccountId}`); + await getBackendSrv().delete(`${BASE_URL}/${serviceAccountUid}`); locationService.push('/org/serviceaccounts'); }; } export function createServiceAccountToken( - saID: number, + saUid: string, token: ServiceAccountToken, onTokenCreated: (key: string) => void ): ThunkResult { return async (dispatch) => { - const result = await getBackendSrv().post(`${BASE_URL}/${saID}/tokens`, token); + const result = await getBackendSrv().post(`${BASE_URL}/${saUid}/tokens`, token); onTokenCreated(result.key); - dispatch(loadServiceAccountTokens(saID)); + dispatch(loadServiceAccountTokens(saUid)); }; } -export function deleteServiceAccountToken(saID: number, id: number): ThunkResult { +export function deleteServiceAccountToken(saUid: string, id: number): ThunkResult { return async (dispatch) => { - await getBackendSrv().delete(`${BASE_URL}/${saID}/tokens/${id}`); - dispatch(loadServiceAccountTokens(saID)); + await getBackendSrv().delete(`${BASE_URL}/${saUid}/tokens/${id}`); + dispatch(loadServiceAccountTokens(saUid)); }; } -export function loadServiceAccountTokens(saID: number): ThunkResult { +export function loadServiceAccountTokens(saUid: string): ThunkResult { return async (dispatch) => { try { - const response = await getBackendSrv().get(`${BASE_URL}/${saID}/tokens`); + const response = await getBackendSrv().get(`${BASE_URL}/${saUid}/tokens`); dispatch(serviceAccountTokensLoaded(response)); } catch (error) { console.error(error); diff --git a/public/app/types/serviceaccount.ts b/public/app/types/serviceaccount.ts index a2ae3846c54..85c2fe1c0de 100644 --- a/public/app/types/serviceaccount.ts +++ b/public/app/types/serviceaccount.ts @@ -27,6 +27,7 @@ export interface ServiceAccount { export interface ServiceAccountDTO extends WithAccessControlMetadata { id: number; + uid: string; orgId: number; tokens: number; name: string; @@ -44,6 +45,7 @@ export interface ServiceAccountDTO extends WithAccessControlMetadata { export interface ServiceAccountCreateApiResponse { avatarUrl?: string; id: number; + uid: string; isDisabled: boolean; login: string; name: string; diff --git a/public/openapi3.json b/public/openapi3.json index be6e8bbce23..aae1bb28923 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -10394,6 +10394,10 @@ "example": 0, "format": "int64", "type": "integer" + }, + "uid": { + "example": "fe1xejlha91xce", + "type": "string" } }, "type": "object" @@ -10460,6 +10464,10 @@ "format": "int64", "type": "integer" }, + "uid": { + "example": "fe1xejlha91xce", + "type": "string" + }, "updatedAt": { "example": "2022-03-21T14:35:33Z", "format": "date-time",