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/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
"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"
|
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
|
||||||
"github.com/grafana/grafana/pkg/services/grpcserver"
|
"github.com/grafana/grafana/pkg/services/grpcserver"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
@ -58,7 +59,7 @@ func ProvideBackgroundServiceRegistry(
|
|||||||
bundleService *supportbundlesimpl.Service, publicDashboardsMetric *publicdashboardsmetric.Service,
|
bundleService *supportbundlesimpl.Service, publicDashboardsMetric *publicdashboardsmetric.Service,
|
||||||
keyRetriever *dynamic.KeyRetriever, dynamicAngularDetectorsProvider *angulardetectorsprovider.Dynamic,
|
keyRetriever *dynamic.KeyRetriever, dynamicAngularDetectorsProvider *angulardetectorsprovider.Dynamic,
|
||||||
grafanaAPIServer grafanaapiserver.Service,
|
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?
|
// Need to make sure these are initialized, is there a better place to put them?
|
||||||
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
||||||
_ serviceaccounts.Service, _ *guardian.Provider,
|
_ serviceaccounts.Service, _ *guardian.Provider,
|
||||||
@ -100,6 +101,7 @@ func ProvideBackgroundServiceRegistry(
|
|||||||
dynamicAngularDetectorsProvider,
|
dynamicAngularDetectorsProvider,
|
||||||
grafanaAPIServer,
|
grafanaAPIServer,
|
||||||
anon,
|
anon,
|
||||||
|
reg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,10 +16,14 @@ const (
|
|||||||
|
|
||||||
type AuthProvider string
|
type AuthProvider string
|
||||||
|
|
||||||
|
//go:generate mockery --name ExternalServiceRegistry --structname ExternalServiceRegistryMock --output tests --outpkg tests --filename extsvcregmock.go
|
||||||
type ExternalServiceRegistry interface {
|
type ExternalServiceRegistry interface {
|
||||||
// HasExternalService returns whether an external service has been saved with that name.
|
// HasExternalService returns whether an external service has been saved with that name.
|
||||||
HasExternalService(ctx context.Context, name string) (bool, error)
|
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 removes an external service and its associated resources from the database (ex: service account, token).
|
||||||
RemoveExternalService(ctx context.Context, name string) error
|
RemoveExternalService(ctx context.Context, name string) error
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ type OAuth2Server interface {
|
|||||||
type Store interface {
|
type Store interface {
|
||||||
DeleteExternalService(ctx context.Context, id string) error
|
DeleteExternalService(ctx context.Context, id string) error
|
||||||
GetExternalService(ctx context.Context, id string) (*OAuthExternalService, error)
|
GetExternalService(ctx context.Context, id string) (*OAuthExternalService, error)
|
||||||
|
GetExternalServiceNames(ctx context.Context) ([]string, error)
|
||||||
GetExternalServiceByName(ctx context.Context, name string) (*OAuthExternalService, error)
|
GetExternalServiceByName(ctx context.Context, name string) (*OAuthExternalService, error)
|
||||||
GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error)
|
GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error)
|
||||||
RegisterExternalService(ctx context.Context, client *OAuthExternalService) error
|
RegisterExternalService(ctx context.Context, client *OAuthExternalService) error
|
||||||
|
@ -193,6 +193,17 @@ func (s *OAuth2ServiceImpl) setClientUser(ctx context.Context, client *oauthserv
|
|||||||
return nil
|
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 {
|
func (s *OAuth2ServiceImpl) RemoveExternalService(ctx context.Context, name string) error {
|
||||||
s.logger.Info("Remove external service", "service", name)
|
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")
|
s.logger.Warn("RegisterExternalService called without registration")
|
||||||
return nil, nil
|
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
|
// 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) {
|
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
|
return nil, errFetchExtSvc
|
||||||
}
|
}
|
||||||
// Otherwise, create a new client
|
// Otherwise, create a new client
|
||||||
if client == nil {
|
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{
|
client = &oauthserver.OAuthExternalService{
|
||||||
Name: registration.Name,
|
Name: slug,
|
||||||
ServiceAccountID: oauthserver.NoServiceAccountID,
|
ServiceAccountID: oauthserver.NoServiceAccountID,
|
||||||
Audiences: s.cfg.AppURL,
|
Audiences: s.cfg.AppURL,
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,10 @@ func (s *FakeService) GetExternalService(ctx context.Context, id string) (*oauth
|
|||||||
return s.ExpectedClient, s.ExpectedErr
|
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 {
|
func (s *FakeService) RemoveExternalService(ctx context.Context, name string) error {
|
||||||
return s.ExpectedErr
|
return s.ExpectedErr
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,32 @@ func (_m *MockStore) GetExternalServiceByName(ctx context.Context, name string)
|
|||||||
return r0, r1
|
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
|
// GetExternalServicePublicKey provides a mock function with given fields: ctx, clientID
|
||||||
func (_m *MockStore) GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error) {
|
func (_m *MockStore) GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error) {
|
||||||
ret := _m.Called(ctx, clientID)
|
ret := _m.Called(ctx, clientID)
|
||||||
|
@ -213,6 +213,17 @@ func getExternalServiceByName(sess *db.Session, name string) (*oauthserver.OAuth
|
|||||||
return res, errPerm
|
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 {
|
func (s *store) UpdateExternalServiceGrantTypes(ctx context.Context, clientID, grantTypes string) error {
|
||||||
if clientID == "" {
|
if clientID == "" {
|
||||||
return oauthserver.ErrClientRequiredID
|
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) {
|
func compareClientToStored(t *testing.T, s *store, wanted *oauthserver.OAuthExternalService) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
stored, err := s.GetExternalService(ctx, wanted.ClientID)
|
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.
|
// HasExternalService returns whether an external service has been saved with that name.
|
||||||
func (r *Registry) HasExternalService(ctx context.Context, name string) (bool, error) {
|
func (r *Registry) HasExternalService(ctx context.Context, name string) (bool, error) {
|
||||||
_, ok := r.extSvcProviders[slugify.Slugify(name)]
|
_, ok := r.extSvcProviders[slugify.Slugify(name)]
|
||||||
return ok, nil
|
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).
|
// 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 {
|
func (r *Registry) RemoveExternalService(ctx context.Context, name string) error {
|
||||||
provider, ok := r.extSvcProviders[slugify.Slugify(name)]
|
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)
|
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/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"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)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
res, err := saSvc.SearchOrgServiceAccounts(ctx, &serviceaccounts.SearchOrgServiceAccountsQuery{
|
res, err := saSvc.SearchOrgServiceAccounts(ctx, &serviceaccounts.SearchOrgServiceAccountsQuery{
|
||||||
OrgID: extsvcauth.TmpOrgID,
|
OrgID: extsvcauth.TmpOrgID,
|
||||||
Filter: serviceaccounts.FilterOnlyExternal,
|
Filter: serviceaccounts.FilterOnlyExternal,
|
||||||
CountOnly: true,
|
CountOnly: true,
|
||||||
SignedInUser: &user.SignedInUser{
|
SignedInUser: extsvcuser,
|
||||||
OrgID: extsvcauth.TmpOrgID,
|
|
||||||
Permissions: map[int64]map[string][]string{
|
|
||||||
extsvcauth.TmpOrgID: {serviceaccounts.ActionRead: {"serviceaccounts:id:*"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Could not compute extsvc_total metric", "error", err)
|
logger.Error("Could not compute extsvc_total metric", "error", err)
|
||||||
|
@ -3,6 +3,9 @@ package extsvcaccounts
|
|||||||
import (
|
import (
|
||||||
"github.com/grafana/grafana/pkg/models/roletype"
|
"github.com/grafana/grafana/pkg/models/roletype"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
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"
|
"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"))
|
ErrCannotListTokens = errutil.BadRequest("extsvcaccounts.ErrCannotListTokens", errutil.WithPublicMessage("cannot list external service account tokens"))
|
||||||
ErrCredentialsNotFound = errutil.NotFound("extsvcaccounts.credentials-not-found")
|
ErrCredentialsNotFound = errutil.NotFound("extsvcaccounts.credentials-not-found")
|
||||||
ErrInvalidName = errutil.BadRequest("extsvcaccounts.ErrInvalidName", errutil.WithPublicMessage("only external service account names can be prefixed with 'extsvc-'"))
|
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
|
// Credentials represents the credentials associated to an external service
|
||||||
|
@ -3,6 +3,7 @@ package extsvcaccounts
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
@ -92,6 +93,28 @@ func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, org
|
|||||||
}, nil
|
}, 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.
|
// 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) {
|
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.
|
// 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())
|
"error", err.Error())
|
||||||
return nil, err
|
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 {
|
func (esa *ExtSvcAccountsService) RemoveExternalService(ctx context.Context, name string) error {
|
||||||
|
@ -4,9 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models/roletype"
|
"github.com/grafana/grafana/pkg/models/roletype"
|
||||||
@ -20,6 +17,8 @@ import (
|
|||||||
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestEnv struct {
|
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