mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Display plugin permissions required (#78355)
* Add definition of external service registration * Add style and tables for permissions needed * Add external service registration to local without counterpart * Add feature toggle check * Add feature flag check in the backend as well * Add the disclaimer for permissions --------- Co-authored-by: Gabriel MABILLE <gabriel.mabille@grafana.com>
This commit is contained in:
parent
e27e2f66ba
commit
824e0f9ce8
@ -2,6 +2,7 @@ package dtos
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ type PluginListItem struct {
|
|||||||
SignatureOrg string `json:"signatureOrg"`
|
SignatureOrg string `json:"signatureOrg"`
|
||||||
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
|
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
|
||||||
AngularDetected bool `json:"angularDetected"`
|
AngularDetected bool `json:"angularDetected"`
|
||||||
|
IAM *plugindef.IAM `json:"iam,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginList []PluginListItem
|
type PluginList []PluginListItem
|
||||||
|
@ -144,6 +144,10 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons
|
|||||||
AngularDetected: pluginDef.Angular.Detected,
|
AngularDetected: pluginDef.Angular.Detected,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
|
||||||
|
listItem.IAM = pluginDef.IAM
|
||||||
|
}
|
||||||
|
|
||||||
update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), pluginDef.ID)
|
update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), pluginDef.ID)
|
||||||
if exists {
|
if exists {
|
||||||
listItem.LatestVersion = update
|
listItem.LatestVersion = update
|
||||||
|
@ -26,6 +26,7 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
|
|||||||
readme: localReadme || remote?.readme,
|
readme: localReadme || remote?.readme,
|
||||||
versions,
|
versions,
|
||||||
statusContext: remote?.statusContext ?? '',
|
statusContext: remote?.statusContext ?? '',
|
||||||
|
iam: remote?.json?.iam,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { AppPlugin, GrafanaTheme2, PluginContextProvider, UrlQueryMap } from '@grafana/data';
|
import { AppPlugin, GrafanaTheme2, PluginContextProvider, UrlQueryMap } from '@grafana/data';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { CellProps, Column, InteractiveTable, Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { VersionList } from '../components/VersionList';
|
import { VersionList } from '../components/VersionList';
|
||||||
import { usePluginConfig } from '../hooks/usePluginConfig';
|
import { usePluginConfig } from '../hooks/usePluginConfig';
|
||||||
import { CatalogPlugin, PluginTabIds } from '../types';
|
import { CatalogPlugin, Permission, PluginTabIds } from '../types';
|
||||||
|
|
||||||
import { AppConfigCtrlWrapper } from './AppConfigWrapper';
|
import { AppConfigCtrlWrapper } from './AppConfigWrapper';
|
||||||
import { PluginDashboards } from './PluginDashboards';
|
import { PluginDashboards } from './PluginDashboards';
|
||||||
@ -18,10 +19,28 @@ type Props = {
|
|||||||
pageId: string;
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Cell<T extends keyof Permission = keyof Permission> = CellProps<Permission, Permission[T]>;
|
||||||
|
|
||||||
export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.Element {
|
export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.Element {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { value: pluginConfig } = usePluginConfig(plugin);
|
const { value: pluginConfig } = usePluginConfig(plugin);
|
||||||
|
|
||||||
|
const columns: Array<Column<Permission>> = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'action',
|
||||||
|
header: 'Action',
|
||||||
|
cell: ({ cell: { value } }: Cell<'action'>) => value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scope',
|
||||||
|
header: 'Scope',
|
||||||
|
cell: ({ cell: { value } }: Cell<'scope'>) => value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
if (pageId === PluginTabIds.OVERVIEW) {
|
if (pageId === PluginTabIds.OVERVIEW) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -49,6 +68,31 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permissions will be returned in the iam field for installed plugins and in the details.iam field when fetching details from gcom
|
||||||
|
const permissions = plugin.iam?.permissions || plugin.details?.iam?.permissions;
|
||||||
|
|
||||||
|
const displayPermissions =
|
||||||
|
config.featureToggles.externalServiceAccounts &&
|
||||||
|
pageId === PluginTabIds.IAM &&
|
||||||
|
permissions &&
|
||||||
|
permissions.length > 0;
|
||||||
|
|
||||||
|
if (displayPermissions) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction="row">
|
||||||
|
The {plugin.name} plugin needs a service account to be able to query Grafana. The following list contains the
|
||||||
|
permissions available to the service account:
|
||||||
|
</Stack>
|
||||||
|
<InteractiveTable
|
||||||
|
columns={columns}
|
||||||
|
data={permissions}
|
||||||
|
getRowId={(permission: Permission) => String(permission.action)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (pluginConfig?.configPages) {
|
if (pluginConfig?.configPages) {
|
||||||
for (const configPage of pluginConfig.configPages) {
|
for (const configPage of pluginConfig.configPages) {
|
||||||
if (pageId === configPage.id) {
|
if (pageId === configPage.id) {
|
||||||
|
@ -161,6 +161,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
|
|||||||
accessControl: accessControl,
|
accessControl: accessControl,
|
||||||
angularDetected,
|
angularDetected,
|
||||||
isFullyInstalled: true,
|
isFullyInstalled: true,
|
||||||
|
iam: plugin.iam,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,6 +219,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
|
|||||||
accessControl: local?.accessControl,
|
accessControl: local?.accessControl,
|
||||||
angularDetected: local?.angularDetected || remote?.angularDetected,
|
angularDetected: local?.angularDetected || remote?.angularDetected,
|
||||||
isFullyInstalled: Boolean(local) || isDisabled,
|
isFullyInstalled: Boolean(local) || isDisabled,
|
||||||
|
iam: local?.iam,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +41,16 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, pageId?: PluginTabI
|
|||||||
return navModelChildren;
|
return navModelChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.featureToggles.externalServiceAccounts && (plugin?.iam || plugin?.details?.iam)) {
|
||||||
|
navModelChildren.push({
|
||||||
|
text: PluginTabLabels.IAM,
|
||||||
|
icon: 'shield',
|
||||||
|
id: PluginTabIds.IAM,
|
||||||
|
url: `${pathname}?page=${PluginTabIds.IAM}`,
|
||||||
|
active: PluginTabIds.IAM === currentPageId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (config.featureToggles.panelTitleSearch && pluginConfig.meta.type === PluginType.panel) {
|
if (config.featureToggles.panelTitleSearch && pluginConfig.meta.type === PluginType.panel) {
|
||||||
navModelChildren.push({
|
navModelChildren.push({
|
||||||
text: PluginTabLabels.USAGE,
|
text: PluginTabLabels.USAGE,
|
||||||
|
@ -62,6 +62,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata {
|
|||||||
// instance plugins may not be fully installed, which means a new instance
|
// instance plugins may not be fully installed, which means a new instance
|
||||||
// running the plugin didn't started yet
|
// running the plugin didn't started yet
|
||||||
isFullyInstalled?: boolean;
|
isFullyInstalled?: boolean;
|
||||||
|
iam?: IdentityAccessManagement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogPluginDetails {
|
export interface CatalogPluginDetails {
|
||||||
@ -74,6 +75,7 @@ export interface CatalogPluginDetails {
|
|||||||
grafanaDependency?: string;
|
grafanaDependency?: string;
|
||||||
pluginDependencies?: PluginDependencies['plugins'];
|
pluginDependencies?: PluginDependencies['plugins'];
|
||||||
statusContext?: string;
|
statusContext?: string;
|
||||||
|
iam?: IdentityAccessManagement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogPluginInfo {
|
export interface CatalogPluginInfo {
|
||||||
@ -93,6 +95,7 @@ export type RemotePlugin = {
|
|||||||
internal: boolean;
|
internal: boolean;
|
||||||
json?: {
|
json?: {
|
||||||
dependencies: PluginDependencies;
|
dependencies: PluginDependencies;
|
||||||
|
iam?: IdentityAccessManagement;
|
||||||
info: {
|
info: {
|
||||||
links: Array<{
|
links: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
@ -175,8 +178,18 @@ export type LocalPlugin = WithAccessControlMetadata & {
|
|||||||
type: PluginType;
|
type: PluginType;
|
||||||
dependencies: PluginDependencies;
|
dependencies: PluginDependencies;
|
||||||
angularDetected: boolean;
|
angularDetected: boolean;
|
||||||
|
iam?: IdentityAccessManagement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IdentityAccessManagement {
|
||||||
|
permissions: Permission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Permission {
|
||||||
|
action: string;
|
||||||
|
scope: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Rel {
|
interface Rel {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
@ -231,6 +244,7 @@ export enum PluginTabLabels {
|
|||||||
CONFIG = 'Config',
|
CONFIG = 'Config',
|
||||||
DASHBOARDS = 'Dashboards',
|
DASHBOARDS = 'Dashboards',
|
||||||
USAGE = 'Usage',
|
USAGE = 'Usage',
|
||||||
|
IAM = 'IAM',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PluginTabIds {
|
export enum PluginTabIds {
|
||||||
@ -239,6 +253,7 @@ export enum PluginTabIds {
|
|||||||
CONFIG = 'config',
|
CONFIG = 'config',
|
||||||
DASHBOARDS = 'dashboards',
|
DASHBOARDS = 'dashboards',
|
||||||
USAGE = 'usage',
|
USAGE = 'usage',
|
||||||
|
IAM = 'iam',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RequestStatus {
|
export enum RequestStatus {
|
||||||
|
Loading…
Reference in New Issue
Block a user