mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 07:35:45 -06:00
Plugin: Remove external service on plugin removal (#77712)
* Plugin: Remove external service on plugin removal * Add feature flag check in the service registration service * Initialize map * Add HasExternalService as suggested * 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> --------- Co-authored-by: linoman <2051016+linoman@users.noreply.github.com>
This commit is contained in:
parent
7169bfdb30
commit
20a2840046
@ -13,5 +13,7 @@ type ExternalService struct {
|
||||
}
|
||||
|
||||
type ExternalServiceRegistry interface {
|
||||
RegisterExternalService(ctx context.Context, name string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*ExternalService, error)
|
||||
HasExternalService(ctx context.Context, pluginID string) bool
|
||||
RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*ExternalService, error)
|
||||
RemoveExternalService(ctx context.Context, pluginID string) error
|
||||
}
|
||||
|
@ -437,10 +437,18 @@ type FakeAuthService struct {
|
||||
Result *auth.ExternalService
|
||||
}
|
||||
|
||||
func (f *FakeAuthService) RegisterExternalService(ctx context.Context, name string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*auth.ExternalService, error) {
|
||||
func (f *FakeAuthService) HasExternalService(ctx context.Context, pluginID string) bool {
|
||||
return f.Result != nil
|
||||
}
|
||||
|
||||
func (f *FakeAuthService) RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*auth.ExternalService, error) {
|
||||
return f.Result, nil
|
||||
}
|
||||
|
||||
func (f *FakeAuthService) RemoveExternalService(ctx context.Context, pluginID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type FakeDiscoverer struct {
|
||||
DiscoverFunc func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/auth"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
@ -24,16 +25,18 @@ type PluginInstaller struct {
|
||||
pluginRegistry registry.Service
|
||||
pluginLoader loader.Service
|
||||
log log.Logger
|
||||
serviceRegistry auth.ExternalServiceRegistry
|
||||
}
|
||||
|
||||
func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service,
|
||||
pluginRepo repo.Service) *PluginInstaller {
|
||||
pluginRepo repo.Service, serviceRegistry auth.ExternalServiceRegistry) *PluginInstaller {
|
||||
return New(pluginRegistry, pluginLoader, pluginRepo,
|
||||
storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath), storage.SimpleDirNameGeneratorFunc)
|
||||
storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath), storage.SimpleDirNameGeneratorFunc, serviceRegistry)
|
||||
}
|
||||
|
||||
func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service,
|
||||
pluginStorage storage.ZipExtractor, pluginStorageDirFunc storage.DirNameGeneratorFunc) *PluginInstaller {
|
||||
pluginStorage storage.ZipExtractor, pluginStorageDirFunc storage.DirNameGeneratorFunc,
|
||||
serviceRegistry auth.ExternalServiceRegistry) *PluginInstaller {
|
||||
return &PluginInstaller{
|
||||
pluginLoader: pluginLoader,
|
||||
pluginRegistry: pluginRegistry,
|
||||
@ -41,6 +44,7 @@ func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRep
|
||||
pluginStorage: pluginStorage,
|
||||
pluginStorageDirFunc: pluginStorageDirFunc,
|
||||
log: log.New("plugin.installer"),
|
||||
serviceRegistry: serviceRegistry,
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,6 +160,9 @@ func (m *PluginInstaller) Remove(ctx context.Context, pluginID string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if m.serviceRegistry.HasExternalService(ctx, pluginID) {
|
||||
return m.serviceRegistry.RemoveExternalService(ctx, pluginID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc)
|
||||
inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{})
|
||||
err := inst.Add(context.Background(), pluginID, v1, testCompatOpts())
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -171,7 +171,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, storage.SimpleDirNameGeneratorFunc)
|
||||
pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{})
|
||||
err := pm.Add(context.Background(), p.ID, "3.2.0", testCompatOpts())
|
||||
require.ErrorIs(t, err, plugins.ErrInstallCorePlugin)
|
||||
|
||||
|
@ -17,6 +17,12 @@ const (
|
||||
type AuthProvider string
|
||||
|
||||
type ExternalServiceRegistry interface {
|
||||
// HasExternalService returns whether an external service has been saved with that name.
|
||||
HasExternalService(ctx context.Context, name string) bool
|
||||
|
||||
// RemoveExternalService removes an external service and its associated resources from the database (ex: service account, token).
|
||||
RemoveExternalService(ctx context.Context, name string) error
|
||||
|
||||
// SaveExternalService creates or updates an external service in the database. Based on the requested auth provider,
|
||||
// it generates client_id, secrets and any additional provider specificities (ex: rsa keys). It also ensures that the
|
||||
// associated service account has the correct permissions.
|
||||
|
@ -33,6 +33,8 @@ type OAuth2Server interface {
|
||||
// GetExternalService retrieves an external service from store by client_id. It populates the SelfPermissions and
|
||||
// SignedInUser from the associated service account.
|
||||
GetExternalService(ctx context.Context, id string) (*OAuthExternalService, error)
|
||||
// RemoveExternalService removes an external service and its associated resources from the store.
|
||||
RemoveExternalService(ctx context.Context, name string) error
|
||||
|
||||
// HandleTokenRequest handles the client's OAuth2 query to obtain an access_token by presenting its authorization
|
||||
// grant (ex: client_credentials, jwtbearer).
|
||||
@ -45,6 +47,7 @@ type OAuth2Server interface {
|
||||
//go:generate mockery --name Store --structname MockStore --outpkg oastest --filename store_mock.go --output ./oastest/
|
||||
|
||||
type Store interface {
|
||||
DeleteExternalService(ctx context.Context, id string) error
|
||||
GetExternalService(ctx context.Context, id string) (*OAuthExternalService, error)
|
||||
GetExternalServiceByName(ctx context.Context, name string) (*OAuthExternalService, error)
|
||||
GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error)
|
||||
|
@ -183,6 +183,33 @@ func (s *OAuth2ServiceImpl) setClientUser(ctx context.Context, client *oauthserv
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OAuth2ServiceImpl) RemoveExternalService(ctx context.Context, name string) error {
|
||||
s.logger.Info("Remove external service", "service", name)
|
||||
|
||||
client, err := s.sqlstore.GetExternalServiceByName(ctx, name)
|
||||
if err != nil {
|
||||
if errors.Is(err, oauthserver.ErrClientNotFound) {
|
||||
s.logger.Debug("No external service linked to this name", "name", name)
|
||||
return nil
|
||||
}
|
||||
s.logger.Error("Error fetching external service", "name", name, "error", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Since we will delete the service, clear cache entry
|
||||
s.cache.Delete(client.ClientID)
|
||||
|
||||
// Delete the OAuth client info in store
|
||||
if err := s.sqlstore.DeleteExternalService(ctx, client.ClientID); err != nil {
|
||||
s.logger.Error("Error deleting external service", "name", name, "error", err.Error())
|
||||
return err
|
||||
}
|
||||
s.logger.Debug("Deleted external service", "name", name, "client_id", client.ClientID)
|
||||
|
||||
// Remove the associated service account
|
||||
return s.saService.RemoveExtSvcAccount(ctx, oauthserver.TmpOrgID, slugify.Slugify(name))
|
||||
}
|
||||
|
||||
// SaveExternalService creates or updates an external service in the database, it generates client_id and secrets and
|
||||
// it ensures that the associated service account has the correct permissions.
|
||||
// Database consistency is not guaranteed, consider changing this in the future.
|
||||
@ -412,14 +439,13 @@ func (s *OAuth2ServiceImpl) handlePluginStateChanged(ctx context.Context, event
|
||||
s.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
|
||||
|
||||
// Retrieve client associated to the plugin
|
||||
slug := slugify.Slugify(event.PluginId)
|
||||
client, err := s.sqlstore.GetExternalServiceByName(ctx, slug)
|
||||
client, err := s.sqlstore.GetExternalServiceByName(ctx, event.PluginId)
|
||||
if err != nil {
|
||||
if errors.Is(err, oauthserver.ErrClientNotFound) {
|
||||
s.logger.Debug("No external service linked to this plugin", "pluginId", event.PluginId)
|
||||
return nil
|
||||
}
|
||||
s.logger.Error("Error fetching service", "pluginId", event.PluginId, "error", err)
|
||||
s.logger.Error("Error fetching service", "pluginId", event.PluginId, "error", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -408,6 +408,51 @@ func assertArrayInMap[K comparable, V string](t *testing.T, m1 map[K][]V, m2 map
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuth2ServiceImpl_RemoveExternalService(t *testing.T) {
|
||||
const serviceName = "my-ext-service"
|
||||
const clientID = "RANDOMID"
|
||||
|
||||
dummyClient := &oauthserver.OAuthExternalService{
|
||||
Name: serviceName,
|
||||
ClientID: clientID,
|
||||
ServiceAccountID: 1,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
init func(*TestEnv)
|
||||
}{
|
||||
{
|
||||
name: "should do nothing on not found",
|
||||
init: func(env *TestEnv) {
|
||||
env.OAuthStore.On("GetExternalServiceByName", mock.Anything, serviceName).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should remove the external service and its associated service account",
|
||||
init: func(env *TestEnv) {
|
||||
env.OAuthStore.On("GetExternalServiceByName", mock.Anything, serviceName).Return(dummyClient, nil)
|
||||
env.OAuthStore.On("DeleteExternalService", mock.Anything, clientID).Return(nil)
|
||||
env.SAService.On("RemoveExtSvcAccount", mock.Anything, oauthserver.TmpOrgID, serviceName).Return(nil)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
if tt.init != nil {
|
||||
tt.init(env)
|
||||
}
|
||||
|
||||
err := env.S.RemoveExternalService(context.Background(), serviceName)
|
||||
require.NoError(t, err)
|
||||
|
||||
env.OAuthStore.AssertExpectations(t)
|
||||
env.SAService.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestOAuth2ServiceImpl_handleKeyOptions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
@ -25,6 +25,10 @@ func (s *FakeService) GetExternalService(ctx context.Context, id string) (*oauth
|
||||
return s.ExpectedClient, s.ExpectedErr
|
||||
}
|
||||
|
||||
func (s *FakeService) RemoveExternalService(ctx context.Context, name string) error {
|
||||
return s.ExpectedErr
|
||||
}
|
||||
|
||||
func (s *FakeService) HandleTokenRequest(rw http.ResponseWriter, req *http.Request) {}
|
||||
|
||||
func (s *FakeService) HandleIntrospectionRequest(rw http.ResponseWriter, req *http.Request) {}
|
||||
|
@ -16,6 +16,20 @@ type MockStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// DeleteExternalService provides a mock function with given fields: ctx, id
|
||||
func (_m *MockStore) DeleteExternalService(ctx context.Context, id string) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetExternalService provides a mock function with given fields: ctx, id
|
||||
func (_m *MockStore) GetExternalService(ctx context.Context, id string) (*oauthserver.OAuthExternalService, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
@ -126,6 +126,7 @@ func (s *store) GetExternalService(ctx context.Context, id string) (*oauthserver
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
res = nil
|
||||
return oauthserver.ErrClientNotFoundFn(id)
|
||||
}
|
||||
|
||||
@ -223,3 +224,18 @@ func (s *store) UpdateExternalServiceGrantTypes(ctx context.Context, clientID, g
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *store) DeleteExternalService(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return oauthserver.ErrClientRequiredID
|
||||
}
|
||||
|
||||
return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
if _, err := sess.Exec(`DELETE FROM oauth_client WHERE client_id = ?`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := sess.Exec(`DELETE FROM oauth_impersonate_permission WHERE client_id = ?`, id)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
@ -352,6 +353,88 @@ vuO8AU0bVoUmYMKhozkcCYHudkeS08hEjQIDAQAB
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_RemoveExternalService(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))
|
||||
|
||||
// Check presence of clients in store
|
||||
getState := func(t *testing.T) map[string]bool {
|
||||
client, err := s.GetExternalService(ctx, "ClientID")
|
||||
if err != nil && !errors.Is(err, oauthserver.ErrClientNotFound) {
|
||||
require.Fail(t, "error fetching client")
|
||||
}
|
||||
|
||||
client2, err := s.GetExternalService(ctx, "ClientID2")
|
||||
if err != nil && !errors.Is(err, oauthserver.ErrClientNotFound) {
|
||||
require.Fail(t, "error fetching client")
|
||||
}
|
||||
|
||||
return map[string]bool{
|
||||
"ClientID": client != nil,
|
||||
"ClientID2": client2 != nil,
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
state map[string]bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no id provided",
|
||||
state: map[string]bool{"ClientID": true, "ClientID2": true},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
id: "ClientID3",
|
||||
state: map[string]bool{"ClientID": true, "ClientID2": true},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "remove client 2",
|
||||
id: "ClientID2",
|
||||
state: map[string]bool{"ClientID": true, "ClientID2": false},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "remove client 1",
|
||||
id: "ClientID",
|
||||
state: map[string]bool{"ClientID": false, "ClientID2": false},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := s.DeleteExternalService(ctx, tt.id)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.EqualValues(t, tt.state, getState(t))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func compareClientToStored(t *testing.T, s *store, wanted *oauthserver.OAuthExternalService) {
|
||||
ctx := context.Background()
|
||||
stored, err := s.GetExternalService(ctx, wanted.ClientID)
|
||||
|
@ -2,8 +2,10 @@ package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
||||
"github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@ -17,14 +19,53 @@ type Registry struct {
|
||||
logger log.Logger
|
||||
oauthServer oauthserver.OAuth2Server
|
||||
saSvc *extsvcaccounts.ExtSvcAccountsService
|
||||
|
||||
extSvcProviders map[string]extsvcauth.AuthProvider
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func ProvideExtSvcRegistry(oauthServer oauthserver.OAuth2Server, saSvc *extsvcaccounts.ExtSvcAccountsService, features featuremgmt.FeatureToggles) *Registry {
|
||||
return &Registry{
|
||||
features: features,
|
||||
logger: log.New("extsvcauth.registry"),
|
||||
oauthServer: oauthServer,
|
||||
saSvc: saSvc,
|
||||
extSvcProviders: map[string]extsvcauth.AuthProvider{},
|
||||
features: features,
|
||||
lock: sync.Mutex{},
|
||||
logger: log.New("extsvcauth.registry"),
|
||||
oauthServer: oauthServer,
|
||||
saSvc: saSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// HasExternalService returns whether an external service has been saved with that name.
|
||||
func (r *Registry) HasExternalService(ctx context.Context, name string) bool {
|
||||
_, ok := r.extSvcProviders[slugify.Slugify(name)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// 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)]
|
||||
if !ok {
|
||||
r.logger.Debug("external service not found", "service", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case extsvcauth.ServiceAccounts:
|
||||
if !r.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) {
|
||||
r.logger.Debug("Skipping External Service removal, flag disabled", "service", name, "flag", featuremgmt.FlagExternalServiceAccounts)
|
||||
return nil
|
||||
}
|
||||
r.logger.Debug("Routing External Service removal to the External Service Account service", "service", name)
|
||||
return r.saSvc.RemoveExternalService(ctx, name)
|
||||
case extsvcauth.OAuth2Server:
|
||||
if !r.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
|
||||
r.logger.Debug("Skipping External Service removal, flag disabled", "service", name, "flag", featuremgmt.FlagExternalServiceAccounts)
|
||||
return nil
|
||||
}
|
||||
r.logger.Debug("Routing External Service removal to the OAuth2Server", "service", name)
|
||||
return r.oauthServer.RemoveExternalService(ctx, name)
|
||||
default:
|
||||
return extsvcauth.ErrUnknownProvider.Errorf("unknow provider '%v'", provider)
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,17 +73,22 @@ func ProvideExtSvcRegistry(oauthServer oauthserver.OAuth2Server, saSvc *extsvcac
|
||||
// it generates client_id, secrets and any additional provider specificities (ex: rsa keys). It also ensures that the
|
||||
// associated service account has the correct permissions.
|
||||
func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) {
|
||||
// Record provider in case of removal
|
||||
r.lock.Lock()
|
||||
r.extSvcProviders[slugify.Slugify(cmd.Name)] = cmd.AuthProvider
|
||||
r.lock.Unlock()
|
||||
|
||||
switch cmd.AuthProvider {
|
||||
case extsvcauth.ServiceAccounts:
|
||||
if !r.features.IsEnabled(featuremgmt.FlagExternalServiceAccounts) {
|
||||
r.logger.Warn("Skipping external service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAccounts)
|
||||
r.logger.Warn("Skipping External Service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAccounts)
|
||||
return nil, nil
|
||||
}
|
||||
r.logger.Debug("Routing the External Service registration to the External Service Account service", "service", cmd.Name)
|
||||
return r.saSvc.SaveExternalService(ctx, cmd)
|
||||
case extsvcauth.OAuth2Server:
|
||||
if !r.features.IsEnabled(featuremgmt.FlagExternalServiceAuth) {
|
||||
r.logger.Warn("Skipping external service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAuth)
|
||||
r.logger.Warn("Skipping External Service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAuth)
|
||||
return nil, nil
|
||||
}
|
||||
r.logger.Debug("Routing the External Service registration to the OAuth2Server", "service", cmd.Name)
|
||||
|
@ -39,8 +39,7 @@ func newExternalServiceRegistration(cfg *config.Cfg, serviceRegistry auth.Extern
|
||||
|
||||
// Register registers the external service with the external service registry, if the feature is enabled.
|
||||
func (r *ExternalServiceRegistration) Register(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
|
||||
if p.ExternalServiceRegistration != nil &&
|
||||
(r.cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAuth) || r.cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAccounts)) {
|
||||
if p.ExternalServiceRegistration != nil {
|
||||
s, err := r.externalServiceRegistry.RegisterExternalService(ctx, p.ID, plugindef.Type(p.Type), p.ExternalServiceRegistration)
|
||||
if err != nil {
|
||||
r.log.Error("Could not register an external service. Initialization skipped", "pluginId", p.ID, "error", err)
|
||||
|
@ -5,32 +5,53 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/auth"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"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/pluginsintegration/pluginsettings"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
reg extsvcauth.ExternalServiceRegistry
|
||||
settingsSvc pluginsettings.Service
|
||||
featureEnabled bool
|
||||
log log.Logger
|
||||
reg extsvcauth.ExternalServiceRegistry
|
||||
settingsSvc pluginsettings.Service
|
||||
}
|
||||
|
||||
func ProvideService(reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service {
|
||||
func ProvideService(cfg *config.Cfg, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service {
|
||||
s := &Service{
|
||||
reg: reg,
|
||||
settingsSvc: settingsSvc,
|
||||
featureEnabled: cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAuth) || cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAccounts),
|
||||
log: log.New("plugins.external.registration"),
|
||||
reg: reg,
|
||||
settingsSvc: settingsSvc,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Service) HasExternalService(ctx context.Context, pluginID string) bool {
|
||||
if !s.featureEnabled {
|
||||
s.log.Debug("Skipping HasExternalService call. The feature is behind a feature toggle and needs to be enabled.")
|
||||
return false
|
||||
}
|
||||
|
||||
return s.reg.HasExternalService(ctx, pluginID)
|
||||
}
|
||||
|
||||
// RegisterExternalService is a simplified wrapper around SaveExternalService for the plugin use case.
|
||||
func (s *Service) RegisterExternalService(ctx context.Context, svcName string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*auth.ExternalService, error) {
|
||||
func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.ExternalServiceRegistration) (*auth.ExternalService, error) {
|
||||
if !s.featureEnabled {
|
||||
s.log.Warn("Skipping External Service Registration. The feature is behind a feature toggle and needs to be enabled.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Datasource plugins can only be enabled
|
||||
enabled := true
|
||||
// App plugins can be disabled
|
||||
if pType == plugindef.TypeApp {
|
||||
settings, err := s.settingsSvc.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{PluginID: svcName})
|
||||
settings, err := s.settingsSvc.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{PluginID: pluginID})
|
||||
if err != nil && !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
@ -56,7 +77,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, svcName string, p
|
||||
}
|
||||
|
||||
registration := &extsvcauth.ExternalServiceRegistration{
|
||||
Name: svcName,
|
||||
Name: pluginID,
|
||||
Impersonation: impersonation,
|
||||
Self: self,
|
||||
}
|
||||
@ -98,3 +119,13 @@ func toAccessControlPermissions(ps []plugindef.Permission) []accesscontrol.Permi
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// RemoveExternalService removes the external service account associated to a plugin
|
||||
func (s *Service) RemoveExternalService(ctx context.Context, pluginID string) error {
|
||||
if !s.featureEnabled {
|
||||
s.log.Debug("Skipping External Service Removal. The feature is behind a feature toggle and needs to be enabled.")
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.reg.RemoveExternalService(ctx, pluginID)
|
||||
}
|
||||
|
@ -126,6 +126,37 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *
|
||||
return &extsvcauth.ExternalService{Name: cmd.Name, ID: slug, Secret: token}, nil
|
||||
}
|
||||
|
||||
func (esa *ExtSvcAccountsService) RemoveExternalService(ctx context.Context, name string) 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
|
||||
}
|
||||
return esa.RemoveExtSvcAccount(ctx, extsvcauth.TmpOrgID, slugify.Slugify(name))
|
||||
}
|
||||
|
||||
func (esa *ExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, orgID int64, extSvcSlug string) error {
|
||||
saID, errRetrieve := esa.saSvc.RetrieveServiceAccountIdByName(ctx, orgID, sa.ExtSvcPrefix+extSvcSlug)
|
||||
if errRetrieve != nil && !errors.Is(errRetrieve, sa.ErrServiceAccountNotFound) {
|
||||
return errRetrieve
|
||||
}
|
||||
|
||||
if saID <= 0 {
|
||||
esa.logger.Debug("No external service account associated with this service", "service", extSvcSlug, "orgID", orgID)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := esa.deleteExtSvcAccount(ctx, orgID, extSvcSlug, saID); err != nil {
|
||||
esa.logger.Error("Error occurred while deleting service account",
|
||||
"service", extSvcSlug,
|
||||
"saID", saID,
|
||||
"error", err.Error())
|
||||
return err
|
||||
}
|
||||
esa.logger.Info("Deleted external service account", "service", extSvcSlug, "orgID", orgID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -153,7 +184,6 @@ 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, no permission",
|
||||
"service", cmd.ExtSvcSlug,
|
||||
@ -173,8 +203,6 @@ 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
|
||||
}
|
||||
|
||||
@ -212,6 +240,8 @@ func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *sa
|
||||
return 0, err
|
||||
}
|
||||
|
||||
esa.metrics.savedCount.Inc()
|
||||
|
||||
return cmd.SaID, nil
|
||||
}
|
||||
|
||||
@ -224,7 +254,11 @@ func (esa *ExtSvcAccountsService) deleteExtSvcAccount(ctx context.Context, orgID
|
||||
if err := esa.acSvc.DeleteExternalServiceRole(ctx, slug); err != nil {
|
||||
return err
|
||||
}
|
||||
return esa.DeleteExtSvcCredentials(ctx, orgID, slug)
|
||||
if err := esa.DeleteExtSvcCredentials(ctx, orgID, slug); err != nil {
|
||||
return err
|
||||
}
|
||||
esa.metrics.deletedCount.Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
// getExtSvcAccountToken get or create the token of an External Service
|
||||
|
@ -380,3 +380,64 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtSvcAccountsService_RemoveExtSvcAccount(t *testing.T) {
|
||||
extSvcSlug := "grafana-test-app"
|
||||
tmpOrgID := int64(1)
|
||||
extSvcAccID := int64(10)
|
||||
tests := []struct {
|
||||
name string
|
||||
init func(env *TestEnv)
|
||||
slug string
|
||||
checks func(t *testing.T, env *TestEnv)
|
||||
want *extsvcauth.ExternalService
|
||||
}{
|
||||
{
|
||||
name: "should not fail if the service account does not exist",
|
||||
init: func(env *TestEnv) {
|
||||
// No previous service account was attached to this slug
|
||||
env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, tmpOrgID, sa.ExtSvcPrefix+extSvcSlug).
|
||||
Return(int64(0), sa.ErrServiceAccountNotFound.Errorf("not found"))
|
||||
},
|
||||
slug: extSvcSlug,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "should remove service account",
|
||||
init: func(env *TestEnv) {
|
||||
// A previous service account was attached to this slug
|
||||
env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, tmpOrgID, sa.ExtSvcPrefix+extSvcSlug).
|
||||
Return(extSvcAccID, nil)
|
||||
env.SaSvc.On("DeleteServiceAccount", mock.Anything, tmpOrgID, extSvcAccID).Return(nil)
|
||||
env.AcStore.On("DeleteExternalServiceRole", mock.Anything, extSvcSlug).Return(nil)
|
||||
// A token was previously stored in the secret store
|
||||
_ = env.SkvStore.Set(context.Background(), tmpOrgID, extSvcSlug, kvStoreType, "ExtSvcSecretToken")
|
||||
},
|
||||
slug: extSvcSlug,
|
||||
checks: func(t *testing.T, env *TestEnv) {
|
||||
_, ok, _ := env.SkvStore.Get(context.Background(), tmpOrgID, extSvcSlug, kvStoreType)
|
||||
require.False(t, ok, "secret should have been removed from store")
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
err := env.S.RemoveExtSvcAccount(ctx, tmpOrgID, tt.slug)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.checks != nil {
|
||||
tt.checks(t, env)
|
||||
}
|
||||
env.SaSvc.AssertExpectations(t)
|
||||
env.AcStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ type ExtSvcAccountsService interface {
|
||||
EnableExtSvcAccount(ctx context.Context, cmd *EnableExtSvcAccountCmd) error
|
||||
// ManageExtSvcAccount creates, updates or deletes the service account associated with an external service
|
||||
ManageExtSvcAccount(ctx context.Context, cmd *ManageExtSvcAccountCmd) (int64, error)
|
||||
// RemoveExtSvcAccount removes the external service account associated with an external service
|
||||
RemoveExtSvcAccount(ctx context.Context, orgID int64, extSvcSlug string) error
|
||||
// RetrieveExtSvcAccount fetches an external service account by ID
|
||||
RetrieveExtSvcAccount(ctx context.Context, orgID, saID int64) (*ExtSvcAccount, error)
|
||||
}
|
||||
|
@ -52,6 +52,20 @@ func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cm
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RemoveExtSvcAccount provides a mock function with given fields: ctx, orgID, extSvcSlug
|
||||
func (_m *MockExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, orgID int64, extSvcSlug string) error {
|
||||
ret := _m.Called(ctx, orgID, extSvcSlug)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok {
|
||||
r0 = rf(ctx, orgID, extSvcSlug)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RetrieveExtSvcAccount provides a mock function with given fields: ctx, orgID, saID
|
||||
func (_m *MockExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, orgID int64, saID int64) (*serviceaccounts.ExtSvcAccount, error) {
|
||||
ret := _m.Called(ctx, orgID, saID)
|
||||
|
Loading…
Reference in New Issue
Block a user