ServiceAccounts: Allow use of UIDs for Service Account routes (#95401)

This commit is contained in:
Jo
2024-10-28 10:49:48 +01:00
committed by GitHub
parent 39d53e65a7
commit ed1ad34f9b
30 changed files with 254 additions and 102 deletions
@@ -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{
+40 -12
View File
@@ -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)
}
+8 -2
View File
@@ -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)
}
@@ -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)
+25 -7
View File
@@ -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",
)
@@ -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)
@@ -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
}
@@ -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
}
@@ -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")
@@ -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
}
@@ -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)
+10
View File
@@ -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
@@ -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
}
@@ -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)
})
@@ -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)
}
@@ -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
+1 -1
View File
@@ -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
}
+8 -8
View File
@@ -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)
}
+8
View File
@@ -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",
+8
View File
@@ -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",
@@ -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]
);
@@ -61,6 +61,7 @@ const setup = (propOverrides: Partial<Props>) => {
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);
});
});
@@ -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 = () => {
@@ -122,7 +122,7 @@ const getCellContent = (
if (isLoading) {
return columnName === 'avatarUrl' ? <Skeleton circle width={24} height={24} /> : <Skeleton width={100} />;
}
const href = `/org/serviceaccounts/${original.id}`;
const href = `/org/serviceaccounts/${original.uid}`;
const ariaLabel = `Edit service account's ${name} details`;
switch (columnName) {
case 'avatarUrl':
@@ -65,6 +65,7 @@ const setup = (propOverrides: Partial<Props>) => {
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');
});
});
@@ -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);
}
};
@@ -84,27 +84,27 @@ const fetchServiceAccountsWithDebounce = debounce((dispatch) => dispatch(fetchSe
export function updateServiceAccount(serviceAccount: ServiceAccountDTO): ThunkResult<void> {
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<void> {
export function deleteServiceAccount(serviceAccountUid: string): ThunkResult<void> {
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<void> {
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());
};
@@ -13,11 +13,11 @@ import {
const BASE_URL = `/api/serviceaccounts`;
export function loadServiceAccount(saID: number): ThunkResult<void> {
export function loadServiceAccount(saUid: string): ThunkResult<void> {
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<void> {
export function updateServiceAccount(serviceAccount: ServiceAccountDTO): ThunkResult<void> {
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<void> {
export function deleteServiceAccount(serviceAccountUid: string): ThunkResult<void> {
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<void> {
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<void> {
export function deleteServiceAccountToken(saUid: string, id: number): ThunkResult<void> {
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<void> {
export function loadServiceAccountTokens(saUid: string): ThunkResult<void> {
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);
+2
View File
@@ -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;
+8
View File
@@ -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",