introduce featureControls.manage capability to control calls to features api

This commit is contained in:
Larry Gregory 2019-04-30 17:21:37 -04:00
parent 5653338801
commit addc149193
No known key found for this signature in database
GPG key ID: 3DC74EB09761BFEB
16 changed files with 265 additions and 11 deletions

View file

@ -18,6 +18,7 @@ import 'plugins/security/services/shield_indices';
import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns';
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
import { FeaturesService } from 'plugins/xpack_main/services';
import { SpacesManager } from '../../../../../spaces/public/lib';
import { checkLicenseError } from 'plugins/security/lib/check_license_error';
import { EDIT_ROLES_PATH, ROLES_PATH } from '../management_urls';
@ -87,10 +88,10 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
return [];
},
privileges() {
return kfetch({ method: 'get', pathname: '/api/security/privileges', query: { includeActions: true } });
return kfetch({ method: 'get', pathname: '/api/security/privileges', query: { includeActions: true } });
},
features() {
return kfetch({ method: 'get', pathname: '/api/features/v1' });
return FeaturesService.getFeatures();
}
},
controllerAs: 'editRole',

View file

@ -333,7 +333,12 @@ describe('features', () => {
all: [
actions.login,
actions.version,
...(expectManageFeatureControls ? [actions.api.get('manage_feature_controls')] : []),
...(expectManageFeatureControls
? [
actions.ui.get('featureControls', 'manage'),
actions.api.get('manage_feature_controls'),
]
: []),
...(expectManageSpaces ? [actions.space.manage, actions.ui.get('spaces', 'manage')] : []),
actions.app.get('app-1'),
actions.app.get('app-2'),
@ -418,7 +423,12 @@ describe('features', () => {
expect(actual).toHaveProperty(`${group}.all`, [
actions.login,
actions.version,
...(expectManageFeatureControls ? [actions.api.get('manage_feature_controls')] : []),
...(expectManageFeatureControls
? [
actions.ui.get('featureControls', 'manage'),
actions.api.get('manage_feature_controls'),
]
: []),
...(expectManageSpaces ? [actions.space.manage, actions.ui.get('spaces', 'manage')] : []),
actions.ui.get('catalogue', 'bar-catalogue-1'),
actions.ui.get('catalogue', 'bar-catalogue-2'),
@ -749,7 +759,12 @@ describe('features', () => {
expect(actual).toHaveProperty(`${group}.all`, [
actions.login,
actions.version,
...(expectManageFeatureControls ? [actions.api.get('manage_feature_controls')] : []),
...(expectManageFeatureControls
? [
actions.ui.get('featureControls', 'manage'),
actions.api.get('manage_feature_controls'),
]
: []),
...(expectManageSpaces ? [actions.space.manage, actions.ui.get('spaces', 'manage')] : []),
actions.allHack,
]);

View file

@ -62,6 +62,7 @@ export function privilegesFactory(actions: Actions, xpackMainPlugin: XPackMainPl
all: [
actions.login,
actions.version,
actions.ui.get('featureControls', 'manage'),
actions.api.get('manage_feature_controls'),
actions.space.manage,
actions.ui.get('spaces', 'manage'),

View file

@ -10,9 +10,9 @@ import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import 'ui/autoload/styles';
import { I18nContext } from 'ui/i18n';
import { kfetch } from 'ui/kfetch';
// @ts-ignore
import routes from 'ui/routes';
import { FeaturesService } from 'plugins/xpack_main/services';
import { SpacesManager } from '../../lib/spaces_manager';
import { ManageSpacePage } from './edit_space';
import { getCreateBreadcrumbs, getEditBreadcrumbs, getListBreadcrumbs } from './lib';
@ -34,7 +34,7 @@ routes.when('/management/spaces/list', {
const spacesManager = new SpacesManager($http, chrome, spaceSelectorURL);
const features = await kfetch({ method: 'get', pathname: '/api/features/v1' });
const features = await FeaturesService.getFeatures();
render(
<I18nContext>
@ -72,7 +72,7 @@ routes.when('/management/spaces/create', {
const spacesManager = new SpacesManager($http, chrome, spaceSelectorURL);
const features = await kfetch({ method: 'get', pathname: '/api/features/v1' });
const features = await FeaturesService.getFeatures();
render(
<I18nContext>
@ -117,7 +117,7 @@ routes.when('/management/spaces/edit/:spaceId', {
const spacesManager = new SpacesManager($http, chrome, spaceSelectorURL);
const features = await kfetch({ method: 'get', pathname: '/api/features/v1' });
const features = await FeaturesService.getFeatures();
render(
<I18nContext>

View file

@ -98,6 +98,11 @@ export const xpackMain = (kibana) => {
telemetryOptedIn: null,
activeSpace: null,
spacesEnabled: config.get('xpack.spaces.enabled'),
uiCapabilities: {
featureControls: {
manage: true,
},
},
};
},
hacks: [

View file

@ -0,0 +1,16 @@
/*
* 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 { kfetch } from 'ui/kfetch';
import { capabilities } from 'ui/capabilities';
export class FeaturesService {
public static async getFeatures() {
if (capabilities.get().featureControls.manage) {
return await kfetch({ method: 'get', pathname: '/api/features/v1' });
}
return [];
}
}

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 { FeaturesService } from './features';

View file

@ -224,7 +224,14 @@ describe('FeatureRegistry', () => {
);
});
['catalogue', 'management', 'navLinks', `doesn't match valid regex`].forEach(prohibitedId => {
[
'catalogue',
'management',
'navLinks',
'spaces',
'featureControls',
`doesn't match valid regex`,
].forEach(prohibitedId => {
it(`prevents features from being registered with an ID of "${prohibitedId}"`, () => {
const featureRegistry = new FeatureRegistry();
expect(() =>

View file

@ -52,7 +52,13 @@ export interface Feature<TPrivileges extends Partial<PrivilegesSet> = Privileges
// Each feature gets its own property on the UICapabilities object,
// but that object has a few built-in properties which should not be overwritten.
const prohibitedFeatureIds: Array<keyof UICapabilities> = ['catalogue', 'management', 'navLinks'];
const prohibitedFeatureIds: Array<keyof UICapabilities> = [
'catalogue',
'management',
'navLinks',
'spaces',
'featureControls',
];
const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/;

View file

@ -920,6 +920,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
all: [
'login:',
`version:${version}`,
`ui:${version}:featureControls/manage`,
`api:${version}:manage_feature_controls`,
`space:${version}:manage`,
`ui:${version}:spaces/manage`,

View file

@ -0,0 +1,90 @@
/*
* 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 expect from '@kbn/expect';
import { mapValues } from 'lodash';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UserAtSpaceScenarios } from '../scenarios';
// eslint-disable-next-line import/no-default-export
export default function featureControlsTests({ getService }: KibanaFunctionalTestDefaultProviders) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
describe('featureControls', () => {
UserAtSpaceScenarios.forEach(scenario => {
it(`${scenario.id}`, async () => {
const { user, space } = scenario;
const uiCapabilities = await uiCapabilitiesService.get(
{ username: user.username, password: user.password },
space.id
);
switch (scenario.id) {
case 'superuser at everything_space':
case 'global_all at everything_space':
case 'dual_privileges_all at everything_space':
case 'superuser at nothing_space':
case 'global_all at nothing_space':
case 'dual_privileges_all at nothing_space': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('featureControls');
// everything is enabled
const expected = mapValues(uiCapabilities.value!.featureControls, () => true);
expect(uiCapabilities.value!.featureControls).to.eql(expected);
break;
}
case 'everything_space_all at everything_space':
case 'global_read at everything_space':
case 'dual_privileges_read at everything_space':
case 'everything_space_read at everything_space': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('featureControls');
// everything is disabled
const expected = mapValues(uiCapabilities.value!.featureControls, () => false);
expect(uiCapabilities.value!.featureControls).to.eql(expected);
break;
}
// the nothing_space has no features enabled, so even if we have
// privileges to perform these actions, we won't be able to
case 'global_read at nothing_space':
case 'dual_privileges_read at nothing_space':
case 'nothing_space_all at nothing_space':
case 'nothing_space_read at nothing_space': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('featureControls');
// everything is disabled
const expected = mapValues(uiCapabilities.value!.featureControls, () => false);
expect(uiCapabilities.value!.featureControls).to.eql(expected);
break;
}
// if we don't have access at the space itself, we're
// redirected to the space selector and the ui capabilities
// are lagely irrelevant because they won't be consumed
case 'no_kibana_privileges at everything_space':
case 'no_kibana_privileges at nothing_space':
case 'legacy_all at everything_space':
case 'legacy_all at nothing_space':
case 'everything_space_all at nothing_space':
case 'everything_space_read at nothing_space':
case 'nothing_space_all at everything_space':
case 'nothing_space_read at everything_space':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(
GetUICapabilitiesFailureReason.RedirectedToRoot
);
break;
default:
throw new UnreachableError(scenario);
}
});
});
});
}

View file

@ -71,6 +71,7 @@ export default function uiCapabilitiesTests({
});
loadTestFile(require.resolve('./catalogue'));
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./foo'));
loadTestFile(require.resolve('./nav_links'));
loadTestFile(require.resolve('./saved_objects_management'));

View file

@ -0,0 +1,63 @@
/*
* 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 expect from '@kbn/expect';
import { mapValues } from 'lodash';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import {
GetUICapabilitiesFailureReason,
UICapabilitiesService,
} from '../../common/services/ui_capabilities';
import { UserScenarios } from '../scenarios';
// eslint-disable-next-line import/no-default-export
export default function featureControlsTests({ getService }: KibanaFunctionalTestDefaultProviders) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
describe('featureControls', () => {
UserScenarios.forEach(scenario => {
it(`${scenario.fullName}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get({
username: scenario.username,
password: scenario.password,
});
switch (scenario.username) {
case 'superuser':
case 'all':
case 'dual_privileges_all': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('featureControls');
// everything is enabled
const expected = mapValues(uiCapabilities.value!.featureControls, () => true);
expect(uiCapabilities.value!.featureControls).to.eql(expected);
break;
}
case 'read':
case 'dual_privileges_read':
case 'foo_all':
case 'foo_read': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('featureControls');
// everything is disabled
const expected = mapValues(uiCapabilities.value!.featureControls, () => false);
expect(uiCapabilities.value!.featureControls).to.eql(expected);
expect(uiCapabilities.value!.featureControls).to.eql(expected);
break;
}
// these users have no access to even get the ui capabilities
case 'legacy_all':
case 'no_kibana_privileges':
expect(uiCapabilities.success).to.be(false);
expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound);
break;
default:
throw new UnreachableError(scenario);
}
});
});
});
}

View file

@ -53,6 +53,7 @@ export default function uiCapabilitesTests({
});
loadTestFile(require.resolve('./catalogue'));
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./foo'));
loadTestFile(require.resolve('./nav_links'));
loadTestFile(require.resolve('./saved_objects_management'));

View file

@ -0,0 +1,39 @@
/*
* 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 expect from '@kbn/expect';
import { mapValues } from 'lodash';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import { UICapabilitiesService } from '../../common/services/ui_capabilities';
import { SpaceScenarios } from '../scenarios';
// eslint-disable-next-line import/no-default-export
export default function featureControlsTests({ getService }: KibanaFunctionalTestDefaultProviders) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
describe('featureControls', () => {
SpaceScenarios.forEach(scenario => {
it(`${scenario.name}`, async () => {
const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id);
switch (scenario.id) {
case 'everything_space':
case 'nothing_space':
case 'foo_disabled_space': {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('featureControls');
// everything is enabled
const expected = mapValues(uiCapabilities.value!.featureControls, () => true);
expect(uiCapabilities.value!.featureControls).to.eql(expected);
break;
}
default:
throw new UnreachableError(scenario);
}
});
});
});
}

View file

@ -39,6 +39,7 @@ export default function uiCapabilitesTests({
});
loadTestFile(require.resolve('./catalogue'));
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./foo'));
loadTestFile(require.resolve('./nav_links'));
loadTestFile(require.resolve('./saved_objects_management'));