From 7f1214ac462990ab29a48326da6efdf59100faa3 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Thu, 25 Jul 2019 16:54:26 +0200 Subject: [PATCH] Permissions: Show plugins in nav for non admin users but hide plugin configuration (#18234) Allow non admins to see plugins list but only with readme. Any config tabs are hidden from the plugin page. Also plugin panel does not show action buttons (like Enable) for non admins. --- pkg/api/index.go | 19 ++ public/app/features/plugins/PluginPage.tsx | 195 ++++++++++-------- .../app/plugins/panel/pluginlist/module.html | 18 +- public/app/plugins/panel/pluginlist/module.ts | 5 +- public/app/routes/ReactContainer.tsx | 1 + public/app/routes/routes.ts | 3 + public/dashboards/home.json | 25 +-- 7 files changed, 148 insertions(+), 118 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index d4103b0aa52..7a597ba90ae 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -307,6 +307,25 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er } } + data.NavTree = append(data.NavTree, cfgNode) + } else { + cfgNode := &dtos.NavLink{ + Id: "cfg", + Text: "Configuration", + SubTitle: "Organization: " + c.OrgName, + Icon: "gicon gicon-cog", + Url: setting.AppSubUrl + "/plugins", + Children: []*dtos.NavLink{ + { + Text: "Plugins", + Id: "plugins", + Description: "View and configure plugins", + Icon: "gicon gicon-plugins", + Url: setting.AppSubUrl + "/plugins", + }, + }, + } + data.NavTree = append(data.NavTree, cfgNode) } diff --git a/public/app/features/plugins/PluginPage.tsx b/public/app/features/plugins/PluginPage.tsx index e7a751d7882..da8e654e998 100644 --- a/public/app/features/plugins/PluginPage.tsx +++ b/public/app/features/plugins/PluginPage.tsx @@ -29,6 +29,7 @@ import { AppConfigCtrlWrapper } from './wrappers/AppConfigWrapper'; import { PluginDashboards } from './PluginDashboards'; import { appEvents } from 'app/core/core'; import { config } from 'app/core/config'; +import { ContextSrv } from '../../core/services/context_srv'; export function getLoadingNav(): NavModel { const node = { @@ -69,6 +70,7 @@ interface Props { pluginId: string; query: UrlQueryMap; path: string; // the URL path + $contextSrv: ContextSrv; } interface State { @@ -93,7 +95,7 @@ class PluginPage extends PureComponent { } async componentDidMount() { - const { pluginId, path, query } = this.props; + const { pluginId, path, query, $contextSrv } = this.props; const { appSubUrl } = config; const plugin = await loadPlugin(pluginId); @@ -105,97 +107,16 @@ class PluginPage extends PureComponent { return; // 404 } - const { meta } = plugin; - let defaultPage: string; - const pages: NavModelItem[] = []; - - if (true) { - pages.push({ - text: 'Readme', - icon: 'fa fa-fw fa-file-text-o', - url: `${appSubUrl}${path}?page=${PAGE_ID_README}`, - id: PAGE_ID_README, - }); - } - - // Only show Config/Pages for app - if (meta.type === PluginType.app) { - // Legacy App Config - if (plugin.angularConfigCtrl) { - pages.push({ - text: 'Config', - icon: 'gicon gicon-cog', - url: `${appSubUrl}${path}?page=${PAGE_ID_CONFIG_CTRL}`, - id: PAGE_ID_CONFIG_CTRL, - }); - defaultPage = PAGE_ID_CONFIG_CTRL; - } - - if (plugin.configPages) { - for (const page of plugin.configPages) { - pages.push({ - text: page.title, - icon: page.icon, - url: path + '?page=' + page.id, - id: page.id, - }); - if (!defaultPage) { - defaultPage = page.id; - } - } - } - - // Check for the dashboard pages - if (find(meta.includes, { type: 'dashboard' })) { - pages.push({ - text: 'Dashboards', - icon: 'gicon gicon-dashboard', - url: `${appSubUrl}${path}?page=${PAGE_ID_DASHBOARDS}`, - id: PAGE_ID_DASHBOARDS, - }); - } - } - - if (!defaultPage) { - defaultPage = pages[0].id; // the first tab - } - - const node = { - text: meta.name, - img: meta.info.logos.large, - subTitle: meta.info.author.name, - breadcrumbs: [{ title: 'Plugins', url: '/plugins' }], - url: `${appSubUrl}${path}`, - children: this.setActivePage(query.page as string, pages, defaultPage), - }; + const { defaultPage, nav } = getPluginTabsNav(plugin, appSubUrl, path, query, $contextSrv.hasRole('Admin')); this.setState({ loading: false, plugin, defaultPage, - nav: { - node: node, - main: node, - }, + nav, }); } - setActivePage(pageId: string, pages: NavModelItem[], defaultPageId: string): NavModelItem[] { - let found = false; - const selected = pageId || defaultPageId; - const changed = pages.map(p => { - const active = !found && selected === p.id; - if (active) { - found = true; - } - return { ...p, active }; - }); - if (!found) { - changed[0].active = true; - } - return changed; - } - componentDidUpdate(prevProps: Props) { const prevPage = prevProps.query.page as string; const page = this.props.query.page as string; @@ -203,7 +124,7 @@ class PluginPage extends PureComponent { const { nav, defaultPage } = this.state; const node = { ...nav.node, - children: this.setActivePage(page, nav.node.children, defaultPage), + children: setActivePage(page, nav.node.children, defaultPage), }; this.setState({ nav: { @@ -369,6 +290,8 @@ class PluginPage extends PureComponent { render() { const { loading, nav, plugin } = this.state; + const { $contextSrv } = this.props; + const isAdmin = $contextSrv.hasRole('Admin'); return ( @@ -379,7 +302,7 @@ class PluginPage extends PureComponent { {plugin && (
{this.renderVersionInfo(plugin.meta)} - {this.renderSidebarIncludes(plugin.meta.includes)} + {isAdmin && this.renderSidebarIncludes(plugin.meta.includes)} {this.renderSidebarDependencies(plugin.meta.dependencies)} {this.renderSidebarLinks(plugin.meta.info)}
@@ -393,6 +316,106 @@ class PluginPage extends PureComponent { } } +function getPluginTabsNav( + plugin: GrafanaPlugin, + appSubUrl: string, + path: string, + query: UrlQueryMap, + isAdmin: boolean +): { defaultPage: string; nav: NavModel } { + const { meta } = plugin; + let defaultPage: string; + const pages: NavModelItem[] = []; + + if (true) { + pages.push({ + text: 'Readme', + icon: 'fa fa-fw fa-file-text-o', + url: `${appSubUrl}${path}?page=${PAGE_ID_README}`, + id: PAGE_ID_README, + }); + } + + // We allow non admins to see plugins but only their readme. Config is hidden even though the API needs to be + // public for plugins to work properly. + if (isAdmin) { + // Only show Config/Pages for app + if (meta.type === PluginType.app) { + // Legacy App Config + if (plugin.angularConfigCtrl) { + pages.push({ + text: 'Config', + icon: 'gicon gicon-cog', + url: `${appSubUrl}${path}?page=${PAGE_ID_CONFIG_CTRL}`, + id: PAGE_ID_CONFIG_CTRL, + }); + defaultPage = PAGE_ID_CONFIG_CTRL; + } + + if (plugin.configPages) { + for (const page of plugin.configPages) { + pages.push({ + text: page.title, + icon: page.icon, + url: path + '?page=' + page.id, + id: page.id, + }); + if (!defaultPage) { + defaultPage = page.id; + } + } + } + + // Check for the dashboard pages + if (find(meta.includes, { type: 'dashboard' })) { + pages.push({ + text: 'Dashboards', + icon: 'gicon gicon-dashboard', + url: `${appSubUrl}${path}?page=${PAGE_ID_DASHBOARDS}`, + id: PAGE_ID_DASHBOARDS, + }); + } + } + } + + if (!defaultPage) { + defaultPage = pages[0].id; // the first tab + } + + const node = { + text: meta.name, + img: meta.info.logos.large, + subTitle: meta.info.author.name, + breadcrumbs: [{ title: 'Plugins', url: '/plugins' }], + url: `${appSubUrl}${path}`, + children: setActivePage(query.page as string, pages, defaultPage), + }; + + return { + defaultPage, + nav: { + node: node, + main: node, + }, + }; +} + +function setActivePage(pageId: string, pages: NavModelItem[], defaultPageId: string): NavModelItem[] { + let found = false; + const selected = pageId || defaultPageId; + const changed = pages.map(p => { + const active = !found && selected === p.id; + if (active) { + found = true; + } + return { ...p, active }; + }); + if (!found) { + changed[0].active = true; + } + return changed; +} + function getPluginIcon(type: string) { switch (type) { case 'datasource': diff --git a/public/app/plugins/panel/pluginlist/module.html b/public/app/plugins/panel/pluginlist/module.html index d182699f4bd..1ab69bbc62a 100644 --- a/public/app/plugins/panel/pluginlist/module.html +++ b/public/app/plugins/panel/pluginlist/module.html @@ -10,14 +10,16 @@ {{plugin.name}} v{{plugin.info.version}} - - Update available! - - - Enable now - - - Up to date + + + Update available! + + + Enable now + + + Up to date + diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index 3aecced2ef1..c6ca83f248a 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import { PanelCtrl } from '../../../features/panel/panel_ctrl'; import { auto } from 'angular'; import { BackendSrv } from '@grafana/runtime'; +import { ContextSrv } from '../../../core/services/context_srv'; class PluginListCtrl extends PanelCtrl { static templateUrl = 'module.html'; @@ -9,16 +10,18 @@ class PluginListCtrl extends PanelCtrl { pluginList: any[]; viewModel: any; + isAdmin: boolean; // Set and populate defaults panelDefaults = {}; /** @ngInject */ - constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv) { + constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv, contextSrv: ContextSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); + this.isAdmin = contextSrv.hasRole('Admin'); this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.pluginList = []; this.viewModel = [ diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index 75b5831bf92..117df53b8b1 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -41,6 +41,7 @@ export function reactContainer($route: any, $location: any, $injector: any, $roo $injector: $injector, $rootScope: $rootScope, $scope: scope, + $contextSrv: contextSrv, routeInfo: $route.current.$$route.routeInfo, }; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 26746f82168..a854b7e1ce6 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -35,6 +35,9 @@ import { DashboardRouteInfo } from 'app/types'; export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) { $locationProvider.html5Mode(true); + // Routes here are guarded both here and server side for react-container routes or just on the server for angular + // ones. That means angular ones could be navigated to in case there is a client side link some where. + $routeProvider .when('/', { template: '', diff --git a/public/dashboards/home.json b/public/dashboards/home.json index f2c441053bb..88709a767ef 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -77,29 +77,8 @@ }, "timepicker": { "hidden": true, - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], "type": "timepicker" }, "timezone": "browser",