diff --git a/.betterer.results b/.betterer.results
index fb78b68f6f9..5ea78d93fbe 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -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"]
],
diff --git a/conf/defaults.ini b/conf/defaults.ini
index 3908ddf4ad4..2bc06325d6c 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -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-.*"
diff --git a/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.test.tsx b/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.test.tsx
new file mode 100644
index 00000000000..5ba64517caf
--- /dev/null
+++ b/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.test.tsx
@@ -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 }) => (
+
+ {children}
+
+ ),
+ });
+
+ // @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 }) => (
+
+ {children}
+
+ ),
+ });
+
+ // @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 }) => (
+
+ {children}
+
+ ),
+ });
+
+ // @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 }) => (
+
+ {children}
+
+ ),
+ });
+
+ // @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;
+
+ // 1. First app
+ result = renderHook(() => useRestrictedGrafanaApis(), {
+ wrapper: ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+ // @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 }) => (
+
+ {children}
+
+ ),
+ });
+ // @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 }) => (
+
+ {children}
+
+ ),
+ });
+
+ // @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;
+
+ result = renderHook(() => useRestrictedGrafanaApis(), {
+ wrapper: ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+ // @ts-expect-error No APIs are defined yet
+ expect(result.result.current.addPanel).not.toBeDefined();
+
+ result = renderHook(() => useRestrictedGrafanaApis(), {
+ wrapper: ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+ // @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 }) => (
+
+ {children}
+
+ ),
+ });
+ // @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 }) => (
+
+ {children}
+
+ ),
+ });
+ // @ts-expect-error No APIs are defined yet
+ expect(result.current.addPanel).not.toBeDefined();
+ });
+});
diff --git a/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx b/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx
new file mode 100644
index 00000000000..7347db86545
--- /dev/null
+++ b/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx
@@ -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 = 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;
+
+// A type for allowing / blocking plugins for a given API
+export type RestrictedGrafanaApisAllowList = Partial<
+ Record>
+>;
+
+export const RestrictedGrafanaApisContext = createContext({});
+
+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): ReactElement {
+ const { children, pluginId, apis, apiAllowList, apiBlockList } = props;
+ const allowedApis = useMemo(() => {
+ const allowedApis: RestrictedGrafanaApisContextType = {};
+
+ for (const api of Object.keys(apis) as Array) {
+ 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 {children};
+}
+
+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;
+}
diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts
index d1d302275d1..edb20df0082 100644
--- a/packages/grafana-data/src/index.ts
+++ b/packages/grafana-data/src/index.ts
@@ -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,
diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts
index 87136be6e1c..485b1ec4294 100644
--- a/packages/grafana-data/src/types/config.ts
+++ b/packages/grafana-data/src/types/config.ts
@@ -311,6 +311,8 @@ export interface GrafanaConfig {
exploreDefaultTimeOffset: string;
exploreHideLogsDownload: boolean;
quickRanges?: TimeOption[];
+ pluginRestrictedAPIsAllowList?: Record;
+ pluginRestrictedAPIsBlockList?: Record;
// The namespace to use for kubernetes apiserver requests
namespace: string;
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 85e9c6c1255..cc50e705d4b 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -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;
diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts
index ec6e967cb7a..2576de932b2 100644
--- a/packages/grafana-runtime/src/config.ts
+++ b/packages/grafana-runtime/src/config.ts
@@ -242,6 +242,8 @@ export class GrafanaBootConfig {
exploreDefaultTimeOffset = '1h';
exploreHideLogsDownload?: boolean;
quickRanges?: TimeOption[];
+ pluginRestrictedAPIsAllowList?: Record;
+ pluginRestrictedAPIsBlockList?: Record;
/**
* Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of
diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go
index 601654dc566..dd1313de717 100644
--- a/pkg/api/dtos/frontend_settings.go
+++ b/pkg/api/dtos/frontend_settings.go
@@ -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"`
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index b9fff7d46fd..c7ee1818700 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -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,
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index bbad4d1c9ba..4f2911a3069 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -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",
},
{
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index c770f0fcc36..8895ebcc190 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -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
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index e1c9df82f22..13041b4714c 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -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"
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 709841d5f36..a7dc6c26854 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -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",
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index a475dfd2a14..6c384c42fac 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -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
diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go
index 3739e253cfc..160e17169b7 100644
--- a/pkg/setting/setting_plugins.go
+++ b/pkg/setting/setting_plugins.go
@@ -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
}
diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx
index 7210f0ff471..ad94e6ea460 100644
--- a/public/app/features/plugins/components/AppRootPage.tsx
+++ b/public/app/features/plugins/components/AppRootPage.tsx
@@ -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) {
/>
)}
>
-
-
-
+
+
+
+
+
);
diff --git a/public/app/features/plugins/components/restrictedGrafanaApis/README.md b/public/app/features/plugins/components/restrictedGrafanaApis/README.md
new file mode 100644
index 00000000000..5f95291318d
--- /dev/null
+++ b/public/app/features/plugins/components/restrictedGrafanaApis/README.md
@@ -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({ ... });
+}
+```
diff --git a/public/app/features/plugins/components/restrictedGrafanaApis/RestrictedGrafanaApisProvider.tsx b/public/app/features/plugins/components/restrictedGrafanaApis/RestrictedGrafanaApisProvider.tsx
new file mode 100644
index 00000000000..3ecbd8f95c0
--- /dev/null
+++ b/public/app/features/plugins/components/restrictedGrafanaApis/RestrictedGrafanaApisProvider.tsx
@@ -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 (
+
+ {children}
+
+ );
+}
diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx
index dc195f08888..5d5498ce0ac 100644
--- a/public/app/features/plugins/extensions/utils.tsx
+++ b/public/app/features/plugins/extensions/utils.tsx
@@ -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 = ({
return (
-
+
+
+
);