diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index 0479721f708..ef9411bea9e 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -37,6 +37,8 @@ export async function getPluginDetails(id: string): Promise ({ getPluginSettings: jest.fn(), })); -const getPluginSettingsMock = getPluginSettings as jest.MockedFunction; +jest.mock('../admin/api', () => ({ + getPluginDetails: jest.fn(), +})); -const fakePlugin: PluginMeta = { +const getPluginSettingsMock = jest.mocked(getPluginSettings); +const getPluginDetailsMock = jest.mocked(getPluginDetails); + +const fakePluginSettings: PluginMeta = { id: 'test-plugin', name: 'Test Plugin', } as PluginMeta; +const fakePluginDetails: CatalogPluginDetails = {} as CatalogPluginDetails; + describe('Sandbox eligibility checks', () => { const originalNodeEnv = process.env.NODE_ENV; beforeEach(() => { jest.clearAllMocks(); + getPluginDetailsMock.mockReset(); + getPluginSettingsMock.mockReset(); + + // restore default check + setSandboxEnabledCheck(isPluginFrontendSandboxEnabled); + config.enableFrontendSandboxForPlugins = []; config.featureToggles.pluginsFrontendSandbox = true; process.env.NODE_ENV = 'development'; @@ -54,26 +69,8 @@ describe('Sandbox eligibility checks', () => { expect(result).toBe(false); }); - test('shouldLoadPluginInFrontendSandbox returns false for Grafana-signed plugins', async () => { - getPluginSettingsMock.mockResolvedValue({ ...fakePlugin, signatureType: PluginSignatureType.grafana }); - const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); - expect(result).toBe(false); - }); - - test('shouldLoadPluginInFrontendSandbox returns true for eligible plugins in the list', async () => { - getPluginSettingsMock.mockResolvedValue({ ...fakePlugin, signatureType: PluginSignatureType.community }); - config.enableFrontendSandboxForPlugins = ['test-plugin']; - const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); - expect(result).toBe(true); - }); - - test('isPluginFrontendSandboxEnabled returns false when plugin is not in the enabled list', async () => { - config.enableFrontendSandboxForPlugins = ['other-plugin']; - const result = await isPluginFrontendSandboxEnabled({ pluginId: 'test-plugin' }); - expect(result).toBe(false); - }); - test('setSandboxEnabledCheck sets custom check function', async () => { + getPluginDetailsMock.mockResolvedValue(fakePluginDetails); const customCheck = jest.fn().mockResolvedValue(true); setSandboxEnabledCheck(customCheck); const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); @@ -82,6 +79,7 @@ describe('Sandbox eligibility checks', () => { }); test('setSandboxEnabledCheck has precedence over default', async () => { + getPluginDetailsMock.mockResolvedValue(fakePluginDetails); const customCheck = jest.fn().mockResolvedValue(false); setSandboxEnabledCheck(customCheck); // this should be ignored by the custom check @@ -91,10 +89,123 @@ describe('Sandbox eligibility checks', () => { expect(result).toBe(false); }); - test('isPluginFrontendSandboxEligible returns false for plugins with internal signature', async () => { - //@ts-expect-error We don't publicly export the internal signature - getPluginSettingsMock.mockResolvedValue({ ...fakePlugin, signature: 'internal' }); - const result = await isPluginFrontendSandboxEligible({ pluginId: 'test-plugin' }); - expect(result).toBe(false); + describe('with getPluginDetails', () => { + test('shouldLoadPluginInFrontendSandbox returns false for Grafana-signed plugins', async () => { + getPluginSettingsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginDetailsMock.mockResolvedValue({ ...fakePluginDetails, signatureType: PluginSignatureType.grafana }); + + const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); + expect(result).toBe(false); + }); + + test('shouldLoadPluginInFrontendSandbox returns true for community plugins', async () => { + getPluginSettingsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginDetailsMock.mockResolvedValue({ ...fakePluginDetails, signatureType: PluginSignatureType.community }); + + config.enableFrontendSandboxForPlugins = ['test-plugin']; + const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); + expect(result).toBe(true); + }); + + test('isPluginFrontendSandboxEnabled returns false when plugin is not in the enabled list', async () => { + getPluginSettingsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginDetailsMock.mockResolvedValue({ ...fakePluginDetails, signatureType: PluginSignatureType.community }); + config.enableFrontendSandboxForPlugins = ['other-plugin']; + const result = await isPluginFrontendSandboxEnabled({ pluginId: 'test-plugin' }); + expect(result).toBe(false); + }); + + test('shouldLoadPluginInFrontendSandbox returns true for commercial plugins in the enabled list', async () => { + getPluginSettingsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginDetailsMock.mockResolvedValue({ ...fakePluginDetails, signatureType: PluginSignatureType.commercial }); + config.enableFrontendSandboxForPlugins = ['test-plugin']; + const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); + expect(result).toBe(true); + }); + + test('shouldLoadPluginInFrontendSandbox returns true for private plugins in the enabled list', async () => { + getPluginSettingsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginDetailsMock.mockResolvedValue({ ...fakePluginDetails, signatureType: PluginSignatureType.private }); + config.enableFrontendSandboxForPlugins = ['test-plugin']; + const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); + expect(result).toBe(true); + }); + + test('isPluginFrontendSandboxEligible returns false for plugins with internal signature', async () => { + getPluginSettingsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginDetailsMock.mockResolvedValue({ + ...fakePluginDetails, + signatureType: PluginSignatureType.community, + signature: PluginSignatureStatus.internal, + }); + + const result = await isPluginFrontendSandboxEligible({ pluginId: 'test-plugin' }); + expect(result).toBe(false); + }); + }); + + describe('with getPluginSettings', () => { + test('shouldLoadPluginInFrontendSandbox returns false for Grafana-signed plugins', async () => { + // if getPluginDetails fails it fallsback to getPluginSettings + getPluginDetailsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginSettingsMock.mockResolvedValue({ ...fakePluginSettings, signatureType: PluginSignatureType.grafana }); + const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); + expect(result).toBe(false); + }); + + test('shouldLoadPluginInFrontendSandbox returns true for eligible plugins in the list', async () => { + // if getPluginDetails fails it fallsback to getPluginSettings + getPluginDetailsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginSettingsMock.mockResolvedValue({ ...fakePluginSettings, signatureType: PluginSignatureType.community }); + config.enableFrontendSandboxForPlugins = ['test-plugin']; + const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); + expect(result).toBe(true); + }); + + test('isPluginFrontendSandboxEnabled returns false when plugin is not in the enabled list', async () => { + // if getPluginDetails fails it fallsback to getPluginSettings + getPluginDetailsMock.mockRejectedValueOnce(new Error('not found')); + + config.enableFrontendSandboxForPlugins = ['other-plugin']; + const result = await isPluginFrontendSandboxEnabled({ pluginId: 'test-plugin' }); + expect(result).toBe(false); + }); + + test('shouldLoadPluginInFrontendSandbox returns true for commercial plugins in the enabled list', async () => { + // if getPluginDetails fails it fallsback to getPluginSettings + getPluginDetailsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginSettingsMock.mockResolvedValue({ ...fakePluginSettings, signatureType: PluginSignatureType.commercial }); + config.enableFrontendSandboxForPlugins = ['test-plugin']; + const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); + expect(result).toBe(true); + }); + + test('shouldLoadPluginInFrontendSandbox returns true for private plugins in the enabled list', async () => { + // if getPluginDetails fails it fallsback to getPluginSettings + getPluginDetailsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginSettingsMock.mockResolvedValue({ ...fakePluginSettings, signatureType: PluginSignatureType.private }); + config.enableFrontendSandboxForPlugins = ['test-plugin']; + const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); + expect(result).toBe(true); + }); + + test('isPluginFrontendSandboxEligible returns false for plugins with internal signature', async () => { + // if getPluginDetails fails it fallsback to getPluginSettings + getPluginDetailsMock.mockRejectedValueOnce(new Error('not found')); + + getPluginSettingsMock.mockResolvedValue({ ...fakePluginSettings, signature: PluginSignatureStatus.internal }); + const result = await isPluginFrontendSandboxEligible({ pluginId: 'test-plugin' }); + expect(result).toBe(false); + }); }); }); diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts index 9d94a3556e0..0d954d4ae75 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts @@ -1,6 +1,7 @@ import { PluginSignatureType } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { getPluginDetails } from '../admin/api'; import { getPluginSettings } from '../pluginSettings'; type SandboxEligibilityCheckParams = { @@ -61,17 +62,20 @@ export async function isPluginFrontendSandboxEligible({ return false; } + // don't run grafana-signed plugins in sandbox try { - // don't run grafana-signed plugins in sandbox - const pluginMeta = await getPluginSettings(pluginId, { showErrorAlert: false }); - if (pluginMeta.signatureType === PluginSignatureType.grafana || pluginMeta.signature === 'internal') { + //this can fail if gcom is not accesible + const details = await getPluginDetails(pluginId); + return details.signatureType !== PluginSignatureType.grafana && details.signature !== 'internal'; + } catch (e) { + try { + // this can fail if we are trying to fetch settings of a non-installed plugin + const pluginMeta = await getPluginSettings(pluginId, { showErrorAlert: false }); + return pluginMeta.signatureType !== PluginSignatureType.grafana && pluginMeta.signature !== 'internal'; + } catch (e) { return false; } - } catch (e) { - // this can fail if we are trying to fetch settings of a non-installed plugin - return false; } - return true; }