mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
introduce featureControls.manage capability to control calls to features api
This commit is contained in:
parent
5653338801
commit
addc149193
16 changed files with 265 additions and 11 deletions
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -98,6 +98,11 @@ export const xpackMain = (kibana) => {
|
|||
telemetryOptedIn: null,
|
||||
activeSpace: null,
|
||||
spacesEnabled: config.get('xpack.spaces.enabled'),
|
||||
uiCapabilities: {
|
||||
featureControls: {
|
||||
manage: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
hacks: [
|
||||
|
|
16
x-pack/plugins/xpack_main/public/services/features.ts
Normal file
16
x-pack/plugins/xpack_main/public/services/features.ts
Normal 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 [];
|
||||
}
|
||||
}
|
7
x-pack/plugins/xpack_main/public/services/index.ts
Normal file
7
x-pack/plugins/xpack_main/public/services/index.ts
Normal 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';
|
|
@ -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(() =>
|
||||
|
|
|
@ -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_-]+$/;
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue