mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins Admin: Avoid disabling auto-enabled apps (#97800)
This commit is contained in:
parent
7601bcbb5d
commit
95dea152b6
@ -86,6 +86,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
|
|||||||
secureJsonData?: KeyValue;
|
secureJsonData?: KeyValue;
|
||||||
secureJsonFields?: KeyValue<boolean>;
|
secureJsonFields?: KeyValue<boolean>;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
autoEnabled?: boolean;
|
||||||
defaultNavUrl?: string;
|
defaultNavUrl?: string;
|
||||||
hasUpdate?: boolean;
|
hasUpdate?: boolean;
|
||||||
enterprise?: boolean;
|
enterprise?: boolean;
|
||||||
|
@ -12,6 +12,7 @@ type PluginSetting struct {
|
|||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Pinned bool `json:"pinned"`
|
Pinned bool `json:"pinned"`
|
||||||
|
AutoEnabled bool `json:"autoEnabled"`
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
BaseUrl string `json:"baseUrl"`
|
BaseUrl string `json:"baseUrl"`
|
||||||
Info plugins.Info `json:"info"`
|
Info plugins.Info `json:"info"`
|
||||||
|
@ -721,6 +721,7 @@ func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[stri
|
|||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
Enabled: plugin.AutoEnabled,
|
Enabled: plugin.AutoEnabled,
|
||||||
Pinned: plugin.AutoEnabled,
|
Pinned: plugin.AutoEnabled,
|
||||||
|
AutoEnabled: plugin.AutoEnabled,
|
||||||
PluginVersion: plugin.Info.Version,
|
PluginVersion: plugin.Info.Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,6 +216,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
|
|||||||
if plugin.IsApp() {
|
if plugin.IsApp() {
|
||||||
dto.Enabled = plugin.AutoEnabled
|
dto.Enabled = plugin.AutoEnabled
|
||||||
dto.Pinned = plugin.AutoEnabled
|
dto.Pinned = plugin.AutoEnabled
|
||||||
|
dto.AutoEnabled = plugin.AutoEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
ps, err := hs.PluginSettings.GetPluginSettingByPluginID(c.Req.Context(), &pluginsettings.GetByPluginIDArgs{
|
ps, err := hs.PluginSettings.GetPluginSettingByPluginID(c.Req.Context(), &pluginsettings.GetByPluginIDArgs{
|
||||||
@ -254,9 +255,13 @@ func (hs *HTTPServer) UpdatePluginSetting(c *contextmodel.ReqContext) response.R
|
|||||||
}
|
}
|
||||||
pluginID := web.Params(c.Req)[":pluginId"]
|
pluginID := web.Params(c.Req)[":pluginId"]
|
||||||
|
|
||||||
if _, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID); !exists {
|
p, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
|
||||||
|
if !exists {
|
||||||
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
|
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
|
||||||
}
|
}
|
||||||
|
if p.AutoEnabled && !cmd.Enabled {
|
||||||
|
return response.Error(http.StatusBadRequest, "Cannot disable auto-enabled plugin", nil)
|
||||||
|
}
|
||||||
|
|
||||||
cmd.OrgId = c.SignedInUser.GetOrgID()
|
cmd.OrgId = c.SignedInUser.GetOrgID()
|
||||||
cmd.PluginId = pluginID
|
cmd.PluginId = pluginID
|
||||||
|
@ -871,3 +871,43 @@ func Test_PluginsSettings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_UpdatePluginSetting(t *testing.T) {
|
||||||
|
pID := "test-app"
|
||||||
|
p1 := createPlugin(plugins.JSONData{
|
||||||
|
ID: pID, Type: "app", Name: pID,
|
||||||
|
Info: plugins.Info{
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
AutoEnabled: true,
|
||||||
|
}, plugins.ClassExternal, plugins.NewFakeFS(),
|
||||||
|
)
|
||||||
|
pluginRegistry := &fakes.FakePluginRegistry{
|
||||||
|
Store: map[string]*plugins.Plugin{
|
||||||
|
p1.ID: p1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
|
||||||
|
pID: {ID: 0, OrgID: 1, PluginID: pID, PluginVersion: "1.0.0", Enabled: true},
|
||||||
|
}}
|
||||||
|
|
||||||
|
t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) {
|
||||||
|
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||||
|
hs.Cfg = setting.NewCfg()
|
||||||
|
hs.PluginSettings = &pluginSettings
|
||||||
|
hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{})
|
||||||
|
hs.pluginFileStore = filestore.ProvideService(pluginRegistry)
|
||||||
|
hs.managedPluginsService = managedplugins.NewNoop()
|
||||||
|
hs.log = log.NewNopLogger()
|
||||||
|
})
|
||||||
|
|
||||||
|
input := strings.NewReader(`{"enabled": false}`)
|
||||||
|
endpoint := fmt.Sprintf("/api/plugins/%s/settings", pID)
|
||||||
|
req := webtest.RequestWithSignedInUser(server.NewPostRequest(endpoint, input), userWithPermissions(1, []ac.Permission{{Action: pluginaccesscontrol.ActionWrite, Scope: "plugins:id:test-app"}}))
|
||||||
|
res, err := server.SendJSON(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||||
|
require.NoError(t, res.Body.Close())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ type InfoDTO struct {
|
|||||||
Enabled bool
|
Enabled bool
|
||||||
Pinned bool
|
Pinned bool
|
||||||
PluginVersion string
|
PluginVersion string
|
||||||
|
AutoEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateArgs struct {
|
type UpdateArgs struct {
|
||||||
|
@ -19,6 +19,7 @@ func Provision(ctx context.Context, configDirectory string, pluginStore pluginst
|
|||||||
cfgProvider: newConfigReader(logger, pluginStore),
|
cfgProvider: newConfigReader(logger, pluginStore),
|
||||||
pluginSettings: pluginSettings,
|
pluginSettings: pluginSettings,
|
||||||
orgService: orgService,
|
orgService: orgService,
|
||||||
|
pluginStore: pluginStore,
|
||||||
}
|
}
|
||||||
return ap.applyChanges(ctx, configDirectory)
|
return ap.applyChanges(ctx, configDirectory)
|
||||||
}
|
}
|
||||||
@ -30,6 +31,7 @@ type PluginProvisioner struct {
|
|||||||
cfgProvider configReader
|
cfgProvider configReader
|
||||||
pluginSettings pluginsettings.Service
|
pluginSettings pluginsettings.Service
|
||||||
orgService org.Service
|
orgService org.Service
|
||||||
|
pluginStore pluginstore.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) error {
|
func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) error {
|
||||||
@ -45,6 +47,14 @@ func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) er
|
|||||||
app.OrgID = 1
|
app.OrgID = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p, found := ap.pluginStore.Plugin(ctx, app.PluginID)
|
||||||
|
if !found {
|
||||||
|
return errors.New("plugin not found")
|
||||||
|
}
|
||||||
|
if p.AutoEnabled && !app.Enabled {
|
||||||
|
return errors.New("plugin is auto enabled and cannot be disabled")
|
||||||
|
}
|
||||||
|
|
||||||
ps, err := ap.pluginSettings.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{
|
ps, err := ap.pluginSettings.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{
|
||||||
OrgID: app.OrgID,
|
OrgID: app.OrgID,
|
||||||
PluginID: app.PluginID,
|
PluginID: app.PluginID,
|
||||||
|
@ -8,9 +8,11 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPluginProvisioner(t *testing.T) {
|
func TestPluginProvisioner(t *testing.T) {
|
||||||
@ -37,7 +39,16 @@ func TestPluginProvisioner(t *testing.T) {
|
|||||||
store := &mockStore{}
|
store := &mockStore{}
|
||||||
orgMock := orgtest.NewOrgServiceFake()
|
orgMock := orgtest.NewOrgServiceFake()
|
||||||
orgMock.ExpectedOrg = &org.Org{ID: 4}
|
orgMock.ExpectedOrg = &org.Org{ID: 4}
|
||||||
ap := PluginProvisioner{log: log.New("test"), cfgProvider: reader, pluginSettings: store, orgService: orgMock}
|
ap := PluginProvisioner{
|
||||||
|
log: log.New("test"),
|
||||||
|
cfgProvider: reader,
|
||||||
|
pluginSettings: store,
|
||||||
|
orgService: orgMock,
|
||||||
|
pluginStore: pluginstore.NewFakePluginStore(
|
||||||
|
pluginstore.Plugin{JSONData: plugins.JSONData{ID: "test-plugin"}},
|
||||||
|
pluginstore.Plugin{JSONData: plugins.JSONData{ID: "test-plugin-2"}},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
err := ap.applyChanges(context.Background(), "")
|
err := ap.applyChanges(context.Background(), "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -68,6 +79,30 @@ func TestPluginProvisioner(t *testing.T) {
|
|||||||
require.Equal(t, tc.ExpectedSecureJSONData, cmd.SecureJSONData)
|
require.Equal(t, tc.ExpectedSecureJSONData, cmd.SecureJSONData)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Should return error trying to disable an auto-enabled plugin", func(t *testing.T) {
|
||||||
|
cfg := []*pluginsAsConfig{
|
||||||
|
{
|
||||||
|
Apps: []*appFromConfig{
|
||||||
|
{PluginID: "test-plugin", OrgID: 2, Enabled: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reader := &testConfigReader{result: cfg}
|
||||||
|
store := &mockStore{}
|
||||||
|
ap := PluginProvisioner{
|
||||||
|
log: log.New("test"),
|
||||||
|
cfgProvider: reader,
|
||||||
|
pluginSettings: store,
|
||||||
|
pluginStore: pluginstore.NewFakePluginStore(
|
||||||
|
pluginstore.Plugin{JSONData: plugins.JSONData{ID: "test-plugin", AutoEnabled: true}},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ap.applyChanges(context.Background(), "")
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "plugin is auto enabled and cannot be disabled")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type testConfigReader struct {
|
type testConfigReader struct {
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { PluginSignatureStatus } from '@grafana/data';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|
||||||
|
import { usePluginConfig } from '../../hooks/usePluginConfig';
|
||||||
|
import { CatalogPlugin } from '../../types';
|
||||||
|
|
||||||
|
import { GetStartedWithApp } from './GetStartedWithApp';
|
||||||
|
|
||||||
|
jest.mock('app/core/core', () => ({
|
||||||
|
contextSrv: {
|
||||||
|
hasPermission: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../api', () => ({
|
||||||
|
updatePluginSettings: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../hooks/usePluginConfig', () => ({
|
||||||
|
usePluginConfig: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockPlugin: CatalogPlugin = {
|
||||||
|
id: 'test-plugin',
|
||||||
|
name: 'Test Plugin',
|
||||||
|
description: 'Test Plugin Description',
|
||||||
|
downloads: 0,
|
||||||
|
hasUpdate: false,
|
||||||
|
info: {
|
||||||
|
logos: {
|
||||||
|
large: 'https://grafana.com/assets/img/brand/grafana_icon.svg',
|
||||||
|
small: 'https://grafana.com/assets/img/brand/grafana_icon.svg',
|
||||||
|
},
|
||||||
|
keywords: [],
|
||||||
|
},
|
||||||
|
isDev: false,
|
||||||
|
isCore: false,
|
||||||
|
isEnterprise: false,
|
||||||
|
isInstalled: false,
|
||||||
|
isDisabled: false,
|
||||||
|
isDeprecated: false,
|
||||||
|
isManaged: false,
|
||||||
|
isPreinstalled: { found: false, withVersion: false },
|
||||||
|
isPublished: false,
|
||||||
|
orgName: 'Test Org',
|
||||||
|
signature: PluginSignatureStatus.valid,
|
||||||
|
popularity: 0,
|
||||||
|
publishedAt: '2021-01-01',
|
||||||
|
updatedAt: '2021-01-01',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('GetStartedWithApp', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders null if pluginConfig is not available', () => {
|
||||||
|
(usePluginConfig as jest.Mock).mockReturnValue({ value: null });
|
||||||
|
(contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
|
||||||
|
|
||||||
|
const { container } = render(<GetStartedWithApp plugin={mockPlugin} />);
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders null if user does not have permission', () => {
|
||||||
|
(usePluginConfig as jest.Mock).mockReturnValue({ value: { meta: {} } });
|
||||||
|
(contextSrv.hasPermission as jest.Mock).mockReturnValue(false);
|
||||||
|
|
||||||
|
const { container } = render(<GetStartedWithApp plugin={mockPlugin} />);
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders enable button if plugin is not enabled', () => {
|
||||||
|
(usePluginConfig as jest.Mock).mockReturnValue({ value: { meta: { enabled: false } } });
|
||||||
|
(contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
|
||||||
|
|
||||||
|
render(<GetStartedWithApp plugin={mockPlugin} />);
|
||||||
|
expect(screen.getByText('Enable')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders disable button if plugin is enabled and not autoEnabled', () => {
|
||||||
|
(usePluginConfig as jest.Mock).mockReturnValue({ value: { meta: { enabled: true, autoEnabled: false } } });
|
||||||
|
(contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
|
||||||
|
|
||||||
|
render(<GetStartedWithApp plugin={mockPlugin} />);
|
||||||
|
expect(screen.getByText('Disable')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render disable button if plugin is enabled and autoEnabled', () => {
|
||||||
|
(usePluginConfig as jest.Mock).mockReturnValue({ value: { meta: { enabled: true, autoEnabled: true } } });
|
||||||
|
(contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
|
||||||
|
|
||||||
|
render(<GetStartedWithApp plugin={mockPlugin} />);
|
||||||
|
expect(screen.queryByText('Disable')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -25,7 +25,7 @@ export function GetStartedWithApp({ plugin }: Props): React.ReactElement | null
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { enabled, jsonData } = pluginConfig?.meta;
|
const { enabled, autoEnabled, jsonData } = pluginConfig?.meta;
|
||||||
|
|
||||||
const enable = () => {
|
const enable = () => {
|
||||||
reportInteraction('plugins_detail_enable_clicked', {
|
reportInteraction('plugins_detail_enable_clicked', {
|
||||||
@ -63,7 +63,7 @@ export function GetStartedWithApp({ plugin }: Props): React.ReactElement | null
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{enabled && (
|
{enabled && !autoEnabled && (
|
||||||
<Button variant="destructive" onClick={disable}>
|
<Button variant="destructive" onClick={disable}>
|
||||||
Disable
|
Disable
|
||||||
</Button>
|
</Button>
|
||||||
|
Loading…
Reference in New Issue
Block a user