check permissions before loading license management actions (#39183) (#39254)

This commit is contained in:
Alison Goryachev 2019-06-19 11:01:19 -04:00 committed by GitHub
parent 0468537fd0
commit cacd96c01d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 271 additions and 14 deletions

View file

@ -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';

View file

@ -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';

View file

@ -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) {
@ -27,6 +32,7 @@ export function licenseManagement(kibana) {
registerLicenseRoute(router, xpackInfo);
registerStartTrialRoutes(router, xpackInfo);
registerStartBasicRoute(router, xpackInfo);
registerPermissionsRoute(router, xpackInfo);
}
});
}

View file

@ -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
));

View file

@ -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>
);
}
}

View file

@ -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);
}

View file

@ -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';

View file

@ -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));
}
};

View file

@ -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;
@ -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;
};

View file

@ -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,
};
},
}, {});

View file

@ -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;
}
}

View file

@ -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';

View file

@ -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);
});
}