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.
This commit is contained in:
Andrej Ocenas 2019-07-25 16:54:26 +02:00 committed by GitHub
parent 3ba2388af7
commit 7f1214ac46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 118 deletions

View File

@ -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) data.NavTree = append(data.NavTree, cfgNode)
} }

View File

@ -29,6 +29,7 @@ import { AppConfigCtrlWrapper } from './wrappers/AppConfigWrapper';
import { PluginDashboards } from './PluginDashboards'; import { PluginDashboards } from './PluginDashboards';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { ContextSrv } from '../../core/services/context_srv';
export function getLoadingNav(): NavModel { export function getLoadingNav(): NavModel {
const node = { const node = {
@ -69,6 +70,7 @@ interface Props {
pluginId: string; pluginId: string;
query: UrlQueryMap; query: UrlQueryMap;
path: string; // the URL path path: string; // the URL path
$contextSrv: ContextSrv;
} }
interface State { interface State {
@ -93,7 +95,7 @@ class PluginPage extends PureComponent<Props, State> {
} }
async componentDidMount() { async componentDidMount() {
const { pluginId, path, query } = this.props; const { pluginId, path, query, $contextSrv } = this.props;
const { appSubUrl } = config; const { appSubUrl } = config;
const plugin = await loadPlugin(pluginId); const plugin = await loadPlugin(pluginId);
@ -105,97 +107,16 @@ class PluginPage extends PureComponent<Props, State> {
return; // 404 return; // 404
} }
const { meta } = plugin; const { defaultPage, nav } = getPluginTabsNav(plugin, appSubUrl, path, query, $contextSrv.hasRole('Admin'));
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),
};
this.setState({ this.setState({
loading: false, loading: false,
plugin, plugin,
defaultPage, defaultPage,
nav: { nav,
node: node,
main: node,
},
}); });
} }
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) { componentDidUpdate(prevProps: Props) {
const prevPage = prevProps.query.page as string; const prevPage = prevProps.query.page as string;
const page = this.props.query.page as string; const page = this.props.query.page as string;
@ -203,7 +124,7 @@ class PluginPage extends PureComponent<Props, State> {
const { nav, defaultPage } = this.state; const { nav, defaultPage } = this.state;
const node = { const node = {
...nav.node, ...nav.node,
children: this.setActivePage(page, nav.node.children, defaultPage), children: setActivePage(page, nav.node.children, defaultPage),
}; };
this.setState({ this.setState({
nav: { nav: {
@ -369,6 +290,8 @@ class PluginPage extends PureComponent<Props, State> {
render() { render() {
const { loading, nav, plugin } = this.state; const { loading, nav, plugin } = this.state;
const { $contextSrv } = this.props;
const isAdmin = $contextSrv.hasRole('Admin');
return ( return (
<Page navModel={nav}> <Page navModel={nav}>
<Page.Contents isLoading={loading}> <Page.Contents isLoading={loading}>
@ -379,7 +302,7 @@ class PluginPage extends PureComponent<Props, State> {
{plugin && ( {plugin && (
<section className="page-sidebar-section"> <section className="page-sidebar-section">
{this.renderVersionInfo(plugin.meta)} {this.renderVersionInfo(plugin.meta)}
{this.renderSidebarIncludes(plugin.meta.includes)} {isAdmin && this.renderSidebarIncludes(plugin.meta.includes)}
{this.renderSidebarDependencies(plugin.meta.dependencies)} {this.renderSidebarDependencies(plugin.meta.dependencies)}
{this.renderSidebarLinks(plugin.meta.info)} {this.renderSidebarLinks(plugin.meta.info)}
</section> </section>
@ -393,6 +316,106 @@ class PluginPage extends PureComponent<Props, State> {
} }
} }
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) { function getPluginIcon(type: string) {
switch (type) { switch (type) {
case 'datasource': case 'datasource':

View File

@ -10,14 +10,16 @@
<span class="pluginlist-title">{{plugin.name}}</span> <span class="pluginlist-title">{{plugin.name}}</span>
<span class="pluginlist-version">v{{plugin.info.version}}</span> <span class="pluginlist-version">v{{plugin.info.version}}</span>
</span> </span>
<span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion"> <span ng-if="ctrl.isAdmin">
Update available! <span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion">
</span> Update available!
<span class="pluginlist-message pluginlist-message--enable" ng-show="!plugin.enabled && !plugin.hasUpdate"> </span>
Enable now <span class="pluginlist-message pluginlist-message--enable" ng-show="!plugin.enabled && !plugin.hasUpdate">
</span> Enable now
<span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate"> </span>
Up to date <span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate">
Up to date
</span>
</span> </span>
</a> </a>
</div> </div>

View File

@ -2,6 +2,7 @@ import _ from 'lodash';
import { PanelCtrl } from '../../../features/panel/panel_ctrl'; import { PanelCtrl } from '../../../features/panel/panel_ctrl';
import { auto } from 'angular'; import { auto } from 'angular';
import { BackendSrv } from '@grafana/runtime'; import { BackendSrv } from '@grafana/runtime';
import { ContextSrv } from '../../../core/services/context_srv';
class PluginListCtrl extends PanelCtrl { class PluginListCtrl extends PanelCtrl {
static templateUrl = 'module.html'; static templateUrl = 'module.html';
@ -9,16 +10,18 @@ class PluginListCtrl extends PanelCtrl {
pluginList: any[]; pluginList: any[];
viewModel: any; viewModel: any;
isAdmin: boolean;
// Set and populate defaults // Set and populate defaults
panelDefaults = {}; panelDefaults = {};
/** @ngInject */ /** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv) { constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv, contextSrv: ContextSrv) {
super($scope, $injector); super($scope, $injector);
_.defaults(this.panel, this.panelDefaults); _.defaults(this.panel, this.panelDefaults);
this.isAdmin = contextSrv.hasRole('Admin');
this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
this.pluginList = []; this.pluginList = [];
this.viewModel = [ this.viewModel = [

View File

@ -41,6 +41,7 @@ export function reactContainer($route: any, $location: any, $injector: any, $roo
$injector: $injector, $injector: $injector,
$rootScope: $rootScope, $rootScope: $rootScope,
$scope: scope, $scope: scope,
$contextSrv: contextSrv,
routeInfo: $route.current.$$route.routeInfo, routeInfo: $route.current.$$route.routeInfo,
}; };

View File

@ -35,6 +35,9 @@ import { DashboardRouteInfo } from 'app/types';
export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) { export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) {
$locationProvider.html5Mode(true); $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 $routeProvider
.when('/', { .when('/', {
template: '<react-container />', template: '<react-container />',

View File

@ -77,29 +77,8 @@
}, },
"timepicker": { "timepicker": {
"hidden": true, "hidden": true,
"refresh_intervals": [ "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"5s", "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"],
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"type": "timepicker" "type": "timepicker"
}, },
"timezone": "browser", "timezone": "browser",