From 40207c53ae8a4ed69bbb16f24eddad4b12b98530 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 26 Jun 2024 17:29:17 +0200 Subject: [PATCH] Plugins: Expose functions to plugins for checking RBAC permissions (#89047) * feat(grafana-data): create rbac functions for checking permissions * feat(grafana-runtime): pass current user to runtime * feat(grafana-runtime): expose rbac functions to check permissions against current user * refactor(contextsrv): use functions from grafana/data to check rbac permissions against user * Apply suggestions from code review Co-authored-by: Marcus Andersson * chore(rbac): fix missing types imports * refactor(rbac): make exposed functions return boolean --------- Co-authored-by: Marcus Andersson --- packages/grafana-data/src/index.ts | 7 +++++ packages/grafana-data/src/rbac/rbac.ts | 19 ++++++++++++ packages/grafana-runtime/src/index.ts | 1 + .../grafana-runtime/src/services/index.ts | 1 + packages/grafana-runtime/src/services/user.ts | 29 +++++++++++++++++++ packages/grafana-runtime/src/utils/rbac.ts | 18 ++++++++++++ public/app/app.ts | 2 ++ public/app/core/services/context_srv.ts | 16 +++++++--- 8 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 packages/grafana-data/src/rbac/rbac.ts create mode 100644 packages/grafana-runtime/src/services/user.ts create mode 100644 packages/grafana-runtime/src/utils/rbac.ts diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 8ce1be9fe18..d0f7dc5015d 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -50,3 +50,10 @@ export { CircularVector } from './vector/CircularVector'; export { vectorator } from './vector/FunctionalVector'; export { ArrayVector } from './vector/ArrayVector'; export * from './dataframe/CircularDataFrame'; +export { + type CurrentUser, + userHasPermission, + userHasPermissionInMetadata, + userHasAllPermissions, + userHasAnyPermission, +} from './rbac/rbac'; diff --git a/packages/grafana-data/src/rbac/rbac.ts b/packages/grafana-data/src/rbac/rbac.ts new file mode 100644 index 00000000000..90e87522caa --- /dev/null +++ b/packages/grafana-data/src/rbac/rbac.ts @@ -0,0 +1,19 @@ +import { CurrentUserDTO, WithAccessControlMetadata } from '../types'; + +export interface CurrentUser extends Omit {} + +export function userHasPermission(action: string, user: CurrentUser): boolean { + return !!user.permissions?.[action]; +} + +export function userHasPermissionInMetadata(action: string, object: WithAccessControlMetadata): boolean { + return !!object.accessControl?.[action]; +} + +export function userHasAllPermissions(actions: string[], user: CurrentUser) { + return actions.every((action) => userHasPermission(action, user)); +} + +export function userHasAnyPermission(actions: string[], user: CurrentUser) { + return actions.some((action) => userHasPermission(action, user)); +} diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index f5fc864e640..b7bf8246a93 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -52,3 +52,4 @@ export { export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter'; export { setReturnToPreviousHook, useReturnToPrevious } from './utils/returnToPrevious'; export { type EmbeddedDashboardProps, EmbeddedDashboard, setEmbeddedDashboard } from './components/EmbeddedDashboard'; +export { hasPermission, hasPermissionInMetadata, hasAllPermissions, hasAnyPermission } from './utils/rbac'; diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index a0dc7db5ce5..1a2cf79cc51 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -33,3 +33,4 @@ export { export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent'; export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils'; +export { setCurrentUser } from './user'; diff --git a/packages/grafana-runtime/src/services/user.ts b/packages/grafana-runtime/src/services/user.ts new file mode 100644 index 00000000000..1ca4e43b301 --- /dev/null +++ b/packages/grafana-runtime/src/services/user.ts @@ -0,0 +1,29 @@ +import { CurrentUser } from '@grafana/data'; + +let singletonInstance: CurrentUser | null = null; + +/** + * Used during startup by Grafana to set the current user so it is available + * for rbac checks. + * + * @internal + */ +export function setCurrentUser(instance: CurrentUser) { + if (singletonInstance) { + throw new Error('User should only be set once, when Grafana is starting.'); + } + singletonInstance = instance; +} + +/** + * Used to retrieve the current user. + * + * @internal + * + */ +export function getCurrentUser(): CurrentUser { + if (!singletonInstance) { + throw new Error('User can only be used after Grafana instance has started.'); + } + return singletonInstance; +} diff --git a/packages/grafana-runtime/src/utils/rbac.ts b/packages/grafana-runtime/src/utils/rbac.ts new file mode 100644 index 00000000000..12ffabff4ee --- /dev/null +++ b/packages/grafana-runtime/src/utils/rbac.ts @@ -0,0 +1,18 @@ +import { + userHasPermission, + userHasPermissionInMetadata, + userHasAllPermissions, + userHasAnyPermission, + WithAccessControlMetadata, +} from '@grafana/data'; + +import { getCurrentUser } from '../services/user'; + +export const hasPermission = (action: string) => userHasPermission(action, getCurrentUser()); + +export const hasPermissionInMetadata = (action: string, object: WithAccessControlMetadata) => + userHasPermissionInMetadata(action, object); + +export const hasAllPermissions = (actions: string[]) => userHasAllPermissions(actions, getCurrentUser()); + +export const hasAnyPermission = (actions: string[]) => userHasAnyPermission(actions, getCurrentUser()); diff --git a/public/app/app.ts b/public/app/app.ts index bc19500502b..89460a9fc03 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -38,6 +38,7 @@ import { setReturnToPreviousHook, setPluginExtensionsHook, setPluginComponentHook, + setCurrentUser, } from '@grafana/runtime'; import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; @@ -144,6 +145,7 @@ export class GrafanaApp { setEmbeddedDashboard(EmbeddedDashboardLazy); setTimeZoneResolver(() => config.bootData.user.timezone); initGrafanaLive(); + setCurrentUser(contextSrv.user); initAuthConfig(); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index ab1016fd71a..25f4ed13e1c 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -1,6 +1,14 @@ import { extend } from 'lodash'; -import { AnalyticsSettings, OrgRole, rangeUtil, WithAccessControlMetadata } from '@grafana/data'; +import { + AnalyticsSettings, + OrgRole, + rangeUtil, + WithAccessControlMetadata, + userHasPermission, + userHasPermissionInMetadata, + userHasAnyPermission, +} from '@grafana/data'; import { featureEnabled, getBackendSrv } from '@grafana/runtime'; import { getSessionExpiry } from 'app/core/utils/auth'; import { AccessControlAction, UserPermission } from 'app/types'; @@ -131,12 +139,12 @@ export class ContextSrv { // Checks whether user has required permission hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean { - return !!object.accessControl?.[action]; + return userHasPermissionInMetadata(action, object); } // Checks whether user has required permission hasPermission(action: AccessControlAction | string): boolean { - return !!this.user.permissions?.[action]; + return userHasPermission(action, this.user); } isGrafanaVisible() { @@ -171,7 +179,7 @@ export class ContextSrv { // evaluates access control permissions, granting access if the user has any of them evaluatePermission(actions: string[]) { - if (actions.some((action) => this.hasPermission(action))) { + if (userHasAnyPermission(actions, this.user)) { return []; } // Hack to reject when user does not have permission