mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ExtSvcAuth: Clean up orphaned external services on start up (#77951)
* Plugin: Remove external service on plugin removal * Early exit no service account * Add log * WIP * Cable OAuth2Server client removal * Move function lower * Add function to test removal * Add test to RemoveExternalService * Test RemoveExtSvcAccount * remove apostrophy in comment * Add cfg to plugin installer to check features * Add feature flag check in the service registration service * Comments * Move metrics Inc * Initialize map * Reorder * Initialize mutex as well * Add HasExternalService as suggested * WIP: CleanUpOrphanedExternalServices * Commit suggestion Co-authored-by: linoman <2051016+linoman@users.noreply.github.com> * Nit on test. Co-authored-by: linoman <2051016+linoman@users.noreply.github.com> * oauthserver return names * Name is not Slug * Use plugin ID not slug * Add background job * remove negation on feature check * Add test to the CleanUp function * Test GetExternalServiceNames * rename test * Add test for ExtSvcAccountsService_GetExternalServiceNames * Add a todo * Add todo * Option based on mix * Rewrite a bit the comment * Opinionated choice use slugs instead of names everywhere * Nit. * Comments and re-ordering * Comment * Add log * Add context --------- Co-authored-by: linoman <2051016+linoman@users.noreply.github.com>
This commit is contained in:
parent
6b6b209e1c
commit
ba717454e1
@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
extsvcreg "github.com/grafana/grafana/pkg/services/extsvcauth/registry"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/grpcserver"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
@ -58,7 +59,7 @@ func ProvideBackgroundServiceRegistry(
|
||||
bundleService *supportbundlesimpl.Service, publicDashboardsMetric *publicdashboardsmetric.Service,
|
||||
keyRetriever *dynamic.KeyRetriever, dynamicAngularDetectorsProvider *angulardetectorsprovider.Dynamic,
|
||||
grafanaAPIServer grafanaapiserver.Service,
|
||||
anon *anonimpl.AnonDeviceService,
|
||||
anon *anonimpl.AnonDeviceService, reg *extsvcreg.Registry,
|
||||
// Need to make sure these are initialized, is there a better place to put them?
|
||||
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
||||
_ serviceaccounts.Service, _ *guardian.Provider,
|
||||
@ -100,6 +101,7 @@ func ProvideBackgroundServiceRegistry(
|
||||
dynamicAngularDetectorsProvider,
|
||||
grafanaAPIServer,
|
||||
anon,
|
||||
reg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -16,10 +16,14 @@ const (
|
||||
|
||||
type AuthProvider string
|
||||
|
||||
//go:generate mockery --name ExternalServiceRegistry --structname ExternalServiceRegistryMock --output tests --outpkg tests --filename extsvcregmock.go
|
||||
type ExternalServiceRegistry interface {
|
||||
// HasExternalService returns whether an external service has been saved with that name.
|
||||
HasExternalService(ctx context.Context, name string) (bool, error)
|
||||
|
||||
// GetExternalServiceNames returns the names of external services registered in store.
|
||||
GetExternalServiceNames(ctx context.Context) ([]string, error)
|
||||
|
||||
// RemoveExternalService removes an external service and its associated resources from the database (ex: service account, token).
|
||||
RemoveExternalService(ctx context.Context, name string) error
|
||||
|
||||
|
@ -49,6 +49,7 @@ type OAuth2Server interface {
|
||||
type Store interface {
|
||||
DeleteExternalService(ctx context.Context, id string) error
|
||||
GetExternalService(ctx context.Context, id string) (*OAuthExternalService, error)
|
||||
GetExternalServiceNames(ctx context.Context) ([]string, error)
|
||||
GetExternalServiceByName(ctx context.Context, name string) (*OAuthExternalService, error)
|
||||
GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error)
|
||||
RegisterExternalService(ctx context.Context, client *OAuthExternalService) error
|
||||
|
@ -193,6 +193,17 @@ func (s *OAuth2ServiceImpl) setClientUser(ctx context.Context, client *oauthserv
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExternalServiceNames get the names of External Service in store
|
||||
func (s *OAuth2ServiceImpl) GetExternalServiceNames(ctx context.Context) ([]string, error) {
|
||||
s.logger.Debug("Get external service names from store")
|
||||
res, err := s.sqlstore.GetExternalServiceNames(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("Could not fetch clients from store", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *OAuth2ServiceImpl) RemoveExternalService(ctx context.Context, name string) error {
|
||||
s.logger.Info("Remove external service", "service", name)
|
||||
|
||||
@ -228,19 +239,20 @@ func (s *OAuth2ServiceImpl) SaveExternalService(ctx context.Context, registratio
|
||||
s.logger.Warn("RegisterExternalService called without registration")
|
||||
return nil, nil
|
||||
}
|
||||
s.logger.Info("Registering external service", "external service name", registration.Name)
|
||||
slug := registration.Name
|
||||
s.logger.Info("Registering external service", "external service", slug)
|
||||
|
||||
// Check if the client already exists in store
|
||||
client, errFetchExtSvc := s.sqlstore.GetExternalServiceByName(ctx, registration.Name)
|
||||
client, errFetchExtSvc := s.sqlstore.GetExternalServiceByName(ctx, slug)
|
||||
if errFetchExtSvc != nil && !errors.Is(errFetchExtSvc, oauthserver.ErrClientNotFound) {
|
||||
s.logger.Error("Error fetching service", "external service", registration.Name, "error", errFetchExtSvc)
|
||||
s.logger.Error("Error fetching service", "external service", slug, "error", errFetchExtSvc)
|
||||
return nil, errFetchExtSvc
|
||||
}
|
||||
// Otherwise, create a new client
|
||||
if client == nil {
|
||||
s.logger.Debug("External service does not yet exist", "external service name", registration.Name)
|
||||
s.logger.Debug("External service does not yet exist", "external service", slug)
|
||||
client = &oauthserver.OAuthExternalService{
|
||||
Name: registration.Name,
|
||||
Name: slug,
|
||||
ServiceAccountID: oauthserver.NoServiceAccountID,
|
||||
Audiences: s.cfg.AppURL,
|
||||
}
|
||||
|
@ -25,6 +25,10 @@ func (s *FakeService) GetExternalService(ctx context.Context, id string) (*oauth
|
||||
return s.ExpectedClient, s.ExpectedErr
|
||||
}
|
||||
|
||||
func (s *FakeService) GetExternalServiceNames(ctx context.Context) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *FakeService) RemoveExternalService(ctx context.Context, name string) error {
|
||||
return s.ExpectedErr
|
||||
}
|
||||
|
@ -82,6 +82,32 @@ func (_m *MockStore) GetExternalServiceByName(ctx context.Context, name string)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetExternalServiceNames provides a mock function with given fields: ctx
|
||||
func (_m *MockStore) GetExternalServiceNames(ctx context.Context) ([]string, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []string); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetExternalServicePublicKey provides a mock function with given fields: ctx, clientID
|
||||
func (_m *MockStore) GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error) {
|
||||
ret := _m.Called(ctx, clientID)
|
||||
|
@ -213,6 +213,17 @@ func getExternalServiceByName(sess *db.Session, name string) (*oauthserver.OAuth
|
||||
return res, errPerm
|
||||
}
|
||||
|
||||
// FIXME: If we ever do a search method remove that method
|
||||
func (s *store) GetExternalServiceNames(ctx context.Context) ([]string, error) {
|
||||
res := []string{}
|
||||
|
||||
err := s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(`SELECT name FROM oauth_client`).Find(&res)
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *store) UpdateExternalServiceGrantTypes(ctx context.Context, clientID, grantTypes string) error {
|
||||
if clientID == "" {
|
||||
return oauthserver.ErrClientRequiredID
|
||||
|
@ -435,6 +435,32 @@ func TestStore_RemoveExternalService(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_store_GetExternalServiceNames(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client1 := oauthserver.OAuthExternalService{
|
||||
Name: "my-external-service",
|
||||
ClientID: "ClientID",
|
||||
ImpersonatePermissions: []accesscontrol.Permission{},
|
||||
}
|
||||
client2 := oauthserver.OAuthExternalService{
|
||||
Name: "my-external-service-2",
|
||||
ClientID: "ClientID2",
|
||||
ImpersonatePermissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "folders:*"},
|
||||
{Action: "dashboards:read", Scope: "dashboards:*"},
|
||||
},
|
||||
}
|
||||
|
||||
// Init store
|
||||
s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})}
|
||||
require.NoError(t, s.SaveExternalService(context.Background(), &client1))
|
||||
require.NoError(t, s.SaveExternalService(context.Background(), &client2))
|
||||
|
||||
got, err := s.GetExternalServiceNames(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{"my-external-service", "my-external-service-2"}, got)
|
||||
}
|
||||
|
||||
func compareClientToStored(t *testing.T, s *store, wanted *oauthserver.OAuthExternalService) {
|
||||
ctx := context.Background()
|
||||
stored, err := s.GetExternalService(ctx, wanted.ClientID)
|
||||
|
@ -35,12 +35,51 @@ func ProvideExtSvcRegistry(oauthServer *oasimpl.OAuth2ServiceImpl, saSvc *extsvc
|
||||
}
|
||||
}
|
||||
|
||||
// CleanUpOrphanedExternalServices remove external services present in store that have not been registered on startup.
|
||||
func (r *Registry) CleanUpOrphanedExternalServices(ctx context.Context) error {
|
||||
extsvcs, err := r.retrieveExtSvcProviders(ctx)
|
||||
if err != nil {
|
||||
r.logger.Error("Could not retrieve external services from store", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
for name, provider := range extsvcs {
|
||||
// The service did not register this time. Removed.
|
||||
if _, ok := r.extSvcProviders[slugify.Slugify(name)]; !ok {
|
||||
r.logger.Info("Detected removed External Service", "service", name, "provider", provider)
|
||||
switch provider {
|
||||
case extsvcauth.ServiceAccounts:
|
||||
if err := r.saReg.RemoveExternalService(ctx, name); err != nil {
|
||||
return err
|
||||
}
|
||||
case extsvcauth.OAuth2Server:
|
||||
if err := r.oauthReg.RemoveExternalService(ctx, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasExternalService returns whether an external service has been saved with that name.
|
||||
func (r *Registry) HasExternalService(ctx context.Context, name string) (bool, error) {
|
||||
_, ok := r.extSvcProviders[slugify.Slugify(name)]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// GetExternalServiceNames returns the list of external services registered in store.
|
||||
func (r *Registry) GetExternalServiceNames(ctx context.Context) ([]string, error) {
|
||||
extSvcProviders, err := r.retrieveExtSvcProviders(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := []string{}
|
||||
for s := range extSvcProviders {
|
||||
names = append(names, s)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// RemoveExternalService removes an external service and its associated resources from the database (ex: service account, token).
|
||||
func (r *Registry) RemoveExternalService(ctx context.Context, name string) error {
|
||||
provider, ok := r.extSvcProviders[slugify.Slugify(name)]
|
||||
@ -97,3 +136,34 @@ func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.Exte
|
||||
return nil, extsvcauth.ErrUnknownProvider.Errorf("unknow provider '%v'", cmd.AuthProvider)
|
||||
}
|
||||
}
|
||||
|
||||
// retrieveExtSvcProviders fetches external services from store and map their associated provider
|
||||
func (r *Registry) retrieveExtSvcProviders(ctx context.Context) (map[string]extsvcauth.AuthProvider, error) {
|
||||
extsvcs := map[string]extsvcauth.AuthProvider{}
|
||||
if r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
|
||||
names, err := r.saReg.GetExternalServiceNames(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range names {
|
||||
extsvcs[names[i]] = extsvcauth.ServiceAccounts
|
||||
}
|
||||
}
|
||||
// Important to run this second as the OAuth server uses External Service Accounts as well.
|
||||
if r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) {
|
||||
names, err := r.oauthReg.GetExternalServiceNames(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range names {
|
||||
extsvcs[names[i]] = extsvcauth.OAuth2Server
|
||||
}
|
||||
}
|
||||
return extsvcs, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Run(ctx context.Context) error {
|
||||
// This is a one-time background job.
|
||||
// Cleans up external services that have not been registered this time.
|
||||
return r.CleanUpOrphanedExternalServices(ctx)
|
||||
}
|
||||
|
122
pkg/services/extsvcauth/registry/service_test.go
Normal file
122
pkg/services/extsvcauth/registry/service_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
||||
"github.com/grafana/grafana/pkg/services/extsvcauth/tests"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type TestEnv struct {
|
||||
r *Registry
|
||||
oauthReg *tests.ExternalServiceRegistryMock
|
||||
saReg *tests.ExternalServiceRegistryMock
|
||||
}
|
||||
|
||||
func setupTestEnv(t *testing.T) *TestEnv {
|
||||
env := TestEnv{}
|
||||
env.oauthReg = tests.NewExternalServiceRegistryMock(t)
|
||||
env.saReg = tests.NewExternalServiceRegistryMock(t)
|
||||
env.r = &Registry{
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth, featuremgmt.FlagExternalServiceAccounts),
|
||||
logger: log.New("extsvcauth.registry.test"),
|
||||
oauthReg: env.oauthReg,
|
||||
saReg: env.saReg,
|
||||
extSvcProviders: map[string]extsvcauth.AuthProvider{},
|
||||
lock: sync.Mutex{},
|
||||
}
|
||||
return &env
|
||||
}
|
||||
|
||||
func TestRegistry_CleanUpOrphanedExternalServices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
init func(*TestEnv)
|
||||
}{
|
||||
{
|
||||
name: "should not clean up when every service registered",
|
||||
init: func(te *TestEnv) {
|
||||
// Have registered two services one requested a service account, the other requested to be an oauth client
|
||||
te.r.extSvcProviders = map[string]extsvcauth.AuthProvider{"sa-svc": extsvcauth.ServiceAccounts, "oauth-svc": extsvcauth.OAuth2Server}
|
||||
|
||||
te.oauthReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"oauth-svc"}, nil)
|
||||
// Also return the external service account attached to the OAuth Server
|
||||
te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "oauth-svc"}, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should clean up an orphaned service account",
|
||||
init: func(te *TestEnv) {
|
||||
// Have registered two services one requested a service account, the other requested to be an oauth client
|
||||
te.r.extSvcProviders = map[string]extsvcauth.AuthProvider{"sa-svc": extsvcauth.ServiceAccounts, "oauth-svc": extsvcauth.OAuth2Server}
|
||||
|
||||
te.oauthReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"oauth-svc"}, nil)
|
||||
// Also return the external service account attached to the OAuth Server
|
||||
te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "orphaned-sa-svc", "oauth-svc"}, nil)
|
||||
|
||||
te.saReg.On("RemoveExternalService", mock.Anything, "orphaned-sa-svc").Return(nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should clean up an orphaned OAuth Client",
|
||||
init: func(te *TestEnv) {
|
||||
// Have registered two services one requested a service account, the other requested to be an oauth client
|
||||
te.r.extSvcProviders = map[string]extsvcauth.AuthProvider{"sa-svc": extsvcauth.ServiceAccounts, "oauth-svc": extsvcauth.OAuth2Server}
|
||||
|
||||
te.oauthReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"oauth-svc", "orphaned-oauth-svc"}, nil)
|
||||
// Also return the external service account attached to the OAuth Server
|
||||
te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "orphaned-oauth-svc", "oauth-svc"}, nil)
|
||||
|
||||
te.oauthReg.On("RemoveExternalService", mock.Anything, "orphaned-oauth-svc").Return(nil)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
tt.init(env)
|
||||
|
||||
err := env.r.CleanUpOrphanedExternalServices(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
env.oauthReg.AssertExpectations(t)
|
||||
env.saReg.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_GetExternalServiceNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
init func(*TestEnv)
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "should deduplicate names",
|
||||
init: func(te *TestEnv) {
|
||||
te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "oauth-svc"}, nil)
|
||||
te.oauthReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"oauth-svc"}, nil)
|
||||
},
|
||||
want: []string{"sa-svc", "oauth-svc"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
tt.init(env)
|
||||
|
||||
names, err := env.r.GetExternalServiceNames(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, tt.want, names)
|
||||
|
||||
env.oauthReg.AssertExpectations(t)
|
||||
env.saReg.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
119
pkg/services/extsvcauth/tests/extsvcregmock.go
Normal file
119
pkg/services/extsvcauth/tests/extsvcregmock.go
Normal file
@ -0,0 +1,119 @@
|
||||
// Code generated by mockery v2.35.2. DO NOT EDIT.
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
extsvcauth "github.com/grafana/grafana/pkg/services/extsvcauth"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ExternalServiceRegistryMock is an autogenerated mock type for the ExternalServiceRegistry type
|
||||
type ExternalServiceRegistryMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetExternalServiceNames provides a mock function with given fields: ctx
|
||||
func (_m *ExternalServiceRegistryMock) GetExternalServiceNames(ctx context.Context) ([]string, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []string); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasExternalService provides a mock function with given fields: ctx, name
|
||||
func (_m *ExternalServiceRegistryMock) HasExternalService(ctx context.Context, name string) (bool, error) {
|
||||
ret := _m.Called(ctx, name)
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
|
||||
return rf(ctx, name)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
|
||||
r0 = rf(ctx, name)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RemoveExternalService provides a mock function with given fields: ctx, name
|
||||
func (_m *ExternalServiceRegistryMock) RemoveExternalService(ctx context.Context, name string) error {
|
||||
ret := _m.Called(ctx, name)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, name)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SaveExternalService provides a mock function with given fields: ctx, cmd
|
||||
func (_m *ExternalServiceRegistryMock) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) {
|
||||
ret := _m.Called(ctx, cmd)
|
||||
|
||||
var r0 *extsvcauth.ExternalService
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error)); ok {
|
||||
return rf(ctx, cmd)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *extsvcauth.ExternalServiceRegistration) *extsvcauth.ExternalService); ok {
|
||||
r0 = rf(ctx, cmd)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*extsvcauth.ExternalService)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *extsvcauth.ExternalServiceRegistration) error); ok {
|
||||
r1 = rf(ctx, cmd)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewExternalServiceRegistryMock creates a new instance of ExternalServiceRegistryMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewExternalServiceRegistryMock(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *ExternalServiceRegistryMock {
|
||||
mock := &ExternalServiceRegistryMock{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -7,7 +7,6 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -30,15 +29,10 @@ func newMetrics(reg prometheus.Registerer, saSvc serviceaccounts.Service, logger
|
||||
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:*"}},
|
||||
},
|
||||
},
|
||||
OrgID: extsvcauth.TmpOrgID,
|
||||
Filter: serviceaccounts.FilterOnlyExternal,
|
||||
CountOnly: true,
|
||||
SignedInUser: extsvcuser,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("Could not compute extsvc_total metric", "error", err)
|
||||
|
@ -3,6 +3,9 @@ package extsvcaccounts
|
||||
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/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
@ -22,6 +25,13 @@ var (
|
||||
ErrCannotListTokens = errutil.BadRequest("extsvcaccounts.ErrCannotListTokens", errutil.WithPublicMessage("cannot list external service account tokens"))
|
||||
ErrCredentialsNotFound = errutil.NotFound("extsvcaccounts.credentials-not-found")
|
||||
ErrInvalidName = errutil.BadRequest("extsvcaccounts.ErrInvalidName", errutil.WithPublicMessage("only external service account names can be prefixed with 'extsvc-'"))
|
||||
|
||||
extsvcuser = &user.SignedInUser{
|
||||
OrgID: extsvcauth.TmpOrgID,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
extsvcauth.TmpOrgID: {serviceaccounts.ActionRead: {"serviceaccounts:id:*"}},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Credentials represents the credentials associated to an external service
|
||||
|
@ -3,6 +3,7 @@ package extsvcaccounts
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
@ -92,6 +93,28 @@ func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, org
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetExternalServiceNames get the names of External Service in store
|
||||
func (esa *ExtSvcAccountsService) GetExternalServiceNames(ctx context.Context) ([]string, error) {
|
||||
esa.logger.Debug("Get external service names from store")
|
||||
sas, err := esa.saSvc.SearchOrgServiceAccounts(ctx, &sa.SearchOrgServiceAccountsQuery{
|
||||
OrgID: extsvcauth.TmpOrgID,
|
||||
Filter: sa.FilterOnlyExternal,
|
||||
SignedInUser: extsvcuser,
|
||||
})
|
||||
if err != nil {
|
||||
esa.logger.Error("Could not fetch external service accounts from store", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
if sas == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
res := make([]string, len(sas.ServiceAccounts))
|
||||
for i := range sas.ServiceAccounts {
|
||||
res[i] = strings.TrimPrefix(sas.ServiceAccounts[i].Name, sa.ExtSvcPrefix)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -135,7 +158,7 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *
|
||||
"error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return &extsvcauth.ExternalService{Name: cmd.Name, ID: slug, Secret: token}, nil
|
||||
return &extsvcauth.ExternalService{Name: slug, ID: slug, Secret: token}, nil
|
||||
}
|
||||
|
||||
func (esa *ExtSvcAccountsService) RemoveExternalService(ctx context.Context, name string) error {
|
||||
|
@ -4,9 +4,6 @@ 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"
|
||||
@ -20,6 +17,8 @@ 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 {
|
||||
@ -441,3 +440,68 @@ func TestExtSvcAccountsService_RemoveExtSvcAccount(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtSvcAccountsService_GetExternalServiceNames(t *testing.T) {
|
||||
sa1 := sa.ServiceAccountDTO{
|
||||
Id: 1,
|
||||
Name: sa.ExtSvcPrefix + "sa-svc-1",
|
||||
Login: sa.ServiceAccountPrefix + sa.ExtSvcPrefix + "sa-svc-1",
|
||||
OrgId: extsvcauth.TmpOrgID,
|
||||
}
|
||||
sa2 := sa.ServiceAccountDTO{
|
||||
Id: 2,
|
||||
Name: sa.ExtSvcPrefix + "sa-svc-2",
|
||||
Login: sa.ServiceAccountPrefix + sa.ExtSvcPrefix + "sa-svc-2",
|
||||
OrgId: extsvcauth.TmpOrgID,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
init func(env *TestEnv)
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "should return names",
|
||||
init: func(env *TestEnv) {
|
||||
env.SaSvc.On("SearchOrgServiceAccounts", mock.Anything, mock.MatchedBy(func(cmd *sa.SearchOrgServiceAccountsQuery) bool {
|
||||
return cmd.OrgID == extsvcauth.TmpOrgID &&
|
||||
cmd.Filter == sa.FilterOnlyExternal &&
|
||||
len(cmd.SignedInUser.GetPermissions()[sa.ActionRead]) > 0
|
||||
})).Return(&sa.SearchOrgServiceAccountsResult{
|
||||
TotalCount: 2,
|
||||
ServiceAccounts: []*sa.ServiceAccountDTO{&sa1, &sa2},
|
||||
Page: 1,
|
||||
PerPage: 2,
|
||||
}, nil)
|
||||
},
|
||||
want: []string{"sa-svc-1", "sa-svc-2"},
|
||||
},
|
||||
{
|
||||
name: "should handle nil search",
|
||||
init: func(env *TestEnv) {
|
||||
env.SaSvc.On("SearchOrgServiceAccounts", mock.Anything, mock.MatchedBy(func(cmd *sa.SearchOrgServiceAccountsQuery) bool {
|
||||
return cmd.OrgID == extsvcauth.TmpOrgID &&
|
||||
cmd.Filter == sa.FilterOnlyExternal &&
|
||||
len(cmd.SignedInUser.GetPermissions()[sa.ActionRead]) > 0
|
||||
})).Return(nil, nil)
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
if tt.init != nil {
|
||||
tt.init(env)
|
||||
}
|
||||
|
||||
got, err := env.S.GetExternalServiceNames(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.ElementsMatch(t, tt.want, got)
|
||||
env.SaSvc.AssertExpectations(t)
|
||||
env.AcStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user