Skip to content

Commit

Permalink
check permissions before loading license management actions (#39183) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
alisonelizabeth authored Jun 19, 2019
1 parent 0468537 commit cacd96c
Show file tree
Hide file tree
Showing 13 changed files with 271 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
export { PLUGIN } from './plugin';
export { BASE_PATH } from './base_path';
export { EXTERNAL_LINKS } from './external_links';
export { APP_PERMISSION } from './permissions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export const APP_PERMISSION = 'cluster:manage';
8 changes: 7 additions & 1 deletion x-pack/plugins/license_management/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

import { resolve } from 'path';
import { PLUGIN } from './common/constants';
import { registerLicenseRoute, registerStartTrialRoutes, registerStartBasicRoute } from './server/routes/api/license/';
import {
registerLicenseRoute,
registerStartTrialRoutes,
registerStartBasicRoute,
registerPermissionsRoute
} from './server/routes/api/license/';
import { createRouter } from '../../server/lib/create_router';

export function licenseManagement(kibana) {
Expand All @@ -27,6 +32,7 @@ export function licenseManagement(kibana) {
registerLicenseRoute(router, xpackInfo);
registerStartTrialRoutes(router, xpackInfo);
registerStartBasicRoute(router, xpackInfo);
registerPermissionsRoute(router, xpackInfo);
}
});
}
28 changes: 28 additions & 0 deletions x-pack/plugins/license_management/public/app.container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';

import { App as PresentationComponent } from './app';
import { getPermission, isPermissionsLoading, getPermissionsError } from './store/reducers/licenseManagement';
import { loadPermissions } from './store/actions/permissions';

const mapStateToProps = state => {
return {
hasPermission: getPermission(state),
permissionsLoading: isPermissionsLoading(state),
permissionsError: getPermissionsError(state),
};
};

const mapDispatchToProps = {
loadPermissions,
};

export const App = withRouter(connect(mapStateToProps, mapDispatchToProps)(
PresentationComponent
));
95 changes: 84 additions & 11 deletions x-pack/plugins/license_management/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,90 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import React, { Component } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { LicenseDashboard, UploadLicense } from './sections/';
import { Switch, Route } from 'react-router-dom';
import { BASE_PATH } from '../common/constants';
import { EuiPageBody } from '@elastic/eui';
import { BASE_PATH, APP_PERMISSION } from '../common/constants';
import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui';

export default () => (
<EuiPageBody>
<Switch>
<Route path={`${BASE_PATH}upload_license`} component={UploadLicense} />
<Route path={`${BASE_PATH}`} component={LicenseDashboard} />
</Switch>
</EuiPageBody>
);
export class App extends Component {
componentDidMount() {
const { loadPermissions } = this.props;
loadPermissions();
}

render() {
const { hasPermission, permissionsLoading, permissionsError } = this.props;

if (permissionsLoading) {
return (
<EuiEmptyPrompt
title={<EuiLoadingSpinner size="xl" />}
body={(
<EuiText color="subdued">
<FormattedMessage
id="xpack.licenseMgmt.app.loadingPermissionsDescription"
defaultMessage="Checking permissions…"
/>
</EuiText>
)}
data-test-subj="sectionLoading"
/>
);
}

if (permissionsError) {
return (
<EuiCallOut
title={<FormattedMessage
id="xpack.licenseMgmt.app.checkingPermissionsErrorMessage"
defaultMessage="Error checking permissions"
/>}
color="danger"
iconType="alert"
>
{permissionsError.data && permissionsError.data.message ? <div>{permissionsError.data.message}</div> : null}
</EuiCallOut>
);
}

if (!hasPermission) {
return (
<EuiPageBody>
<EuiEmptyPrompt
iconType="securityApp"
title={
<h2>
<FormattedMessage
id="xpack.licenseMgmt.app.deniedPermissionTitle"
defaultMessage="You're missing cluster privileges"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.licenseMgmt.app.deniedPermissionDescription"
defaultMessage="To use License Management, you must have {permissionType} privileges."
values={{
permissionType: <strong>{APP_PERMISSION}</strong>
}}
/>
</p>
}
/>
</EuiPageBody>
);
}

return (
<EuiPageBody>
<Switch>
<Route path={`${BASE_PATH}upload_license`} component={UploadLicense} />
<Route path={`${BASE_PATH}`} component={LicenseDashboard} />
</Switch>
</EuiPageBody>
);
}
}
12 changes: 12 additions & 0 deletions x-pack/plugins/license_management/public/lib/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,15 @@ export function canStartTrial() {
return $.ajax(options);
}

export function getPermissions() {
const options = {
url: chrome.addBasePath('/api/license/permissions'),
contentType: 'application/json',
cache: false,
crossDomain: true,
type: 'POST',
};

return $.ajax(options);
}

2 changes: 1 addition & 1 deletion x-pack/plugins/license_management/public/register_route.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { setTelemetryOptInService, setTelemetryEnabled, setHttpClient, Telemetry
import { I18nContext } from 'ui/i18n';
import chrome from 'ui/chrome';

import App from './app';
import { App } from './app.container';
import { BASE_PATH } from '../common/constants/base_path';

import routes from 'ui/routes';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { createAction } from 'redux-actions';
import { getPermissions } from '../../lib/es';

export const permissionsLoading = createAction(
'LICENSE_MANAGEMENT_PERMISSIONS_LOADING'
);

export const permissionsSuccess = createAction(
'LICENSE_MANAGEMENT_PERMISSIONS_SUCCESS'
);

export const permissionsError = createAction(
'LICENSE_MANAGEMENT_PERMISSIONS_ERROR'
);

export const loadPermissions = () => async dispatch => {
dispatch(permissionsLoading(true));
try {
const permissions = await getPermissions();
dispatch(permissionsLoading(false));
dispatch(permissionsSuccess(permissions.hasPermission));
} catch (e) {
dispatch(permissionsLoading(false));
dispatch(permissionsError(e));
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { uploadStatus } from './upload_status';
import { startBasicStatus } from './start_basic_license_status';
import { uploadErrorMessage } from './upload_error_message';
import { trialStatus } from './trial_status';
import { permissions } from './permissions';
import moment from 'moment-timezone';

export const WARNING_THRESHOLD_IN_DAYS = 25;
Expand All @@ -19,9 +20,23 @@ export const licenseManagement = combineReducers({
uploadStatus,
uploadErrorMessage,
trialStatus,
startBasicStatus
startBasicStatus,
permissions,
});

export const getPermission = state => {
return state.permissions.hasPermission;
};

export const isPermissionsLoading = state => {
return state.permissions.loading;
};


export const getPermissionsError = state => {
return state.permissions.error;
};

export const getLicense = state => {
return state.license;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { handleActions } from 'redux-actions';

import { permissionsSuccess, permissionsError, permissionsLoading } from '../actions/permissions';

export const permissions = handleActions({
[permissionsLoading](state, { payload }) {
return {
loading: payload,
};
},
[permissionsSuccess](state, { payload }) {
return {
hasPermission: payload,
};
},
[permissionsError](state, { payload }) {
return {
error: payload,
};
},
}, {});
42 changes: 42 additions & 0 deletions x-pack/plugins/license_management/server/lib/permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { wrapCustomError } from '../../../../server/lib/create_router/error_wrappers';

export async function getPermissions(req, xpackInfo) {
if (!xpackInfo) {
// xpackInfo is updated via poll, so it may not be available until polling has begun.
// In this rare situation, tell the client the service is temporarily unavailable.
throw wrapCustomError(new Error('Security info unavailable'), 503);
}

const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security');
if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
// If security isn't enabled, let the user use license management
return {
hasPermission: true,
};
}

const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin');
const options = {
method: 'POST',
path: '/_security/user/_has_privileges',
body: {
cluster: ['manage'], // License management requires "manage" cluster privileges
}
};

try {
const response = await callWithRequest(req, 'transport.request', options);
return {
hasPermission: response.cluster.manage,
};
} catch (error) {
return error.body;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
export { registerLicenseRoute } from './register_license_route';
export { registerStartBasicRoute } from './register_start_basic_route';
export { registerStartTrialRoutes } from './register_start_trial_routes';
export { registerPermissionsRoute } from './register_permissions_route';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { getPermissions } from '../../../lib/permissions';

export function registerPermissionsRoute(router, xpackInfo) {
router.post('/permissions', (request) => {
return getPermissions(request, xpackInfo);
});
}

0 comments on commit cacd96c

Please sign in to comment.