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:
linoman 2023-12-20 09:29:13 -06:00 committed by GitHub
parent e27e2f66ba
commit 824e0f9ce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 81 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

@ -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,
}; };
} }

View File

@ -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) {

View File

@ -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,
}; };
} }

View File

@ -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,

View File

@ -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 {