AuthN: Add metrics to external service accounts management (#76789)

* AuthN: Add metrics to external service accounts management

* Add a new metric to count stored external service accounts

* Update variable names

Co-authored-by: linoman <2051016+linoman@users.noreply.github.com>

* Add test to SearchOrgServiceAccounts

* Add feature flags checks before registering and using the metrics

---------

Co-authored-by: linoman <2051016+linoman@users.noreply.github.com>
This commit is contained in:
Gabriel MABILLE 2023-10-24 15:54:14 +02:00 committed by GitHub
parent 07909464f1
commit 897e3a4dab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 352 additions and 30 deletions

View File

@ -270,9 +270,6 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
}
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
sess := dbSession.Table("org_user")
sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user")))
whereConditions := make([]string, 0)
whereParams := make([]any, 0)
@ -312,10 +309,38 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
whereConditions,
"is_disabled = ?")
whereParams = append(whereParams, s.sqlStore.GetDialect().BooleanStr(true))
case serviceaccounts.FilterOnlyExternal:
whereConditions = append(
whereConditions,
"login "+s.sqlStore.GetDialect().LikeStr()+" ?")
whereParams = append(whereParams, serviceaccounts.ServiceAccountPrefix+serviceaccounts.ExtSvcPrefix+"%")
default:
s.log.Warn("Invalid filter user for service account filtering", "service account search filtering", query.Filter)
}
// Count the number of accounts
serviceaccount := serviceaccounts.ServiceAccountDTO{}
countSess := dbSession.Table("org_user")
countSess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user")))
if len(whereConditions) > 0 {
countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)
}
count, err := countSess.Count(&serviceaccount)
if err != nil {
return err
}
searchResult.TotalCount = count
// Stop here if we only wanted to count the number of accounts
if query.CountOnly {
return nil
}
// Fetch service accounts
sess := dbSession.Table("org_user")
sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user")))
if len(whereConditions) > 0 {
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
}
@ -338,21 +363,6 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
if err := sess.Find(&searchResult.ServiceAccounts); err != nil {
return err
}
// get total
serviceaccount := serviceaccounts.ServiceAccountDTO{}
countSess := dbSession.Table("org_user")
sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user")))
if len(whereConditions) > 0 {
countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)
}
count, err := countSess.Count(&serviceaccount)
if err != nil {
return err
}
searchResult.TotalCount = count
return nil
})
if err != nil {

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey/apikeyimpl"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
@ -368,3 +369,129 @@ func TestStore_MigrateAllApiKeys(t *testing.T) {
})
}
}
func TestServiceAccountsStoreImpl_SearchOrgServiceAccounts(t *testing.T) {
initUsers := []tests.TestUser{
{Name: "satest-1", Role: string(org.RoleViewer), Login: "sa-satest-1", IsServiceAccount: true},
{Name: "usertest-2", Role: string(org.RoleEditor), Login: "usertest-2", IsServiceAccount: false},
{Name: "satest-3", Role: string(org.RoleEditor), Login: "sa-satest-3", IsServiceAccount: true},
{Name: "satest-4", Role: string(org.RoleAdmin), Login: "sa-satest-4", IsServiceAccount: true},
{Name: "extsvc-test-5", Role: string(org.RoleNone), Login: "sa-extsvc-test-5", IsServiceAccount: true},
{Name: "extsvc-test-6", Role: string(org.RoleNone), Login: "sa-extsvc-test-6", IsServiceAccount: true},
{Name: "extsvc-test-7", Role: string(org.RoleNone), Login: "sa-extsvc-test-7", IsServiceAccount: true},
{Name: "extsvc-test-8", Role: string(org.RoleNone), Login: "sa-extsvc-test-8", IsServiceAccount: true},
}
db, store := setupTestDatabase(t)
orgID := tests.SetupUsersServiceAccounts(t, db, initUsers)
userWithPerm := &user.SignedInUser{
OrgID: orgID,
Permissions: map[int64]map[string][]string{orgID: {serviceaccounts.ActionRead: {serviceaccounts.ScopeAll}}},
}
tt := []struct {
desc string
query *serviceaccounts.SearchOrgServiceAccountsQuery
expectedTotal int64 // Value of the result.TotalCount
expectedCount int // Length of the result.ServiceAccounts slice
expectedErr error
}{
{
desc: "should list all service accounts",
query: &serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: orgID,
SignedInUser: userWithPerm,
Filter: serviceaccounts.FilterIncludeAll,
},
expectedTotal: 7,
expectedCount: 7,
},
{
desc: "should list no service accounts without permissions",
query: &serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: orgID,
SignedInUser: &user.SignedInUser{
OrgID: orgID,
Permissions: map[int64]map[string][]string{orgID: {}},
},
Filter: serviceaccounts.FilterIncludeAll,
},
expectedTotal: 0,
expectedCount: 0,
},
{
desc: "should list one service accounts with restricted permissions",
query: &serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: orgID,
SignedInUser: &user.SignedInUser{
OrgID: orgID,
Permissions: map[int64]map[string][]string{orgID: {serviceaccounts.ActionRead: {
ac.Scope("serviceaccounts", "id", "1"),
ac.Scope("serviceaccounts", "id", "7"),
}}},
},
Filter: serviceaccounts.FilterIncludeAll,
},
expectedTotal: 2,
expectedCount: 2,
},
{
desc: "should list only external service accounts",
query: &serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: orgID,
SignedInUser: userWithPerm,
Filter: serviceaccounts.FilterOnlyExternal,
},
expectedTotal: 4,
expectedCount: 4,
},
{
desc: "should return service accounts with sa-satest login",
query: &serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: orgID,
Query: "sa-satest",
SignedInUser: userWithPerm,
Filter: serviceaccounts.FilterIncludeAll,
},
expectedTotal: 3,
expectedCount: 3,
},
{
desc: "should only count service accounts",
query: &serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: orgID,
SignedInUser: userWithPerm,
Filter: serviceaccounts.FilterIncludeAll,
CountOnly: true,
},
expectedTotal: 7,
expectedCount: 0,
},
{
desc: "should paginate result",
query: &serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: orgID,
Page: 4,
Limit: 2,
SignedInUser: userWithPerm,
Filter: serviceaccounts.FilterIncludeAll,
},
expectedTotal: 7,
expectedCount: 1,
},
}
for _, tc := range tt {
t.Run(tc.desc, func(t *testing.T) {
ctx := context.Background()
got, err := store.SearchOrgServiceAccounts(ctx, tc.query)
if tc.expectedErr != nil {
require.ErrorIs(t, err, tc.expectedErr)
return
}
require.Equal(t, tc.expectedTotal, got.TotalCount)
require.Len(t, got.ServiceAccounts, tc.expectedCount)
})
}
}

View File

@ -0,0 +1,68 @@
package extsvcaccounts
import (
"context"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/extsvcauth"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/user"
"github.com/prometheus/client_golang/prometheus"
)
type metrics struct {
storedCount prometheus.GaugeFunc
savedCount prometheus.Counter
deletedCount prometheus.Counter
}
func newMetrics(reg prometheus.Registerer, saSvc serviceaccounts.Service, logger log.Logger) *metrics {
var m metrics
m.storedCount = prometheus.NewGaugeFunc(
prometheus.GaugeOpts{
Namespace: metricsNamespace,
Name: "extsvc_total",
Help: "Number of external service accounts in store",
},
func() float64 {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
res, err := saSvc.SearchOrgServiceAccounts(ctx, &serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: extsvcauth.TmpOrgID,
Filter: serviceaccounts.FilterOnlyExternal,
CountOnly: true,
SignedInUser: &user.SignedInUser{
OrgID: extsvcauth.TmpOrgID,
Permissions: map[int64]map[string][]string{
extsvcauth.TmpOrgID: {serviceaccounts.ActionRead: {"serviceaccounts:id:*"}},
},
},
})
if err != nil {
logger.Error("Could not compute extsvc_total metric", "error", err)
return 0.0
}
return float64(res.TotalCount)
},
)
m.savedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: metricsNamespace,
Name: "extsvc_saved_total",
Help: "Number of external service accounts saved since start up.",
})
m.deletedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: metricsNamespace,
Name: "extsvc_deleted_total",
Help: "Number of external service accounts deleted since start up.",
})
if reg != nil {
reg.MustRegister(m.storedCount)
reg.MustRegister(m.savedCount)
reg.MustRegister(m.deletedCount)
}
return &m
}

View File

@ -7,6 +7,8 @@ import (
)
const (
metricsNamespace = "grafana"
kvStoreType = "extsvc-token"
// #nosec G101 - this is not a hardcoded secret
tokenNamePrefix = "extsvc-token"

View File

@ -4,6 +4,8 @@ import (
"context"
"errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/components/satokengen"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
@ -11,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/models/roletype"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/extsvcauth"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
@ -19,19 +22,29 @@ import (
type ExtSvcAccountsService struct {
acSvc ac.Service
features *featuremgmt.FeatureManager
logger log.Logger
metrics *metrics
saSvc sa.Service
skvStore kvstore.SecretsKVStore
}
func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc *manager.ServiceAccountsService, db db.DB, secretsSvc secrets.Service) *ExtSvcAccountsService {
func ProvideExtSvcAccountsService(acSvc ac.Service, db db.DB, features *featuremgmt.FeatureManager, reg prometheus.Registerer, saSvc *manager.ServiceAccountsService, secretsSvc secrets.Service) *ExtSvcAccountsService {
logger := log.New("serviceauth.extsvcaccounts")
return &ExtSvcAccountsService{
esa := &ExtSvcAccountsService{
acSvc: acSvc,
logger: logger,
saSvc: saSvc,
features: features,
skvStore: kvstore.NewSQLSecretsKVStore(db, secretsSvc, logger), // Using SQL store to avoid a cyclic dependency
}
// Register the metrics
if features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
esa.metrics = newMetrics(reg, saSvc, logger)
}
return esa
}
// RetrieveExtSvcAccount fetches an external service account by ID
@ -52,6 +65,12 @@ func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, org
// SaveExternalService creates, updates or delete a service account (and its token) with the requested permissions.
func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) {
// This is double proofing, we should never reach here anyway the flags have already been checked.
if !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services")
return nil, nil
}
if cmd == nil {
esa.logger.Warn("Received no input")
return nil, nil
@ -92,6 +111,12 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *
// ManageExtSvcAccount creates, updates or deletes the service account associated with an external service
func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *sa.ManageExtSvcAccountCmd) (int64, error) {
// This is double proofing, we should never reach here anyway the flags have already been checked.
if !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services")
return 0, nil
}
if cmd == nil {
esa.logger.Warn("Received no input")
return 0, nil
@ -111,6 +136,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *
"error", err.Error())
return 0, err
}
esa.metrics.deletedCount.Inc()
}
esa.logger.Info("Skipping service account creation",
"service", cmd.ExtSvcSlug,
@ -130,6 +156,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *
esa.logger.Error("Could not save service account", "service", cmd.ExtSvcSlug, "error", errSave.Error())
return 0, errSave
}
esa.metrics.savedCount.Inc()
return saID, nil
}

View File

@ -4,6 +4,9 @@ import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models/roletype"
@ -17,8 +20,6 @@ import (
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type TestEnv struct {
@ -39,9 +40,12 @@ func setupTestEnv(t *testing.T) *TestEnv {
SaSvc: &tests.MockServiceAccountService{},
SkvStore: kvstore.NewFakeSecretsKVStore(),
}
logger := log.New("extsvcaccounts.test")
env.S = &ExtSvcAccountsService{
acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt),
logger: log.New("extsvcaccounts.test"),
features: fmgt,
logger: logger,
metrics: newMetrics(nil, env.SaSvc, logger),
saSvc: env.SaSvc,
skvStore: env.SkvStore,
}

View File

@ -84,6 +84,8 @@ type ServiceAccountDTO struct {
OrgId int64 `json:"orgId" xorm:"org_id"`
// example: false
IsDisabled bool `json:"isDisabled" xorm:"is_disabled"`
// example: false
IsExternal bool `json:"isExternal,omitempty" xorm:"-"`
// example: Viewer
Role string `json:"role" xorm:"role"`
// example: 0
@ -112,6 +114,7 @@ type SearchOrgServiceAccountsQuery struct {
Filter ServiceAccountFilter
Page int
Limit int
CountOnly bool
SignedInUser identity.Requester
}
@ -166,6 +169,7 @@ const (
FilterOnlyExpiredTokens ServiceAccountFilter = "expiredTokens"
FilterOnlyDisabled ServiceAccountFilter = "disabled"
FilterIncludeAll ServiceAccountFilter = "all"
FilterOnlyExternal ServiceAccountFilter = "external"
)
type Stats struct {

View File

@ -100,6 +100,18 @@ func (s *ServiceAccountsProxy) UpdateServiceAccount(ctx context.Context, orgID,
return s.proxiedService.UpdateServiceAccount(ctx, orgID, serviceAccountID, saForm)
}
func (s *ServiceAccountsProxy) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) {
sa, err := s.proxiedService.SearchOrgServiceAccounts(ctx, query)
if err != nil {
return nil, err
}
for i := range sa.ServiceAccounts {
sa.ServiceAccounts[i].IsExternal = isExternalServiceAccount(sa.ServiceAccounts[i].Login)
}
return sa, nil
}
func isNameValid(name string) bool {
return !strings.HasPrefix(name, serviceaccounts.ExtSvcPrefix)
}

View File

@ -4,15 +4,18 @@ import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts"
"github.com/stretchr/testify/assert"
)
type FakeServiceAccountsService struct {
ExpectedServiceAccountProfileDTO *serviceaccounts.ServiceAccountProfileDTO
ExpectedServiceAccountProfileDTO *serviceaccounts.ServiceAccountProfileDTO
ExpectedSearchOrgServiceAccountsResult *serviceaccounts.SearchOrgServiceAccountsResult
}
var _ serviceaccounts.Service = (*FakeServiceAccountsService)(nil)
@ -47,7 +50,11 @@ func (f *FakeServiceAccountsService) AddServiceAccountToken(ctx context.Context,
return nil, nil
}
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
func (f *FakeServiceAccountsService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) {
return f.ExpectedSearchOrgServiceAccountsResult, nil
}
func TestProvideServiceAccount_crudServiceAccount(t *testing.T) {
testOrgId := int64(1)
testServiceAccountId := int64(1)
serviceMock := newServiceAccountServiceFake()
@ -257,3 +264,28 @@ func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
assert.True(t, isExternalServiceAccount("sa-extsvc-my-service-account"))
})
}
func TestProvideServiceAccount_SearchServiceAccount(t *testing.T) {
serviceMock := newServiceAccountServiceFake()
svc := ServiceAccountsProxy{
log.New("test"),
serviceMock,
}
t.Run("should mark external service accounts correctly", func(t *testing.T) {
serviceMock.ExpectedSearchOrgServiceAccountsResult = &serviceaccounts.SearchOrgServiceAccountsResult{
TotalCount: 2,
ServiceAccounts: []*serviceaccounts.ServiceAccountDTO{
{Login: "test"},
{Login: serviceaccounts.ServiceAccountPrefix + serviceaccounts.ExtSvcPrefix + "test"},
},
Page: 1,
PerPage: 2,
}
res, err := svc.SearchOrgServiceAccounts(context.Background(), &serviceaccounts.SearchOrgServiceAccountsQuery{OrgID: 1})
require.Len(t, res.ServiceAccounts, 2)
require.NoError(t, err)
require.False(t, res.ServiceAccounts[0].IsExternal)
require.True(t, res.ServiceAccounts[1].IsExternal)
})
}

View File

@ -13,14 +13,13 @@ Service accounts are used to authenticate API requests. They are not users and
do not have a password.
*/
type Service interface {
AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *AddServiceAccountTokenCommand) (*apikey.APIKey, error)
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)
RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error)
UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64,
saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error)
AddServiceAccountToken(ctx context.Context, serviceAccountID int64,
cmd *AddServiceAccountTokenCommand) (*apikey.APIKey, error)
SearchOrgServiceAccounts(ctx context.Context, query *SearchOrgServiceAccountsQuery) (*SearchOrgServiceAccountsResult, error)
UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error)
}
//go:generate mockery --name ExtSvcAccountsService --structname MockExtSvcAccountsService --output tests --outpkg tests --filename extsvcaccmock.go

View File

@ -102,3 +102,35 @@ func SetupApiKey(t *testing.T, sqlStore *sqlstore.SQLStore, testKey TestApiKey)
return key
}
// SetupUsersServiceAccounts creates in "test org" all users or service accounts passed in parameter
// To achieve this, it sets the AutoAssignOrg and AutoAssignOrgId settings.
func SetupUsersServiceAccounts(t *testing.T, sqlStore *sqlstore.SQLStore, testUsers []TestUser) (orgID int64) {
role := string(org.RoleNone)
quotaService := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg)
orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
require.NoError(t, err)
usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService())
require.NoError(t, err)
org, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{
Name: "test org",
})
require.NoError(t, err)
sqlStore.Cfg.AutoAssignOrg = true
sqlStore.Cfg.AutoAssignOrgId = int(org.ID)
for i := range testUsers {
_, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{
Login: testUsers[i].Login,
IsServiceAccount: testUsers[i].IsServiceAccount,
DefaultOrgRole: role,
Name: testUsers[i].Name,
OrgID: org.ID,
})
require.NoError(t, err)
}
return org.ID
}

View File

@ -50,3 +50,8 @@ func (s *MockServiceAccountService) UpdateServiceAccount(ctx context.Context, or
mockedArgs := s.Called(ctx, orgID, serviceAccountID)
return mockedArgs.Get(0).(*serviceaccounts.ServiceAccountProfileDTO), mockedArgs.Error(1)
}
func (s *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) {
mockedArgs := s.Called(ctx, query)
return mockedArgs.Get(0).(*serviceaccounts.SearchOrgServiceAccountsResult), mockedArgs.Error(1)
}