Plugins: Expose core APIs only for certain plugins (#107967)

* feat(plugins): add a way to expose core apis only to certain plugins

* review: update naming

* review: update the owners of the feature toggle

* feat: share the restricted apis with extensions

* fix: linters

* feat: remove the `addPanel` api

* chore: fix linting and betterer issue

* tests: use `@ts-expect-error` for more clarity
This commit is contained in:
Levente Balogh 2025-09-01 11:57:00 +02:00 committed by GitHub
parent da43e2ae07
commit d31e682345
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 504 additions and 43 deletions

View File

@ -28,6 +28,9 @@ exports[`better eslint`] = {
"packages/grafana-alerting/src/grafana/notificationPolicies/utils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-data/src/dataframe/ArrayDataFrame.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],

View File

@ -2203,3 +2203,15 @@ fail_tests_on_console = true
# Whether to enable betterer eslint rules for local development
# Useful if you want to always see betterer rules that we're trying to fix so they're more prevalent
betterer_eslint_rules = false
#################################### Plugin API Restrictions ##########################################
# Configure which plugins can access specific restricted APIs.
# Use plugin IDs or regex patterns. Allow list takes precedence over block list.
[plugins.restricted_apis_allowlist]
# Example: Allow specific plugins to access an API
# addPanel = "myorg-admin-app, grafana-enterprise-.*"
[plugins.restricted_apis_blocklist]
# Example: Block specific plugins from accessing an API
# addPanel = "untrusted-.*, experimental-.*"

View File

@ -0,0 +1,197 @@
import { renderHook, RenderHookResult } from '@testing-library/react';
import {
RestrictedGrafanaApisContextProvider,
RestrictedGrafanaApisContextType,
useRestrictedGrafanaApis,
} from './RestrictedGrafanaApis';
describe('RestrictedGrafanaApis', () => {
const apis: RestrictedGrafanaApisContextType = {
addPanel: () => {},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should share an API if the plugin is allowed', () => {
const { result } = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'grafana-test-app'}
apiAllowList={{ addPanel: ['grafana-test-app'] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.current.addPanel).toEqual(apis.addPanel);
expect(Object.keys(result.current)).toEqual(['addPanel']);
});
it('should share an API if the plugin is allowed using a regexp', () => {
const { result } = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'grafana-test-app'}
apiAllowList={{ addPanel: [/^grafana-/] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.current.addPanel).toEqual(apis.addPanel);
expect(Object.keys(result.current)).toEqual(['addPanel']);
});
it('should not share an API if the plugin is not directly allowed and no allow regexp matches it', () => {
const { result } = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'myorg-test-app'}
apiAllowList={{ addPanel: [/^grafana-/] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.current.addPanel).not.toBeDefined();
});
// Ideally the `allowList` and the `blockList` are not used together
it('should share an API if the plugin is both allowed and blocked (allow-list takes precendence)', () => {
const { result } = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'grafana-test-app'}
apiAllowList={{ addPanel: ['grafana-test-app'] }}
apiBlockList={{ addPanel: ['grafana-test-app'] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.current.addPanel).toEqual(apis.addPanel);
expect(Object.keys(result.current)).toEqual(['addPanel']);
});
it('should share an API with allowed plugins (testing multiple plugins)', () => {
let result: RenderHookResult<RestrictedGrafanaApisContextType, unknown>;
// 1. First app
result = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'grafana-test-app'}
apiAllowList={{ addPanel: ['grafana-test-app', 'grafana-assistant-app'] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.result.current.addPanel).toEqual(apis.addPanel);
// 2. Second app
result = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'grafana-assistant-app'}
apiAllowList={{ addPanel: ['grafana-test-app', 'grafana-assistant-app'] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.result.current.addPanel).toEqual(apis.addPanel);
});
it('should not share APIs with plugins that are not allowed', () => {
const { result } = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'grafana-restricted-app'}
apiAllowList={{ addPanel: ['grafana-authorised-app'] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.current.addPanel).not.toBeDefined();
});
it('should not share APIs with anyone if both the allowList and the blockList are empty', () => {
let result: RenderHookResult<RestrictedGrafanaApisContextType, unknown>;
result = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider apis={apis} pluginId={'grafana-test-app'} apiAllowList={{ addPanel: [] }}>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.result.current.addPanel).not.toBeDefined();
result = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider apis={apis} pluginId={'grafana-test-app'}>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.result.current.addPanel).not.toBeDefined();
});
it('should not share APIs with blocked plugins', () => {
const { result } = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'grafana-test-app'}
apiBlockList={{ addPanel: ['grafana-test-app'] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.current.addPanel).not.toBeDefined();
});
it('should not share APIs with plugins that match any block list regexes', () => {
const { result } = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
<RestrictedGrafanaApisContextProvider
apis={apis}
pluginId={'myorg-test-app'}
apiBlockList={{ addPanel: [/^myorg-/] }}
>
{children}
</RestrictedGrafanaApisContextProvider>
),
});
// @ts-expect-error No APIs are defined yet
expect(result.current.addPanel).not.toBeDefined();
});
});

View File

@ -0,0 +1,79 @@
import { createContext, ReactElement, PropsWithChildren, useMemo, useContext } from 'react';
export interface RestrictedGrafanaApisContextTypeInternal {
// Add types for restricted Grafana APIs here
// (Make sure that they are typed as optional properties)
// e.g. addPanel?: (vizPanel: VizPanel) => void;
}
// We are exposing this through a "type validation", to make sure that all APIs are optional (which helps plugins catering for scenarios when they are not available).
type RequireAllPropertiesOptional<T> = keyof T extends never
? T
: { [K in keyof T]-?: undefined extends T[K] ? never : K }[keyof T] extends never
? T
: 'Error: all properties of `RestrictedGrafanaApisContextTypeInternal` must be marked as optional, as their availability is controlled via a configuration parameter. Please have a look at `RestrictedGrafanaApisContextTypeInternal`.';
export type RestrictedGrafanaApisContextType = RequireAllPropertiesOptional<RestrictedGrafanaApisContextTypeInternal>;
// A type for allowing / blocking plugins for a given API
export type RestrictedGrafanaApisAllowList = Partial<
Record<keyof RestrictedGrafanaApisContextType | string, Array<string | RegExp>>
>;
export const RestrictedGrafanaApisContext = createContext<RestrictedGrafanaApisContextType>({});
export type Props = {
pluginId: string;
apis: RestrictedGrafanaApisContextType;
// Use it to share APIs with plugins (TAKES PRECEDENCE over `apiBlockList`)
apiAllowList?: RestrictedGrafanaApisAllowList;
// Use it to disable sharing APIs with plugins.
apiBlockList?: RestrictedGrafanaApisAllowList;
};
export function RestrictedGrafanaApisContextProvider(props: PropsWithChildren<Props>): ReactElement {
const { children, pluginId, apis, apiAllowList, apiBlockList } = props;
const allowedApis = useMemo(() => {
const allowedApis: RestrictedGrafanaApisContextType = {};
for (const api of Object.keys(apis) as Array<keyof RestrictedGrafanaApisContextType>) {
if (
apiAllowList &&
apiAllowList[api] &&
(apiAllowList[api].includes(pluginId) ||
apiAllowList[api].some((keyword) => keyword instanceof RegExp && keyword.test(pluginId)))
) {
allowedApis[api] = apis[api];
continue;
}
// IF no allow list is defined (only block list), then we only omit the blocked APIs
if (
(!apiAllowList || Object.keys(apiAllowList).length === 0) &&
apiBlockList &&
apiBlockList[api] &&
!(
apiBlockList[api].includes(pluginId) ||
apiBlockList[api].some((keyword) => keyword instanceof RegExp && keyword.test(pluginId))
)
) {
allowedApis[api] = apis[api];
}
}
return allowedApis;
}, [apis, apiAllowList, apiBlockList, pluginId]);
return <RestrictedGrafanaApisContext.Provider value={allowedApis}>{children}</RestrictedGrafanaApisContext.Provider>;
}
export function useRestrictedGrafanaApis(): RestrictedGrafanaApisContextType {
const context = useContext(RestrictedGrafanaApisContext);
if (!context) {
throw new Error(
'useRestrictedGrafanaApis() can only be used inside a plugin context (The `RestrictedGrafanaApisContext` is not available).'
);
}
return context;
}

View File

@ -439,6 +439,13 @@ export {
type DataSourcePluginContextType,
PluginContext,
} from './context/plugins/PluginContext';
export {
type RestrictedGrafanaApisContextType,
type RestrictedGrafanaApisAllowList,
RestrictedGrafanaApisContext,
RestrictedGrafanaApisContextProvider,
useRestrictedGrafanaApis,
} from './context/plugins/RestrictedGrafanaApis';
export { type PluginContextProviderProps, PluginContextProvider } from './context/plugins/PluginContextProvider';
export {
type DataSourcePluginContextProviderProps,

View File

@ -311,6 +311,8 @@ export interface GrafanaConfig {
exploreDefaultTimeOffset: string;
exploreHideLogsDownload: boolean;
quickRanges?: TimeOption[];
pluginRestrictedAPIsAllowList?: Record<string, string[]>;
pluginRestrictedAPIsBlockList?: Record<string, string[]>;
// The namespace to use for kubernetes apiserver requests
namespace: string;

View File

@ -1076,10 +1076,14 @@ export interface FeatureToggles {
dashboardLevelTimeMacros?: boolean;
/**
* Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.
* @default false
*/
alertmanagerRemoteSecondaryWithRemoteState?: boolean;
/**
* Enables sharing a list of APIs with a list of plugins
* @default false
*/
restrictedPluginApis?: boolean;
/**
* Enable adhoc filter buttons in visualization tooltips
*/
adhocFiltersInTooltips?: boolean;

View File

@ -242,6 +242,8 @@ export class GrafanaBootConfig {
exploreDefaultTimeOffset = '1h';
exploreHideLogsDownload?: boolean;
quickRanges?: TimeOption[];
pluginRestrictedAPIsAllowList?: Record<string, string[]>;
pluginRestrictedAPIsBlockList?: Record<string, string[]>;
/**
* Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of

View File

@ -207,25 +207,27 @@ type FrontendSettingsDTO struct {
DashboardPerformanceMetrics []string `json:"dashboardPerformanceMetrics"`
PanelSeriesLimit int `json:"panelSeriesLimit"`
FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"`
ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"`
ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"`
DisableLoginForm bool `json:"disableLoginForm"`
DisableUserSignUp bool `json:"disableUserSignUp"`
LoginHint string `json:"loginHint"`
PasswordHint string `json:"passwordHint"`
ExternalUserMngInfo string `json:"externalUserMngInfo"`
ExternalUserMngLinkUrl string `json:"externalUserMngLinkUrl"`
ExternalUserMngLinkName string `json:"externalUserMngLinkName"`
ExternalUserMngAnalytics bool `json:"externalUserMngAnalytics"`
ExternalUserMngAnalyticsParams string `json:"externalUserMngAnalyticsParams"`
ViewersCanEdit bool `json:"viewersCanEdit"`
DisableSanitizeHtml bool `json:"disableSanitizeHtml"`
TrustedTypesDefaultPolicyEnabled bool `json:"trustedTypesDefaultPolicyEnabled"`
CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"`
EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"`
ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"`
ExploreHideLogsDownload bool `json:"exploreHideLogsDownload"`
FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"`
ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"`
ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"`
DisableLoginForm bool `json:"disableLoginForm"`
DisableUserSignUp bool `json:"disableUserSignUp"`
LoginHint string `json:"loginHint"`
PasswordHint string `json:"passwordHint"`
ExternalUserMngInfo string `json:"externalUserMngInfo"`
ExternalUserMngLinkUrl string `json:"externalUserMngLinkUrl"`
ExternalUserMngLinkName string `json:"externalUserMngLinkName"`
ExternalUserMngAnalytics bool `json:"externalUserMngAnalytics"`
ExternalUserMngAnalyticsParams string `json:"externalUserMngAnalyticsParams"`
ViewersCanEdit bool `json:"viewersCanEdit"`
DisableSanitizeHtml bool `json:"disableSanitizeHtml"`
TrustedTypesDefaultPolicyEnabled bool `json:"trustedTypesDefaultPolicyEnabled"`
CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"`
EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"`
PluginRestrictedAPIsAllowList map[string][]string `json:"pluginRestrictedAPIsAllowList"`
PluginRestrictedAPIsBlockList map[string][]string `json:"pluginRestrictedAPIsBlockList"`
ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"`
ExploreHideLogsDownload bool `json:"exploreHideLogsDownload"`
Auth FrontendSettingsAuthDTO `json:"auth"`

View File

@ -250,6 +250,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
QuickRanges: hs.Cfg.QuickRanges,
SecureSocksDSProxyEnabled: hs.Cfg.SecureSocksDSProxy.Enabled && hs.Cfg.SecureSocksDSProxy.ShowUI,
EnableFrontendSandboxForPlugins: hs.Cfg.EnableFrontendSandboxForPlugins,
PluginRestrictedAPIsAllowList: hs.Cfg.PluginRestrictedAPIsAllowList,
PluginRestrictedAPIsBlockList: hs.Cfg.PluginRestrictedAPIsBlockList,
PublicDashboardAccessToken: c.PublicDashboardAccessToken,
PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled,
CloudMigrationIsTarget: isCloudMigrationTarget,

View File

@ -1869,6 +1869,15 @@ var (
Owner: grafanaAlertingSquad,
HideFromAdminPage: true,
HideFromDocs: true,
},
{
Name: "restrictedPluginApis",
Description: "Enables sharing a list of APIs with a list of plugins",
Stage: FeatureStageExperimental,
Owner: grafanaPluginsPlatformSquad,
HideFromAdminPage: true,
HideFromDocs: true,
FrontendOnly: true,
Expression: "false",
},
{

View File

@ -241,6 +241,7 @@ unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,f
dashboardDsAdHocFiltering,experimental,@grafana/datapro,false,false,true
dashboardLevelTimeMacros,experimental,@grafana/dashboards-squad,false,false,true
alertmanagerRemoteSecondaryWithRemoteState,experimental,@grafana/alerting-squad,false,false,false
restrictedPluginApis,experimental,@grafana/plugins-platform-backend,false,false,true
adhocFiltersInTooltips,experimental,@grafana/datapro,false,false,true
favoriteDatasources,experimental,@grafana/plugins-platform-backend,false,false,true
newLogContext,experimental,@grafana/observability-logs,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
241 dashboardDsAdHocFiltering experimental @grafana/datapro false false true
242 dashboardLevelTimeMacros experimental @grafana/dashboards-squad false false true
243 alertmanagerRemoteSecondaryWithRemoteState experimental @grafana/alerting-squad false false false
244 restrictedPluginApis experimental @grafana/plugins-platform-backend false false true
245 adhocFiltersInTooltips experimental @grafana/datapro false false true
246 favoriteDatasources experimental @grafana/plugins-platform-backend false false true
247 newLogContext experimental @grafana/observability-logs false false true

View File

@ -975,6 +975,10 @@ const (
// Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.
FlagAlertmanagerRemoteSecondaryWithRemoteState = "alertmanagerRemoteSecondaryWithRemoteState"
// FlagRestrictedPluginApis
// Enables sharing a list of APIs with a list of plugins
FlagRestrictedPluginApis = "restrictedPluginApis"
// FlagAdhocFiltersInTooltips
// Enable adhoc filter buttons in visualization tooltips
FlagAdhocFiltersInTooltips = "adhocFiltersInTooltips"

View File

@ -572,16 +572,18 @@
{
"metadata": {
"name": "alertmanagerRemoteSecondaryWithRemoteState",
"resourceVersion": "1753448760331",
"creationTimestamp": "2025-07-25T13:06:00Z"
"resourceVersion": "1753776005753",
"creationTimestamp": "2025-07-25T13:06:00Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-29 08:00:05.753498 +0000 UTC"
}
},
"spec": {
"description": "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
"hideFromDocs": true
}
},
{
@ -2889,6 +2891,25 @@
"expression": "false"
}
},
{
"metadata": {
"name": "restrictedPluginApis",
"resourceVersion": "1753776783657",
"creationTimestamp": "2025-07-25T07:46:26Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-29 08:13:03.657209 +0000 UTC"
}
},
"spec": {
"description": "Enables sharing a list of APIs with a list of plugins",
"stage": "experimental",
"codeowner": "@grafana/plugins-platform-backend",
"frontend": true,
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "rolePickerDrawer",

View File

@ -214,6 +214,10 @@ type Cfg struct {
PluginUpdateStrategy string
// Plugin API restrictions - maps API name to list of plugin IDs/patterns
PluginRestrictedAPIsAllowList map[string][]string
PluginRestrictedAPIsBlockList map[string][]string
// Panels
DisableSanitizeHtml bool
@ -1057,6 +1061,10 @@ func NewCfg() *Cfg {
Raw: ini.Empty(),
Azure: &azsettings.AzureSettings{},
// Initialize plugin API restriction maps
PluginRestrictedAPIsAllowList: make(map[string][]string),
PluginRestrictedAPIsBlockList: make(map[string][]string),
// Avoid nil pointer
IsFeatureToggleEnabled: func(_ string) bool {
return false

View File

@ -110,6 +110,26 @@ func (cfg *Cfg) processPreinstallPlugins(rawInstallPlugins []string, preinstallP
}
}
// readPluginAPIRestrictionsSection reads a plugin API restrictions section and returns a map of API names to plugin lists
func readPluginAPIRestrictionsSection(iniFile *ini.File, sectionName string) map[string][]string {
result := make(map[string][]string)
if !iniFile.HasSection(sectionName) {
return result
}
section := iniFile.Section(sectionName)
for _, key := range section.Keys() {
apiName := key.Name()
pluginList := util.SplitString(key.MustString(""))
if len(pluginList) > 0 {
result[apiName] = pluginList
}
}
return result
}
func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error {
pluginsSection := iniFile.Section("plugins")
@ -179,5 +199,9 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error {
cfg.PluginUpdateStrategy = pluginsSection.Key("update_strategy").In(PluginUpdateStrategyLatest, []string{PluginUpdateStrategyLatest, PluginUpdateStrategyMinor})
// Plugin API restrictions - read from sections
cfg.PluginRestrictedAPIsAllowList = readPluginAPIRestrictionsSection(iniFile, "plugins.restricted_apis_allowlist")
cfg.PluginRestrictedAPIsBlockList = readPluginAPIRestrictionsSection(iniFile, "plugins.restricted_apis_blocklist")
return nil
}

View File

@ -38,6 +38,7 @@ import { buildPluginSectionNav, pluginsLogger } from '../utils';
import { PluginErrorBoundary } from './PluginErrorBoundary';
import { buildPluginPageContext, PluginPageContext } from './PluginPageContext';
import { RestrictedGrafanaApisProvider } from './restrictedGrafanaApis/RestrictedGrafanaApisProvider';
interface Props {
// The ID of the plugin we would like to load and display
@ -116,22 +117,24 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
/>
)}
>
<ExtensionRegistriesProvider
registries={{
addedLinksRegistry: addedLinksRegistry.readOnly(),
addedComponentsRegistry: addedComponentsRegistry.readOnly(),
exposedComponentsRegistry: exposedComponentsRegistry.readOnly(),
addedFunctionsRegistry: addedFunctionsRegistry.readOnly(),
}}
>
<plugin.root
meta={plugin.meta}
basename={location.pathname}
onNavChanged={onNavChanged}
query={queryParams}
path={location.pathname}
/>
</ExtensionRegistriesProvider>
<RestrictedGrafanaApisProvider pluginId={pluginId}>
<ExtensionRegistriesProvider
registries={{
addedLinksRegistry: addedLinksRegistry.readOnly(),
addedComponentsRegistry: addedComponentsRegistry.readOnly(),
exposedComponentsRegistry: exposedComponentsRegistry.readOnly(),
addedFunctionsRegistry: addedFunctionsRegistry.readOnly(),
}}
>
<plugin.root
meta={plugin.meta}
basename={location.pathname}
onNavChanged={onNavChanged}
query={queryParams}
path={location.pathname}
/>
</ExtensionRegistriesProvider>
</RestrictedGrafanaApisProvider>
</PluginErrorBoundary>
</PluginContextProvider>
);

View File

@ -0,0 +1,48 @@
# Restricted Grafana APIs
The APIs available here are used to be only shared with certain plugins using the `RestrictedGrafanaApisContextProvider`.
### FAQ
**When should I use it to expose an API?**
If you only would like to share functionality with certain plugin IDs.
**How to add an API to the list?**
1. Add the API to a separate file under `public/app/features/plugins/components/restrictedGrafanaApis/`
2. Reference the API in the `restrictedGrafanaApis` variable in `public/app/features/plugins/components/restrictedGrafanaApis/RestrictedGrafanaApisProvider.tsx`
3. Update the `RestrictedGrafanaApisContextType` type under `packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx`
**How to share an API with plugins?**
Enabling plugins is done via the Grafana config (config.ini).
**Enabling APIs for a plugin**
```ini
[plugins.restricted_apis_allowlist]
# This will share the `addPanel` api with app plugins that either have an id of "myorg-test-app"
addPanel = "myorg-test-app
```
**Disabling APIs for a plugin**
```ini
[plugins.restricted_apis_blocklist]
# This is not sharing the `addPanel` api with app plugins that either have an id of "myorg-test-app"
addPanel = "myorg-test-app"
```
**How to use restricted APIs in a plugin?**
You should be access the restricted APIs in your plugin using the `useRestrictedGrafanaApis()` hook:
```ts
import { RestrictedGrafanaApisContextType, useRestrictedGrafanaApis } from "@grafana/data";
// Inside a component
const { addPanel } = useRestrictedGrafanaApis();
// Make sure you cater for scenarios where the API is not available
if (addPanel) {
addPanel({ ... });
}
```

View File

@ -0,0 +1,29 @@
import { PropsWithChildren, ReactElement } from 'react';
import { RestrictedGrafanaApisContextProvider, RestrictedGrafanaApisContextType } from '@grafana/data';
import { config } from '@grafana/runtime';
const restrictedGrafanaApis: RestrictedGrafanaApisContextType = config.featureToggles.restrictedPluginApis
? {
// Add your restricted APIs here
// (APIs that should be availble to ALL plugins should be shared via our packages, e.g. @grafana/data.)
}
: {};
// This Provider is a wrapper around `RestrictedGrafanaApisContextProvider` from `@grafana/data`.
// The reason for this is that like this we only need to define the configuration once (here) and can use it in multiple places (app root page, extensions).
export function RestrictedGrafanaApisProvider({
children,
pluginId,
}: PropsWithChildren<{ pluginId: string }>): ReactElement {
return (
<RestrictedGrafanaApisContextProvider
pluginId={pluginId}
apis={restrictedGrafanaApis}
apiAllowList={config.bootData.settings.pluginRestrictedAPIsAllowList}
apiBlockList={config.bootData.settings.pluginRestrictedAPIsBlockList}
>
{children}
</RestrictedGrafanaApisContextProvider>
);
}

View File

@ -22,6 +22,8 @@ import appEvents from 'app/core/app_events';
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ShowModalReactEvent } from 'app/types/events';
import { RestrictedGrafanaApisProvider } from '../components/restrictedGrafanaApis/RestrictedGrafanaApisProvider';
import { ExtensionErrorBoundary } from './ExtensionErrorBoundary';
import { ExtensionsLog, log as baseLog } from './logs/log';
import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry';
@ -98,9 +100,11 @@ export const wrapWithPluginContext = <T,>({
return (
<PluginContextProvider meta={pluginMeta}>
<ExtensionErrorBoundary pluginId={pluginId} extensionTitle={extensionTitle} log={log}>
<Component
{...writableProxy(props, { log, source: 'extension', pluginId, pluginVersion: pluginMeta.info?.version })}
/>
<RestrictedGrafanaApisProvider pluginId={pluginId}>
<Component
{...writableProxy(props, { log, source: 'extension', pluginId, pluginVersion: pluginMeta.info?.version })}
/>
</RestrictedGrafanaApisProvider>
</ExtensionErrorBoundary>
</PluginContextProvider>
);