Commit 7f1214ac by Andrej Ocenas Committed by GitHub

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.
parent 3ba2388a
...@@ -308,6 +308,25 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er ...@@ -308,6 +308,25 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
} }
data.NavTree = append(data.NavTree, cfgNode) 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)
} }
if c.IsGrafanaAdmin { if c.IsGrafanaAdmin {
......
...@@ -29,6 +29,7 @@ import { AppConfigCtrlWrapper } from './wrappers/AppConfigWrapper'; ...@@ -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 { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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':
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
<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 ng-if="ctrl.isAdmin">
<span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion"> <span class="pluginlist-message pluginlist-message--update" ng-show="plugin.hasUpdate" ng-click="ctrl.updateAvailable(plugin, $event)" bs-tooltip="'New version: ' + plugin.latestVersion">
Update available! Update available!
</span> </span>
...@@ -19,6 +20,7 @@ ...@@ -19,6 +20,7 @@
<span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate"> <span class="pluginlist-message pluginlist-message--no-update" ng-show="plugin.enabled && !plugin.hasUpdate">
Up to date Up to date
</span> </span>
</span>
</a> </a>
</div> </div>
<div class="pluginlist-item" ng-show="category.list.length === 0"> <div class="pluginlist-item" ng-show="category.list.length === 0">
......
...@@ -2,6 +2,7 @@ import _ from 'lodash'; ...@@ -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 { ...@@ -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 = [
......
...@@ -41,6 +41,7 @@ export function reactContainer($route: any, $location: any, $injector: any, $roo ...@@ -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,
}; };
......
...@@ -35,6 +35,9 @@ import { DashboardRouteInfo } from 'app/types'; ...@@ -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 />',
......
...@@ -77,29 +77,8 @@ ...@@ -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",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment