mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Support for sub-feature privileges (#60563)
* initial server-side support for sub-feature privileges (#57507) * initial server-side support for sub-feature privileges * start addressing PR feedback * renaming interfaces * move privilege id collision check to security plugin * additional testing * change featurePrivilegeIterator import location * fix link assertions following rebase from master * Initial UI support for sub-feature privileges (#59198) * Initial UI support for sub-feature privileges * Address PR feedback * display deleted spaces correctly in the privilege summary * additional testing * update snapshot * Enables sub-feature privileges for gold+ licenses (#59750) * enables sub-feature privileges for gold+ licenses * Address PR feedback * address platform review feedback
This commit is contained in:
parent
f371acff33
commit
b82cc6ed4a
180 changed files with 12486 additions and 7108 deletions
|
@ -890,7 +890,8 @@ export class DashboardAppController {
|
|||
share.toggleShareContextMenu({
|
||||
anchorElement,
|
||||
allowEmbed: true,
|
||||
allowShortUrl: !dashboardConfig.getHideWriteControls(),
|
||||
allowShortUrl:
|
||||
!dashboardConfig.getHideWriteControls() || dashboardCapabilities.createShortUrl,
|
||||
shareableUrl: unhashUrl(window.location.href),
|
||||
objectId: dash.id,
|
||||
objectType: 'dashboard',
|
||||
|
|
|
@ -96,6 +96,7 @@ export const apm: LegacyPluginInitializer = kibana => {
|
|||
name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', {
|
||||
defaultMessage: 'APM'
|
||||
}),
|
||||
order: 900,
|
||||
icon: 'apmApp',
|
||||
navLinkId: 'apm',
|
||||
app: ['apm', 'kibana'],
|
||||
|
@ -103,6 +104,7 @@ export const apm: LegacyPluginInitializer = kibana => {
|
|||
// see x-pack/plugins/features/common/feature_kibana_privileges.ts
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['apm', 'kibana'],
|
||||
api: ['apm', 'apm_write', 'actions-read', 'alerting-read'],
|
||||
catalogue: ['apm'],
|
||||
savedObject: {
|
||||
|
@ -121,6 +123,7 @@ export const apm: LegacyPluginInitializer = kibana => {
|
|||
]
|
||||
},
|
||||
read: {
|
||||
app: ['apm', 'kibana'],
|
||||
api: ['apm', 'actions-read', 'alerting-read'],
|
||||
catalogue: ['apm'],
|
||||
savedObject: {
|
||||
|
|
|
@ -37,6 +37,7 @@ export const graph: LegacyPluginInitializer = kibana => {
|
|||
name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', {
|
||||
defaultMessage: 'Graph',
|
||||
}),
|
||||
order: 1200,
|
||||
icon: 'graphApp',
|
||||
navLinkId: 'graph',
|
||||
app: ['graph', 'kibana'],
|
||||
|
@ -44,6 +45,8 @@ export const graph: LegacyPluginInitializer = kibana => {
|
|||
validLicenses: ['platinum', 'enterprise', 'trial'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['graph', 'kibana'],
|
||||
catalogue: ['graph'],
|
||||
savedObject: {
|
||||
all: ['graph-workspace'],
|
||||
read: ['index-pattern'],
|
||||
|
@ -51,6 +54,8 @@ export const graph: LegacyPluginInitializer = kibana => {
|
|||
ui: ['save', 'delete'],
|
||||
},
|
||||
read: {
|
||||
app: ['graph', 'kibana'],
|
||||
catalogue: ['graph'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern', 'graph-workspace'],
|
||||
|
|
|
@ -23,12 +23,15 @@ export class MapPlugin {
|
|||
name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', {
|
||||
defaultMessage: 'Maps',
|
||||
}),
|
||||
order: 600,
|
||||
icon: APP_ICON,
|
||||
navLinkId: APP_ID,
|
||||
app: [APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
app: [APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
savedObject: {
|
||||
all: [MAP_SAVED_OBJECT_TYPE, 'query'],
|
||||
read: ['index-pattern'],
|
||||
|
@ -36,6 +39,8 @@ export class MapPlugin {
|
|||
ui: ['save', 'show', 'saveQuery'],
|
||||
},
|
||||
read: {
|
||||
app: [APP_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query'],
|
||||
|
|
|
@ -97,12 +97,15 @@ export class Plugin {
|
|||
name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', {
|
||||
defaultMessage: 'SIEM',
|
||||
}),
|
||||
order: 1100,
|
||||
icon: 'securityAnalyticsApp',
|
||||
navLinkId: 'siem',
|
||||
app: ['siem', 'kibana'],
|
||||
catalogue: ['siem'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['siem', 'kibana'],
|
||||
catalogue: ['siem'],
|
||||
api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'],
|
||||
savedObject: {
|
||||
all: [
|
||||
|
@ -128,6 +131,8 @@ export class Plugin {
|
|||
],
|
||||
},
|
||||
read: {
|
||||
app: ['siem', 'kibana'],
|
||||
catalogue: ['siem'],
|
||||
api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'],
|
||||
savedObject: {
|
||||
all: ['alert', 'action', 'action_task_params'],
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
*/
|
||||
|
||||
import KbnServer from 'src/legacy/server/kbn_server';
|
||||
import { Feature, FeatureWithAllOrReadPrivileges } from '../../../../plugins/features/server';
|
||||
import { Feature, FeatureConfig } from '../../../../plugins/features/server';
|
||||
import { XPackInfo, XPackInfoOptions } from './lib/xpack_info';
|
||||
export { XPackFeature } from './lib/xpack_info';
|
||||
|
||||
export interface XPackMainPlugin {
|
||||
info: XPackInfo;
|
||||
getFeatures(): Feature[];
|
||||
registerFeature(feature: FeatureWithAllOrReadPrivileges): void;
|
||||
registerFeature(feature: FeatureConfig): void;
|
||||
}
|
||||
|
|
|
@ -32,12 +32,15 @@ export class CanvasPlugin implements Plugin {
|
|||
plugins.features.registerFeature({
|
||||
id: 'canvas',
|
||||
name: 'Canvas',
|
||||
order: 400,
|
||||
icon: 'canvasApp',
|
||||
navLinkId: 'canvas',
|
||||
app: ['canvas', 'kibana'],
|
||||
catalogue: ['canvas'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['canvas', 'kibana'],
|
||||
catalogue: ['canvas'],
|
||||
savedObject: {
|
||||
all: ['canvas-workpad', 'canvas-element'],
|
||||
read: ['index-pattern'],
|
||||
|
@ -45,6 +48,8 @@ export class CanvasPlugin implements Plugin {
|
|||
ui: ['save', 'show'],
|
||||
},
|
||||
read: {
|
||||
app: ['canvas', 'kibana'],
|
||||
catalogue: ['canvas'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern', 'canvas-workpad', 'canvas-element'],
|
||||
|
|
|
@ -43,6 +43,7 @@ export class EndpointPlugin
|
|||
app: ['endpoint', 'kibana'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['endpoint', 'kibana'],
|
||||
api: ['resolver'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
@ -51,6 +52,7 @@ export class EndpointPlugin
|
|||
ui: ['save'],
|
||||
},
|
||||
read: {
|
||||
app: ['endpoint', 'kibana'],
|
||||
api: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FeatureKibanaPrivileges, FeatureKibanaPrivilegesSet } from './feature_kibana_privileges';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { FeatureKibanaPrivileges } from './feature_kibana_privileges';
|
||||
import { SubFeatureConfig, SubFeature } from './sub_feature';
|
||||
|
||||
/**
|
||||
* Interface for registering a feature.
|
||||
* Feature registration allows plugins to hide their applications with spaces,
|
||||
* and secure access when configured for security.
|
||||
*/
|
||||
export interface Feature<
|
||||
TPrivileges extends Partial<FeatureKibanaPrivilegesSet> = FeatureKibanaPrivilegesSet
|
||||
> {
|
||||
export interface FeatureConfig {
|
||||
/**
|
||||
* Unique identifier for this feature.
|
||||
* This identifier is also used when generating UI Capabilities.
|
||||
|
@ -28,6 +28,11 @@ export interface Feature<
|
|||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* An ordinal used to sort features relative to one another for display.
|
||||
*/
|
||||
order?: number;
|
||||
|
||||
/**
|
||||
* Whether or not this feature should be excluded from the base privileges.
|
||||
* This is primarily helpful when migrating applications with a "legacy" privileges model
|
||||
|
@ -98,7 +103,15 @@ export interface Feature<
|
|||
* ```
|
||||
* @see FeatureKibanaPrivileges
|
||||
*/
|
||||
privileges: TPrivileges;
|
||||
privileges: {
|
||||
all: FeatureKibanaPrivileges;
|
||||
read: FeatureKibanaPrivileges;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* Optional sub-feature privilege definitions. This can only be specified if `privileges` are are also defined.
|
||||
*/
|
||||
subFeatures?: SubFeatureConfig[];
|
||||
|
||||
/**
|
||||
* Optional message to display on the Role Management screen when configuring permissions for this feature.
|
||||
|
@ -114,7 +127,64 @@ export interface Feature<
|
|||
};
|
||||
}
|
||||
|
||||
export type FeatureWithAllOrReadPrivileges = Feature<{
|
||||
all?: FeatureKibanaPrivileges;
|
||||
read?: FeatureKibanaPrivileges;
|
||||
}>;
|
||||
export class Feature {
|
||||
public readonly subFeatures: SubFeature[];
|
||||
|
||||
constructor(protected readonly config: RecursiveReadonly<FeatureConfig>) {
|
||||
this.subFeatures = (config.subFeatures ?? []).map(
|
||||
subFeatureConfig => new SubFeature(subFeatureConfig)
|
||||
);
|
||||
}
|
||||
|
||||
public get id() {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
public get name() {
|
||||
return this.config.name;
|
||||
}
|
||||
|
||||
public get order() {
|
||||
return this.config.order;
|
||||
}
|
||||
|
||||
public get navLinkId() {
|
||||
return this.config.navLinkId;
|
||||
}
|
||||
|
||||
public get app() {
|
||||
return this.config.app;
|
||||
}
|
||||
|
||||
public get catalogue() {
|
||||
return this.config.catalogue;
|
||||
}
|
||||
|
||||
public get management() {
|
||||
return this.config.management;
|
||||
}
|
||||
|
||||
public get icon() {
|
||||
return this.config.icon;
|
||||
}
|
||||
|
||||
public get validLicenses() {
|
||||
return this.config.validLicenses;
|
||||
}
|
||||
|
||||
public get privileges() {
|
||||
return this.config.privileges;
|
||||
}
|
||||
|
||||
public get excludeFromBasePrivileges() {
|
||||
return this.config.excludeFromBasePrivileges ?? false;
|
||||
}
|
||||
|
||||
public get reserved() {
|
||||
return this.config.reserved;
|
||||
}
|
||||
|
||||
public toRaw() {
|
||||
return { ...this.config } as FeatureConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,5 +123,3 @@ export interface FeatureKibanaPrivileges {
|
|||
*/
|
||||
ui: string[];
|
||||
}
|
||||
|
||||
export type FeatureKibanaPrivilegesSet = Record<string, FeatureKibanaPrivileges>;
|
||||
|
|
|
@ -5,4 +5,11 @@
|
|||
*/
|
||||
|
||||
export { FeatureKibanaPrivileges } from './feature_kibana_privileges';
|
||||
export * from './feature';
|
||||
export { Feature, FeatureConfig } from './feature';
|
||||
export {
|
||||
SubFeature,
|
||||
SubFeatureConfig,
|
||||
SubFeaturePrivilegeConfig,
|
||||
SubFeaturePrivilegeGroupConfig,
|
||||
SubFeaturePrivilegeGroupType,
|
||||
} from './sub_feature';
|
||||
|
|
87
x-pack/plugins/features/common/sub_feature.ts
Normal file
87
x-pack/plugins/features/common/sub_feature.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { FeatureKibanaPrivileges } from './feature_kibana_privileges';
|
||||
|
||||
/**
|
||||
* Configuration for a sub-feature.
|
||||
*/
|
||||
export interface SubFeatureConfig {
|
||||
/** Display name for this sub-feature */
|
||||
name: string;
|
||||
|
||||
/** Collection of privilege groups */
|
||||
privilegeGroups: SubFeaturePrivilegeGroupConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of privilege group.
|
||||
* - `mutually_exclusive`::
|
||||
* Users will be able to select at most one privilege within this group.
|
||||
* Privileges must be specified in descending order of permissiveness (e.g. `All`, `Read`, not `Read`, `All)
|
||||
* - `independent`::
|
||||
* Users will be able to select any combination of privileges within this group.
|
||||
*/
|
||||
export type SubFeaturePrivilegeGroupType = 'mutually_exclusive' | 'independent';
|
||||
|
||||
/**
|
||||
* Configuration for a sub-feature privilege group.
|
||||
*/
|
||||
export interface SubFeaturePrivilegeGroupConfig {
|
||||
/**
|
||||
* The type of privilege group.
|
||||
* - `mutually_exclusive`::
|
||||
* Users will be able to select at most one privilege within this group.
|
||||
* Privileges must be specified in descending order of permissiveness (e.g. `All`, `Read`, not `Read`, `All)
|
||||
* - `independent`::
|
||||
* Users will be able to select any combination of privileges within this group.
|
||||
*/
|
||||
groupType: SubFeaturePrivilegeGroupType;
|
||||
|
||||
/**
|
||||
* The privileges which belong to this group.
|
||||
*/
|
||||
privileges: SubFeaturePrivilegeConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a sub-feature privilege.
|
||||
*/
|
||||
export interface SubFeaturePrivilegeConfig
|
||||
extends Omit<FeatureKibanaPrivileges, 'excludeFromBasePrivileges'> {
|
||||
/**
|
||||
* Identifier for this privilege. Must be unique across all other privileges within a feature.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The display name for this privilege.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Denotes which Primary Feature Privilege this sub-feature privilege should be included in.
|
||||
* `read` is also included in `all` automatically.
|
||||
*/
|
||||
includeIn: 'all' | 'read' | 'none';
|
||||
}
|
||||
|
||||
export class SubFeature {
|
||||
constructor(protected readonly config: RecursiveReadonly<SubFeatureConfig>) {}
|
||||
|
||||
public get name() {
|
||||
return this.config.name;
|
||||
}
|
||||
|
||||
public get privilegeGroups() {
|
||||
return this.config.privilegeGroups;
|
||||
}
|
||||
|
||||
public toRaw() {
|
||||
return { ...this.config };
|
||||
}
|
||||
}
|
|
@ -4,5 +4,5 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"optionalPlugins": ["timelion"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
"ui": true
|
||||
}
|
||||
|
|
44
x-pack/plugins/features/public/features_api_client.test.ts
Normal file
44
x-pack/plugins/features/public/features_api_client.test.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { coreMock } from 'src/core/public/mocks';
|
||||
import { FeaturesAPIClient } from './features_api_client';
|
||||
|
||||
describe('Features API Client', () => {
|
||||
describe('#getFeatures', () => {
|
||||
it('returns an array of Features', async () => {
|
||||
const rawFeatures = [
|
||||
{
|
||||
id: 'feature-a',
|
||||
},
|
||||
{
|
||||
id: 'feature-b',
|
||||
},
|
||||
{
|
||||
id: 'feature-c',
|
||||
},
|
||||
{
|
||||
id: 'feature-d',
|
||||
},
|
||||
{
|
||||
id: 'feature-e',
|
||||
},
|
||||
];
|
||||
const coreSetup = coreMock.createSetup();
|
||||
coreSetup.http.get.mockResolvedValue(rawFeatures);
|
||||
|
||||
const client = new FeaturesAPIClient(coreSetup.http);
|
||||
const result = await client.getFeatures();
|
||||
expect(result.map(f => f.id)).toEqual([
|
||||
'feature-a',
|
||||
'feature-b',
|
||||
'feature-c',
|
||||
'feature-d',
|
||||
'feature-e',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
17
x-pack/plugins/features/public/features_api_client.ts
Normal file
17
x-pack/plugins/features/public/features_api_client.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from 'src/core/public';
|
||||
import { FeatureConfig, Feature } from '.';
|
||||
|
||||
export class FeaturesAPIClient {
|
||||
constructor(private readonly http: HttpSetup) {}
|
||||
|
||||
public async getFeatures() {
|
||||
const features = await this.http.get<FeatureConfig[]>('/api/features');
|
||||
return features.map(config => new Feature(config));
|
||||
}
|
||||
}
|
|
@ -4,4 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common';
|
||||
import { PluginInitializer } from 'src/core/public';
|
||||
import { FeaturesPlugin, FeaturesPluginSetup, FeaturesPluginStart } from './plugin';
|
||||
|
||||
export {
|
||||
Feature,
|
||||
FeatureConfig,
|
||||
FeatureKibanaPrivileges,
|
||||
SubFeatureConfig,
|
||||
SubFeaturePrivilegeConfig,
|
||||
} from '../common';
|
||||
|
||||
export { FeaturesPluginSetup, FeaturesPluginStart } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<FeaturesPluginSetup, FeaturesPluginStart> = () =>
|
||||
new FeaturesPlugin();
|
||||
|
|
17
x-pack/plugins/features/public/mocks.ts
Normal file
17
x-pack/plugins/features/public/mocks.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { FeaturesPluginStart } from './plugin';
|
||||
|
||||
const createStart = (): jest.Mocked<FeaturesPluginStart> => {
|
||||
return {
|
||||
getFeatures: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
export const featuresPluginMock = {
|
||||
createStart,
|
||||
};
|
53
x-pack/plugins/features/public/plugin.test.ts
Normal file
53
x-pack/plugins/features/public/plugin.test.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { FeaturesPlugin } from './plugin';
|
||||
|
||||
import { coreMock, httpServiceMock } from 'src/core/public/mocks';
|
||||
|
||||
jest.mock('./features_api_client', () => {
|
||||
const instance = {
|
||||
getFeatures: jest.fn(),
|
||||
};
|
||||
return {
|
||||
FeaturesAPIClient: jest.fn().mockImplementation(() => instance),
|
||||
};
|
||||
});
|
||||
|
||||
import { FeaturesAPIClient } from './features_api_client';
|
||||
|
||||
describe('Features Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
it('returns expected public contract', () => {
|
||||
const plugin = new FeaturesPlugin();
|
||||
expect(plugin.setup(coreMock.createSetup())).toMatchInlineSnapshot(`undefined`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
it('returns expected public contract', () => {
|
||||
const plugin = new FeaturesPlugin();
|
||||
plugin.setup(coreMock.createSetup());
|
||||
|
||||
expect(plugin.start()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"getFeatures": [Function],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('#getFeatures calls the underlying FeaturesAPIClient', () => {
|
||||
const plugin = new FeaturesPlugin();
|
||||
const apiClient = new FeaturesAPIClient(httpServiceMock.createSetupContract());
|
||||
|
||||
plugin.setup(coreMock.createSetup());
|
||||
|
||||
const start = plugin.start();
|
||||
start.getFeatures();
|
||||
expect(apiClient.getFeatures).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
27
x-pack/plugins/features/public/plugin.ts
Normal file
27
x-pack/plugins/features/public/plugin.ts
Normal 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 { Plugin, CoreSetup } from 'src/core/public';
|
||||
import { FeaturesAPIClient } from './features_api_client';
|
||||
|
||||
export class FeaturesPlugin implements Plugin<FeaturesPluginSetup, FeaturesPluginStart> {
|
||||
private apiClient?: FeaturesAPIClient;
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
this.apiClient = new FeaturesAPIClient(core.http);
|
||||
}
|
||||
|
||||
public start() {
|
||||
return {
|
||||
getFeatures: () => this.apiClient!.getFeatures(),
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
export type FeaturesPluginSetup = ReturnType<FeaturesPlugin['setup']>;
|
||||
export type FeaturesPluginStart = ReturnType<FeaturesPlugin['start']>;
|
458
x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap
generated
Normal file
458
x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap
generated
Normal file
|
@ -0,0 +1,458 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`buildOSSFeatures returns the advancedSettings feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"advanced_settings",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"settings",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"config",
|
||||
],
|
||||
"read": Array [],
|
||||
},
|
||||
"ui": Array [
|
||||
"save",
|
||||
],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"advanced_settings",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"settings",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [],
|
||||
},
|
||||
"ui": Array [],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures returns the dashboard feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"dashboard",
|
||||
],
|
||||
"management": Object {},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"dashboard",
|
||||
"url",
|
||||
"query",
|
||||
],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
"search",
|
||||
"visualization",
|
||||
"timelion-sheet",
|
||||
"canvas-workpad",
|
||||
"lens",
|
||||
"map",
|
||||
],
|
||||
},
|
||||
"ui": Array [
|
||||
"createNew",
|
||||
"show",
|
||||
"showWriteControls",
|
||||
"saveQuery",
|
||||
"createShortUrl",
|
||||
],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"dashboard",
|
||||
],
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
"search",
|
||||
"visualization",
|
||||
"timelion-sheet",
|
||||
"canvas-workpad",
|
||||
"map",
|
||||
"dashboard",
|
||||
"query",
|
||||
],
|
||||
},
|
||||
"ui": Array [
|
||||
"show",
|
||||
],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures returns the dev_tools feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [
|
||||
"console",
|
||||
],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"console",
|
||||
"searchprofiler",
|
||||
"grokdebugger",
|
||||
],
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [],
|
||||
},
|
||||
"ui": Array [
|
||||
"show",
|
||||
"save",
|
||||
],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [
|
||||
"console",
|
||||
],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"console",
|
||||
"searchprofiler",
|
||||
"grokdebugger",
|
||||
],
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [],
|
||||
},
|
||||
"ui": Array [
|
||||
"show",
|
||||
],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures returns the discover feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"discover",
|
||||
],
|
||||
"management": Object {},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"search",
|
||||
"query",
|
||||
"url",
|
||||
],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
],
|
||||
},
|
||||
"ui": Array [
|
||||
"show",
|
||||
"save",
|
||||
"saveQuery",
|
||||
"createShortUrl",
|
||||
],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"discover",
|
||||
],
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
"search",
|
||||
"query",
|
||||
],
|
||||
},
|
||||
"ui": Array [
|
||||
"show",
|
||||
],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"index_patterns",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"index_patterns",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"index-pattern",
|
||||
],
|
||||
"read": Array [],
|
||||
},
|
||||
"ui": Array [
|
||||
"save",
|
||||
],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"index_patterns",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"index_patterns",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
],
|
||||
},
|
||||
"ui": Array [],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures returns the savedObjectsManagement feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [
|
||||
"copySavedObjectsToSpaces",
|
||||
],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"saved_objects",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"objects",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
"read": Array [],
|
||||
},
|
||||
"ui": Array [
|
||||
"read",
|
||||
"edit",
|
||||
"delete",
|
||||
"copyIntoSpace",
|
||||
],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [
|
||||
"copySavedObjectsToSpaces",
|
||||
],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"saved_objects",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"objects",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
},
|
||||
"ui": Array [
|
||||
"read",
|
||||
],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures returns the timelion feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"timelion",
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"timelion",
|
||||
],
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"timelion-sheet",
|
||||
],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
],
|
||||
},
|
||||
"ui": Array [
|
||||
"save",
|
||||
],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"timelion",
|
||||
"kibana",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"timelion",
|
||||
],
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
"timelion-sheet",
|
||||
],
|
||||
},
|
||||
"ui": Array [],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures returns the visualize feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
"lens",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"visualize",
|
||||
],
|
||||
"management": Object {},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"visualization",
|
||||
"query",
|
||||
"lens",
|
||||
"url",
|
||||
],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
"search",
|
||||
],
|
||||
},
|
||||
"ui": Array [
|
||||
"show",
|
||||
"delete",
|
||||
"save",
|
||||
"saveQuery",
|
||||
"createShortUrl",
|
||||
],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"app": Array [
|
||||
"kibana",
|
||||
"lens",
|
||||
],
|
||||
"catalogue": Array [
|
||||
"visualize",
|
||||
],
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [
|
||||
"index-pattern",
|
||||
"search",
|
||||
"visualization",
|
||||
"query",
|
||||
"lens",
|
||||
],
|
||||
},
|
||||
"ui": Array [
|
||||
"show",
|
||||
],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -5,15 +5,15 @@
|
|||
*/
|
||||
|
||||
import { FeatureRegistry } from './feature_registry';
|
||||
import { Feature } from '../common/feature';
|
||||
import { FeatureConfig } from '../common/feature';
|
||||
|
||||
describe('FeatureRegistry', () => {
|
||||
it('allows a minimal feature to be registered', () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
@ -22,18 +22,18 @@ describe('FeatureRegistry', () => {
|
|||
expect(result).toHaveLength(1);
|
||||
|
||||
// Should be the equal, but not the same instance (i.e., a defensive copy)
|
||||
expect(result[0]).not.toBe(feature);
|
||||
expect(result[0]).toEqual(feature);
|
||||
expect(result[0].toRaw()).not.toBe(feature);
|
||||
expect(result[0].toRaw()).toEqual(feature);
|
||||
});
|
||||
|
||||
it('allows a complex feature to be registered', () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
excludeFromBasePrivileges: true,
|
||||
icon: 'addDataApp',
|
||||
navLinkId: 'someNavLink',
|
||||
app: ['app1', 'app2'],
|
||||
app: ['app1'],
|
||||
validLicenses: ['standard', 'basic', 'gold', 'platinum'],
|
||||
catalogue: ['foo'],
|
||||
management: {
|
||||
|
@ -53,7 +53,61 @@ describe('FeatureRegistry', () => {
|
|||
api: ['someApiEndpointTag', 'anotherEndpointTag'],
|
||||
ui: ['allowsFoo', 'showBar', 'showBaz'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['config', 'url'],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'sub-feature-1',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'foo',
|
||||
name: 'foo',
|
||||
includeIn: 'read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
id: 'bar',
|
||||
name: 'bar',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
{
|
||||
id: 'baz',
|
||||
name: 'baz',
|
||||
includeIn: 'none',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
privilegesTooltip: 'some fancy tooltip',
|
||||
reserved: {
|
||||
privilege: {
|
||||
|
@ -79,36 +133,61 @@ describe('FeatureRegistry', () => {
|
|||
expect(result).toHaveLength(1);
|
||||
|
||||
// Should be the equal, but not the same instance (i.e., a defensive copy)
|
||||
expect(result[0]).not.toBe(feature);
|
||||
expect(result[0]).toEqual(feature);
|
||||
expect(result[0].toRaw()).not.toBe(feature);
|
||||
expect(result[0].toRaw()).toEqual(feature);
|
||||
});
|
||||
|
||||
it(`automatically grants 'all' access to telemetry saved objects for the 'all' privilege`, () => {
|
||||
const feature: Feature = {
|
||||
it(`requires a value for privileges`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
privileges: {
|
||||
all: {
|
||||
ui: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"child \\"privileges\\" fails because [\\"privileges\\" is required]"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`does not allow sub-features to be registered when no primary privileges are not registered`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
privileges: null,
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'my sub feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'my-sub-priv',
|
||||
name: 'my sub priv',
|
||||
includeIn: 'none',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
featureRegistry.register(feature);
|
||||
const result = featureRegistry.getAll();
|
||||
|
||||
const allPrivilege = result[0].privileges.all;
|
||||
expect(allPrivilege.savedObject.all).toEqual(['telemetry']);
|
||||
expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"child \\"subFeatures\\" fails because [\\"subFeatures\\" must contain less than or equal to 0 items]"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`automatically grants 'read' access to config and url saved objects for both privileges`, () => {
|
||||
const feature: Feature = {
|
||||
it(`automatically grants 'all' access to telemetry saved objects for the 'all' privilege`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
|
@ -134,18 +213,55 @@ describe('FeatureRegistry', () => {
|
|||
featureRegistry.register(feature);
|
||||
const result = featureRegistry.getAll();
|
||||
|
||||
const allPrivilege = result[0].privileges.all;
|
||||
const readPrivilege = result[0].privileges.read;
|
||||
expect(allPrivilege.savedObject.read).toEqual(['config', 'url']);
|
||||
expect(readPrivilege.savedObject.read).toEqual(['config', 'url']);
|
||||
expect(result[0].privileges).toHaveProperty('all');
|
||||
expect(result[0].privileges).toHaveProperty('read');
|
||||
|
||||
const allPrivilege = result[0].privileges?.all;
|
||||
expect(allPrivilege?.savedObject.all).toEqual(['telemetry']);
|
||||
});
|
||||
|
||||
it(`automatically grants 'all' access to telemetry and 'read' to [config, url] saved objects for the reserved privilege`, () => {
|
||||
const feature: Feature = {
|
||||
it(`automatically grants 'read' access to config and url saved objects for both privileges`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: {
|
||||
all: {
|
||||
ui: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
ui: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
featureRegistry.register(feature);
|
||||
const result = featureRegistry.getAll();
|
||||
|
||||
expect(result[0].privileges).toHaveProperty('all');
|
||||
expect(result[0].privileges).toHaveProperty('read');
|
||||
|
||||
const allPrivilege = result[0].privileges?.all;
|
||||
const readPrivilege = result[0].privileges?.read;
|
||||
expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']);
|
||||
expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']);
|
||||
});
|
||||
|
||||
it(`automatically grants 'all' access to telemetry and 'read' to [config, url] saved objects for the reserved privilege`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
privileges: null,
|
||||
reserved: {
|
||||
description: 'foo',
|
||||
privilege: {
|
||||
|
@ -168,7 +284,7 @@ describe('FeatureRegistry', () => {
|
|||
});
|
||||
|
||||
it(`does not duplicate the automatic grants if specified on the incoming feature`, () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
|
@ -194,26 +310,29 @@ describe('FeatureRegistry', () => {
|
|||
featureRegistry.register(feature);
|
||||
const result = featureRegistry.getAll();
|
||||
|
||||
const allPrivilege = result[0].privileges.all;
|
||||
const readPrivilege = result[0].privileges.read;
|
||||
expect(allPrivilege.savedObject.all).toEqual(['telemetry']);
|
||||
expect(allPrivilege.savedObject.read).toEqual(['config', 'url']);
|
||||
expect(readPrivilege.savedObject.read).toEqual(['config', 'url']);
|
||||
expect(result[0].privileges).toHaveProperty('all');
|
||||
expect(result[0].privileges).toHaveProperty('read');
|
||||
|
||||
const allPrivilege = result[0].privileges!.all;
|
||||
const readPrivilege = result[0].privileges!.read;
|
||||
expect(allPrivilege?.savedObject.all).toEqual(['telemetry']);
|
||||
expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']);
|
||||
expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']);
|
||||
});
|
||||
|
||||
it(`does not allow duplicate features to be registered`, () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
};
|
||||
|
||||
const duplicateFeature: Feature = {
|
||||
const duplicateFeature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Duplicate Test Feature',
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
@ -233,7 +352,7 @@ describe('FeatureRegistry', () => {
|
|||
name: 'some feature',
|
||||
navLinkId: prohibitedChars,
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
})
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
@ -248,7 +367,7 @@ describe('FeatureRegistry', () => {
|
|||
kibana: [prohibitedChars],
|
||||
},
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
})
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
@ -261,7 +380,7 @@ describe('FeatureRegistry', () => {
|
|||
name: 'some feature',
|
||||
catalogue: [prohibitedChars],
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
})
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
@ -275,19 +394,20 @@ describe('FeatureRegistry', () => {
|
|||
id: prohibitedId,
|
||||
name: 'some feature',
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
})
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents features from being registered with invalid privilege names', () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: ['app1', 'app2'],
|
||||
privileges: {
|
||||
foo: {
|
||||
name: 'Foo',
|
||||
app: ['app1', 'app2'],
|
||||
savedObject: {
|
||||
all: ['config', 'space', 'etc'],
|
||||
|
@ -296,7 +416,7 @@ describe('FeatureRegistry', () => {
|
|||
api: ['someApiEndpointTag', 'anotherEndpointTag'],
|
||||
ui: ['allowsFoo', 'showBar', 'showBaz'],
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
@ -306,7 +426,7 @@ describe('FeatureRegistry', () => {
|
|||
});
|
||||
|
||||
it(`prevents privileges from specifying app entries that don't exist at the root level`, () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: ['bar'],
|
||||
|
@ -319,6 +439,14 @@ describe('FeatureRegistry', () => {
|
|||
ui: [],
|
||||
app: ['foo', 'bar', 'baz'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: ['foo', 'bar', 'baz'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -329,12 +457,67 @@ describe('FeatureRegistry', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it(`prevents features from specifying app entries that don't exist at the privilege level`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: ['foo', 'bar', 'baz'],
|
||||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: ['bar'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'my sub feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cool-sub-feature-privilege',
|
||||
name: 'cool privilege',
|
||||
includeIn: 'none',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: ['foo'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
||||
expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Feature test-feature specifies app entries which are not granted to any privileges: baz"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`prevents reserved privileges from specifying app entries that don't exist at the root level`, () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: ['bar'],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
reserved: {
|
||||
description: 'something',
|
||||
privilege: {
|
||||
|
@ -355,8 +538,34 @@ describe('FeatureRegistry', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it(`prevents features from specifying app entries that don't exist at the reserved privilege level`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: ['foo', 'bar', 'baz'],
|
||||
privileges: null,
|
||||
reserved: {
|
||||
description: 'something',
|
||||
privilege: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: ['foo', 'bar'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
||||
expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Feature test-feature specifies app entries which are not granted to any privileges: baz"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`prevents privileges from specifying catalogue entries that don't exist at the root level`, () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
|
@ -371,6 +580,15 @@ describe('FeatureRegistry', () => {
|
|||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
read: {
|
||||
catalogue: ['foo', 'bar', 'baz'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -381,13 +599,71 @@ describe('FeatureRegistry', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it(`prevents features from specifying catalogue entries that don't exist at the privilege level`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
catalogue: ['foo', 'bar', 'baz'],
|
||||
privileges: {
|
||||
all: {
|
||||
catalogue: ['foo'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
read: {
|
||||
catalogue: ['foo'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'my sub feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cool-sub-feature-privilege',
|
||||
name: 'cool privilege',
|
||||
includeIn: 'none',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
catalogue: ['bar'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
||||
expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`prevents reserved privileges from specifying catalogue entries that don't exist at the root level`, () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
catalogue: ['bar'],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
reserved: {
|
||||
description: 'something',
|
||||
privilege: {
|
||||
|
@ -409,8 +685,36 @@ describe('FeatureRegistry', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it(`prevents features from specifying catalogue entries that don't exist at the reserved privilege level`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
catalogue: ['foo', 'bar', 'baz'],
|
||||
privileges: null,
|
||||
reserved: {
|
||||
description: 'something',
|
||||
privilege: {
|
||||
catalogue: ['foo', 'bar'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
||||
expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`prevents privileges from specifying management sections that don't exist at the root level`, () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
|
@ -431,6 +735,18 @@ describe('FeatureRegistry', () => {
|
|||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
read: {
|
||||
catalogue: ['bar'],
|
||||
management: {
|
||||
elasticsearch: ['hey'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -441,8 +757,79 @@ describe('FeatureRegistry', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it(`prevents features from specifying management sections that don't exist at the privilege level`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
catalogue: ['bar'],
|
||||
management: {
|
||||
kibana: ['hey'],
|
||||
elasticsearch: ['hey', 'there'],
|
||||
},
|
||||
privileges: {
|
||||
all: {
|
||||
catalogue: ['bar'],
|
||||
management: {
|
||||
elasticsearch: ['hey'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
read: {
|
||||
catalogue: ['bar'],
|
||||
management: {
|
||||
elasticsearch: ['hey'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'my sub feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cool-sub-feature-privilege',
|
||||
name: 'cool privilege',
|
||||
includeIn: 'none',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
management: {
|
||||
kibana: ['hey'],
|
||||
elasticsearch: ['hey'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
||||
expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Feature test-feature specifies management entries which are not granted to any privileges: elasticsearch.there"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`prevents reserved privileges from specifying management entries that don't exist at the root level`, () => {
|
||||
const feature: Feature = {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
|
@ -450,7 +837,7 @@ describe('FeatureRegistry', () => {
|
|||
management: {
|
||||
kibana: ['hey'],
|
||||
},
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
reserved: {
|
||||
description: 'something',
|
||||
privilege: {
|
||||
|
@ -475,18 +862,52 @@ describe('FeatureRegistry', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('cannot register feature after getAll has been called', () => {
|
||||
const feature1: Feature = {
|
||||
it(`prevents features from specifying management entries that don't exist at the reserved privilege level`, () => {
|
||||
const feature: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
privileges: {},
|
||||
catalogue: ['bar'],
|
||||
management: {
|
||||
kibana: ['hey', 'hey-there'],
|
||||
},
|
||||
privileges: null,
|
||||
reserved: {
|
||||
description: 'something',
|
||||
privilege: {
|
||||
catalogue: ['bar'],
|
||||
management: {
|
||||
kibana: ['hey-there'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
app: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const feature2: Feature = {
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
||||
expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Feature test-feature specifies management entries which are not granted to any privileges: kibana.hey"`
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot register feature after getAll has been called', () => {
|
||||
const feature1: FeatureConfig = {
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
app: [],
|
||||
privileges: null,
|
||||
};
|
||||
const feature2: FeatureConfig = {
|
||||
id: 'test-feature-2',
|
||||
name: 'Test Feature 2',
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
};
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
*/
|
||||
|
||||
import { cloneDeep, uniq } from 'lodash';
|
||||
import { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common';
|
||||
import { FeatureConfig, Feature, FeatureKibanaPrivileges } from '../common';
|
||||
import { validateFeature } from './feature_schema';
|
||||
|
||||
export class FeatureRegistry {
|
||||
private locked = false;
|
||||
private features: Record<string, Feature> = {};
|
||||
private features: Record<string, FeatureConfig> = {};
|
||||
|
||||
public register(feature: FeatureWithAllOrReadPrivileges) {
|
||||
public register(feature: FeatureConfig) {
|
||||
if (this.locked) {
|
||||
throw new Error(
|
||||
`Features are locked, can't register new features. Attempt to register ${feature.id} failed.`
|
||||
|
@ -25,20 +25,21 @@ export class FeatureRegistry {
|
|||
throw new Error(`Feature with id ${feature.id} is already registered.`);
|
||||
}
|
||||
|
||||
const featureCopy: Feature = cloneDeep(feature as Feature);
|
||||
const featureCopy = cloneDeep(feature);
|
||||
|
||||
this.features[feature.id] = applyAutomaticPrivilegeGrants(featureCopy as Feature);
|
||||
this.features[feature.id] = applyAutomaticPrivilegeGrants(featureCopy);
|
||||
}
|
||||
|
||||
public getAll(): Feature[] {
|
||||
this.locked = true;
|
||||
return cloneDeep(Object.values(this.features));
|
||||
return Object.values(this.features).map(featureConfig => new Feature(featureConfig));
|
||||
}
|
||||
}
|
||||
|
||||
function applyAutomaticPrivilegeGrants(feature: Feature): Feature {
|
||||
const { all: allPrivilege, read: readPrivilege } = feature.privileges;
|
||||
const reservedPrivilege = feature.reserved ? feature.reserved.privilege : null;
|
||||
function applyAutomaticPrivilegeGrants(feature: FeatureConfig): FeatureConfig {
|
||||
const allPrivilege = feature.privileges?.all;
|
||||
const readPrivilege = feature.privileges?.read;
|
||||
const reservedPrivilege = feature.reserved?.privilege;
|
||||
|
||||
applyAutomaticAllPrivilegeGrants(allPrivilege, reservedPrivilege);
|
||||
applyAutomaticReadPrivilegeGrants(readPrivilege);
|
||||
|
@ -46,7 +47,9 @@ function applyAutomaticPrivilegeGrants(feature: Feature): Feature {
|
|||
return feature;
|
||||
}
|
||||
|
||||
function applyAutomaticAllPrivilegeGrants(...allPrivileges: Array<FeatureKibanaPrivileges | null>) {
|
||||
function applyAutomaticAllPrivilegeGrants(
|
||||
...allPrivileges: Array<FeatureKibanaPrivileges | undefined>
|
||||
) {
|
||||
allPrivileges.forEach(allPrivilege => {
|
||||
if (allPrivilege) {
|
||||
allPrivilege.savedObject.all = uniq([...allPrivilege.savedObject.all, 'telemetry']);
|
||||
|
@ -56,7 +59,7 @@ function applyAutomaticAllPrivilegeGrants(...allPrivileges: Array<FeatureKibanaP
|
|||
}
|
||||
|
||||
function applyAutomaticReadPrivilegeGrants(
|
||||
...readPrivileges: Array<FeatureKibanaPrivileges | null>
|
||||
...readPrivileges: Array<FeatureKibanaPrivileges | undefined>
|
||||
) {
|
||||
readPrivileges.forEach(readPrivilege => {
|
||||
if (readPrivilege) {
|
||||
|
|
|
@ -8,13 +8,15 @@ import Joi from 'joi';
|
|||
|
||||
import { difference } from 'lodash';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/server';
|
||||
import { FeatureWithAllOrReadPrivileges } from '../common/feature';
|
||||
import { FeatureConfig } from '../common/feature';
|
||||
import { FeatureKibanaPrivileges } from '.';
|
||||
|
||||
// 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 featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
const subFeaturePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/;
|
||||
|
||||
|
@ -43,12 +45,52 @@ const privilegeSchema = Joi.object({
|
|||
.required(),
|
||||
});
|
||||
|
||||
const subFeaturePrivilegeSchema = Joi.object({
|
||||
id: Joi.string()
|
||||
.regex(subFeaturePrivilegePartRegex)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
includeIn: Joi.string()
|
||||
.allow('all', 'read', 'none')
|
||||
.required(),
|
||||
management: managementSchema,
|
||||
catalogue: catalogueSchema,
|
||||
api: Joi.array().items(Joi.string()),
|
||||
app: Joi.array().items(Joi.string()),
|
||||
savedObject: Joi.object({
|
||||
all: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
read: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
}).required(),
|
||||
ui: Joi.array()
|
||||
.items(Joi.string().regex(uiCapabilitiesRegex))
|
||||
.required(),
|
||||
});
|
||||
|
||||
const subFeatureSchema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
privilegeGroups: Joi.array().items(
|
||||
Joi.object({
|
||||
groupType: Joi.string()
|
||||
.valid('mutually_exclusive', 'independent')
|
||||
.required(),
|
||||
privileges: Joi.array()
|
||||
.items(subFeaturePrivilegeSchema)
|
||||
.min(1),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const schema = Joi.object({
|
||||
id: Joi.string()
|
||||
.regex(featurePrivilegePartRegex)
|
||||
.invalid(...prohibitedFeatureIds)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
order: Joi.number(),
|
||||
excludeFromBasePrivileges: Joi.boolean(),
|
||||
validLicenses: Joi.array().items(
|
||||
Joi.string().valid('basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial')
|
||||
|
@ -64,7 +106,16 @@ const schema = Joi.object({
|
|||
privileges: Joi.object({
|
||||
all: privilegeSchema,
|
||||
read: privilegeSchema,
|
||||
}).required(),
|
||||
})
|
||||
.allow(null)
|
||||
.required(),
|
||||
subFeatures: Joi.when('privileges', {
|
||||
is: null,
|
||||
then: Joi.array()
|
||||
.items(subFeatureSchema)
|
||||
.max(0),
|
||||
otherwise: Joi.array().items(subFeatureSchema),
|
||||
}),
|
||||
privilegesTooltip: Joi.string(),
|
||||
reserved: Joi.object({
|
||||
privilege: privilegeSchema.required(),
|
||||
|
@ -72,7 +123,7 @@ const schema = Joi.object({
|
|||
}),
|
||||
});
|
||||
|
||||
export function validateFeature(feature: FeatureWithAllOrReadPrivileges) {
|
||||
export function validateFeature(feature: FeatureConfig) {
|
||||
const validateResult = Joi.validate(feature, schema);
|
||||
if (validateResult.error) {
|
||||
throw validateResult.error;
|
||||
|
@ -80,17 +131,21 @@ export function validateFeature(feature: FeatureWithAllOrReadPrivileges) {
|
|||
// the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid.
|
||||
const { app = [], management = {}, catalogue = [] } = feature;
|
||||
|
||||
const privilegeEntries = [...Object.entries(feature.privileges)];
|
||||
if (feature.reserved) {
|
||||
privilegeEntries.push(['reserved', feature.reserved.privilege]);
|
||||
}
|
||||
const unseenApps = new Set(app);
|
||||
|
||||
privilegeEntries.forEach(([privilegeId, privilegeDefinition]) => {
|
||||
if (!privilegeDefinition) {
|
||||
throw new Error('Privilege definition may not be null or undefined');
|
||||
}
|
||||
const managementSets = Object.entries(management).map(entry => [
|
||||
entry[0],
|
||||
new Set(entry[1]),
|
||||
]) as Array<[string, Set<string>]>;
|
||||
|
||||
const unknownAppEntries = difference(privilegeDefinition.app || [], app);
|
||||
const unseenManagement = new Map<string, Set<string>>(managementSets);
|
||||
|
||||
const unseenCatalogue = new Set(catalogue);
|
||||
|
||||
function validateAppEntry(privilegeId: string, entry: string[] = []) {
|
||||
entry.forEach(privilegeApp => unseenApps.delete(privilegeApp));
|
||||
|
||||
const unknownAppEntries = difference(entry, app);
|
||||
if (unknownAppEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
|
@ -98,8 +153,12 @@ export function validateFeature(feature: FeatureWithAllOrReadPrivileges) {
|
|||
}.${privilegeId} has unknown app entries: ${unknownAppEntries.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const unknownCatalogueEntries = difference(privilegeDefinition.catalogue || [], catalogue);
|
||||
function validateCatalogueEntry(privilegeId: string, entry: string[] = []) {
|
||||
entry.forEach(privilegeCatalogue => unseenCatalogue.delete(privilegeCatalogue));
|
||||
|
||||
const unknownCatalogueEntries = difference(entry || [], catalogue);
|
||||
if (unknownCatalogueEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
|
@ -107,27 +166,113 @@ export function validateFeature(feature: FeatureWithAllOrReadPrivileges) {
|
|||
}.${privilegeId} has unknown catalogue entries: ${unknownCatalogueEntries.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(privilegeDefinition.management || {}).forEach(
|
||||
([managementSectionId, managementEntry]) => {
|
||||
if (!management[managementSectionId]) {
|
||||
throw new Error(
|
||||
`Feature privilege ${feature.id}.${privilegeId} has unknown management section: ${managementSectionId}`
|
||||
);
|
||||
}
|
||||
|
||||
const unknownSectionEntries = difference(managementEntry, management[managementSectionId]);
|
||||
|
||||
if (unknownSectionEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
feature.id
|
||||
}.${privilegeId} has unknown management entries for section ${managementSectionId}: ${unknownSectionEntries.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
function validateManagementEntry(
|
||||
privilegeId: string,
|
||||
managementEntry: Record<string, string[]> = {}
|
||||
) {
|
||||
Object.entries(managementEntry).forEach(([managementSectionId, managementSectionEntry]) => {
|
||||
if (unseenManagement.has(managementSectionId)) {
|
||||
managementSectionEntry.forEach(entry => {
|
||||
unseenManagement.get(managementSectionId)!.delete(entry);
|
||||
if (unseenManagement.get(managementSectionId)?.size === 0) {
|
||||
unseenManagement.delete(managementSectionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
if (!management[managementSectionId]) {
|
||||
throw new Error(
|
||||
`Feature privilege ${feature.id}.${privilegeId} has unknown management section: ${managementSectionId}`
|
||||
);
|
||||
}
|
||||
|
||||
const unknownSectionEntries = difference(
|
||||
managementSectionEntry,
|
||||
management[managementSectionId]
|
||||
);
|
||||
|
||||
if (unknownSectionEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
feature.id
|
||||
}.${privilegeId} has unknown management entries for section ${managementSectionId}: ${unknownSectionEntries.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const privilegeEntries: Array<[string, FeatureKibanaPrivileges]> = [];
|
||||
if (feature.privileges) {
|
||||
privilegeEntries.push(...Object.entries(feature.privileges));
|
||||
}
|
||||
if (feature.reserved) {
|
||||
privilegeEntries.push(['reserved', feature.reserved.privilege]);
|
||||
}
|
||||
|
||||
if (privilegeEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
privilegeEntries.forEach(([privilegeId, privilegeDefinition]) => {
|
||||
if (!privilegeDefinition) {
|
||||
throw new Error('Privilege definition may not be null or undefined');
|
||||
}
|
||||
|
||||
validateAppEntry(privilegeId, privilegeDefinition.app);
|
||||
|
||||
validateCatalogueEntry(privilegeId, privilegeDefinition.catalogue);
|
||||
|
||||
validateManagementEntry(privilegeId, privilegeDefinition.management);
|
||||
});
|
||||
|
||||
const subFeatureEntries = feature.subFeatures ?? [];
|
||||
subFeatureEntries.forEach(subFeature => {
|
||||
subFeature.privilegeGroups.forEach(subFeaturePrivilegeGroup => {
|
||||
subFeaturePrivilegeGroup.privileges.forEach(subFeaturePrivilege => {
|
||||
validateAppEntry(subFeaturePrivilege.id, subFeaturePrivilege.app);
|
||||
validateCatalogueEntry(subFeaturePrivilege.id, subFeaturePrivilege.catalogue);
|
||||
validateManagementEntry(subFeaturePrivilege.id, subFeaturePrivilege.management);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (unseenApps.size > 0) {
|
||||
throw new Error(
|
||||
`Feature ${
|
||||
feature.id
|
||||
} specifies app entries which are not granted to any privileges: ${Array.from(
|
||||
unseenApps.values()
|
||||
).join(',')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (unseenCatalogue.size > 0) {
|
||||
throw new Error(
|
||||
`Feature ${
|
||||
feature.id
|
||||
} specifies catalogue entries which are not granted to any privileges: ${Array.from(
|
||||
unseenCatalogue.values()
|
||||
).join(',')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (unseenManagement.size > 0) {
|
||||
const ungrantedManagement = Array.from(unseenManagement.entries()).reduce((acc, entry) => {
|
||||
const values = Array.from(entry[1].values()).map(
|
||||
managementPage => `${entry[0]}.${managementPage}`
|
||||
);
|
||||
return [...acc, ...values];
|
||||
}, [] as string[]);
|
||||
|
||||
throw new Error(
|
||||
`Feature ${
|
||||
feature.id
|
||||
} specifies management entries which are not granted to any privileges: ${ungrantedManagement.join(
|
||||
','
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { Plugin } from './plugin';
|
|||
// run-time contracts.
|
||||
export { uiCapabilitiesRegex } from './feature_schema';
|
||||
|
||||
export { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common';
|
||||
export { Feature, FeatureConfig, FeatureKibanaPrivileges } from '../common';
|
||||
export { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
|
||||
import { buildOSSFeatures } from './oss_features';
|
||||
import { featurePrivilegeIterator } from '../../security/server/authorization';
|
||||
import { Feature } from '.';
|
||||
|
||||
describe('buildOSSFeatures', () => {
|
||||
it('returns features including timelion', () => {
|
||||
|
@ -39,4 +41,17 @@ Array [
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
const features = buildOSSFeatures({ savedObjectTypes: ['foo', 'bar'], includeTimelion: true });
|
||||
features.forEach(featureConfig => {
|
||||
it(`returns the ${featureConfig.id} feature augmented with appropriate sub feature privileges`, () => {
|
||||
const privileges = [];
|
||||
for (const featurePrivilege of featurePrivilegeIterator(new Feature(featureConfig), {
|
||||
augmentWithSubFeaturePrivileges: true,
|
||||
})) {
|
||||
privileges.push(featurePrivilege);
|
||||
}
|
||||
expect(privileges).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Feature } from '../common/feature';
|
||||
import { FeatureConfig } from '../common/feature';
|
||||
|
||||
export interface BuildOSSFeaturesParams {
|
||||
savedObjectTypes: string[];
|
||||
|
@ -18,19 +18,24 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
name: i18n.translate('xpack.features.discoverFeatureName', {
|
||||
defaultMessage: 'Discover',
|
||||
}),
|
||||
order: 100,
|
||||
icon: 'discoverApp',
|
||||
navLinkId: 'kibana:discover',
|
||||
app: ['kibana'],
|
||||
catalogue: ['discover'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['discover'],
|
||||
savedObject: {
|
||||
all: ['search', 'url', 'query'],
|
||||
all: ['search', 'query'],
|
||||
read: ['index-pattern'],
|
||||
},
|
||||
ui: ['show', 'createShortUrl', 'save', 'saveQuery'],
|
||||
ui: ['show', 'save', 'saveQuery'],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['discover'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern', 'search', 'query'],
|
||||
|
@ -38,25 +43,59 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.features.ossFeatures.discoverShortUrlSubFeatureName', {
|
||||
defaultMessage: 'Short URLs',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'url_create',
|
||||
name: i18n.translate(
|
||||
'xpack.features.ossFeatures.discoverCreateShortUrlPrivilegeName',
|
||||
{
|
||||
defaultMessage: 'Create Short URLs',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['url'],
|
||||
read: [],
|
||||
},
|
||||
ui: ['createShortUrl'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'visualize',
|
||||
name: i18n.translate('xpack.features.visualizeFeatureName', {
|
||||
defaultMessage: 'Visualize',
|
||||
}),
|
||||
order: 200,
|
||||
icon: 'visualizeApp',
|
||||
navLinkId: 'kibana:visualize',
|
||||
app: ['kibana', 'lens'],
|
||||
catalogue: ['visualize'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['kibana', 'lens'],
|
||||
catalogue: ['visualize'],
|
||||
savedObject: {
|
||||
all: ['visualization', 'url', 'query', 'lens'],
|
||||
all: ['visualization', 'query', 'lens'],
|
||||
read: ['index-pattern', 'search'],
|
||||
},
|
||||
ui: ['show', 'createShortUrl', 'delete', 'save', 'saveQuery'],
|
||||
ui: ['show', 'delete', 'save', 'saveQuery'],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana', 'lens'],
|
||||
catalogue: ['visualize'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern', 'search', 'visualization', 'query', 'lens'],
|
||||
|
@ -64,18 +103,50 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.features.ossFeatures.visualizeShortUrlSubFeatureName', {
|
||||
defaultMessage: 'Short URLs',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'url_create',
|
||||
name: i18n.translate(
|
||||
'xpack.features.ossFeatures.visualizeCreateShortUrlPrivilegeName',
|
||||
{
|
||||
defaultMessage: 'Create Short URLs',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['url'],
|
||||
read: [],
|
||||
},
|
||||
ui: ['createShortUrl'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dashboard',
|
||||
name: i18n.translate('xpack.features.dashboardFeatureName', {
|
||||
defaultMessage: 'Dashboard',
|
||||
}),
|
||||
order: 300,
|
||||
icon: 'dashboardApp',
|
||||
navLinkId: 'kibana:dashboard',
|
||||
app: ['kibana'],
|
||||
catalogue: ['dashboard'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['dashboard'],
|
||||
savedObject: {
|
||||
all: ['dashboard', 'url', 'query'],
|
||||
read: [
|
||||
|
@ -91,6 +162,8 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
ui: ['createNew', 'show', 'showWriteControls', 'saveQuery'],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['dashboard'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [
|
||||
|
@ -107,18 +180,50 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.features.ossFeatures.dashboardShortUrlSubFeatureName', {
|
||||
defaultMessage: 'Short URLs',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'url_create',
|
||||
name: i18n.translate(
|
||||
'xpack.features.ossFeatures.dashboardCreateShortUrlPrivilegeName',
|
||||
{
|
||||
defaultMessage: 'Create Short URLs',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['url'],
|
||||
read: [],
|
||||
},
|
||||
ui: ['createShortUrl'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dev_tools',
|
||||
name: i18n.translate('xpack.features.devToolsFeatureName', {
|
||||
defaultMessage: 'Dev Tools',
|
||||
}),
|
||||
order: 1300,
|
||||
icon: 'devToolsApp',
|
||||
navLinkId: 'kibana:dev_tools',
|
||||
app: ['kibana'],
|
||||
catalogue: ['console', 'searchprofiler', 'grokdebugger'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['console', 'searchprofiler', 'grokdebugger'],
|
||||
api: ['console'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
@ -127,6 +232,8 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
ui: ['show', 'save'],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['console', 'searchprofiler', 'grokdebugger'],
|
||||
api: ['console'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
@ -145,6 +252,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
name: i18n.translate('xpack.features.advancedSettingsFeatureName', {
|
||||
defaultMessage: 'Advanced Settings',
|
||||
}),
|
||||
order: 1500,
|
||||
icon: 'advancedSettingsApp',
|
||||
app: ['kibana'],
|
||||
catalogue: ['advanced_settings'],
|
||||
|
@ -153,6 +261,11 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
},
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['advanced_settings'],
|
||||
management: {
|
||||
kibana: ['settings'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['config'],
|
||||
read: [],
|
||||
|
@ -160,6 +273,11 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
ui: ['save'],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['advanced_settings'],
|
||||
management: {
|
||||
kibana: ['settings'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -173,6 +291,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
name: i18n.translate('xpack.features.indexPatternFeatureName', {
|
||||
defaultMessage: 'Index Pattern Management',
|
||||
}),
|
||||
order: 1600,
|
||||
icon: 'indexPatternApp',
|
||||
app: ['kibana'],
|
||||
catalogue: ['index_patterns'],
|
||||
|
@ -181,6 +300,11 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
},
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['index_patterns'],
|
||||
management: {
|
||||
kibana: ['index_patterns'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['index-pattern'],
|
||||
read: [],
|
||||
|
@ -188,6 +312,11 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
ui: ['save'],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['index_patterns'],
|
||||
management: {
|
||||
kibana: ['index_patterns'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern'],
|
||||
|
@ -201,6 +330,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
name: i18n.translate('xpack.features.savedObjectsManagementFeatureName', {
|
||||
defaultMessage: 'Saved Objects Management',
|
||||
}),
|
||||
order: 1700,
|
||||
icon: 'savedObjectsApp',
|
||||
app: ['kibana'],
|
||||
catalogue: ['saved_objects'],
|
||||
|
@ -209,6 +339,11 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
},
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['saved_objects'],
|
||||
management: {
|
||||
kibana: ['objects'],
|
||||
},
|
||||
api: ['copySavedObjectsToSpaces'],
|
||||
savedObject: {
|
||||
all: [...savedObjectTypes],
|
||||
|
@ -217,6 +352,11 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
ui: ['read', 'edit', 'delete', 'copyIntoSpace'],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana'],
|
||||
catalogue: ['saved_objects'],
|
||||
management: {
|
||||
kibana: ['objects'],
|
||||
},
|
||||
api: ['copySavedObjectsToSpaces'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
@ -227,18 +367,21 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
},
|
||||
},
|
||||
...(includeTimelion ? [timelionFeature] : []),
|
||||
];
|
||||
] as FeatureConfig[];
|
||||
};
|
||||
|
||||
const timelionFeature: Feature = {
|
||||
const timelionFeature: FeatureConfig = {
|
||||
id: 'timelion',
|
||||
name: 'Timelion',
|
||||
order: 350,
|
||||
icon: 'timelionApp',
|
||||
navLinkId: 'timelion',
|
||||
app: ['timelion', 'kibana'],
|
||||
catalogue: ['timelion'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['timelion', 'kibana'],
|
||||
catalogue: ['timelion'],
|
||||
savedObject: {
|
||||
all: ['timelion-sheet'],
|
||||
read: ['index-pattern'],
|
||||
|
@ -246,6 +389,8 @@ const timelionFeature: Feature = {
|
|||
ui: ['save'],
|
||||
},
|
||||
read: {
|
||||
app: ['timelion', 'kibana'],
|
||||
catalogue: ['timelion'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern', 'timelion-sheet'],
|
||||
|
|
|
@ -15,7 +15,7 @@ import { deepFreeze } from '../../../../src/core/utils';
|
|||
import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_info';
|
||||
import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/timelion/server';
|
||||
import { FeatureRegistry } from './feature_registry';
|
||||
import { Feature, FeatureWithAllOrReadPrivileges } from '../common/feature';
|
||||
import { Feature, FeatureConfig } from '../common/feature';
|
||||
import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
|
||||
import { buildOSSFeatures } from './oss_features';
|
||||
import { defineRoutes } from './routes';
|
||||
|
@ -24,7 +24,7 @@ import { defineRoutes } from './routes';
|
|||
* Describes public Features plugin contract returned at the `setup` stage.
|
||||
*/
|
||||
export interface PluginSetupContract {
|
||||
registerFeature(feature: FeatureWithAllOrReadPrivileges): void;
|
||||
registerFeature(feature: FeatureConfig): void;
|
||||
getFeatures(): Feature[];
|
||||
getFeaturesUICapabilities(): UICapabilities;
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
|
||||
|
|
|
@ -10,6 +10,7 @@ import { defineRoutes } from './index';
|
|||
import { httpServerMock, httpServiceMock } from '../../../../../src/core/server/mocks';
|
||||
import { XPackInfoLicense } from '../../../../legacy/plugins/xpack_main/server/lib/xpack_info_license';
|
||||
import { RequestHandler } from '../../../../../src/core/server';
|
||||
import { FeatureConfig } from '../../common';
|
||||
|
||||
let currentLicenseLevel: string = 'gold';
|
||||
|
||||
|
@ -21,7 +22,23 @@ describe('GET /api/features', () => {
|
|||
id: 'feature_1',
|
||||
name: 'Feature 1',
|
||||
app: [],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
});
|
||||
|
||||
featureRegistry.register({
|
||||
id: 'feature_2',
|
||||
name: 'Feature 2',
|
||||
order: 2,
|
||||
app: [],
|
||||
privileges: null,
|
||||
});
|
||||
|
||||
featureRegistry.register({
|
||||
id: 'feature_3',
|
||||
name: 'Feature 2',
|
||||
order: 1,
|
||||
app: [],
|
||||
privileges: null,
|
||||
});
|
||||
|
||||
featureRegistry.register({
|
||||
|
@ -29,7 +46,7 @@ describe('GET /api/features', () => {
|
|||
name: 'Licensed Feature',
|
||||
app: ['bar-app'],
|
||||
validLicenses: ['gold'],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
});
|
||||
|
||||
const routerMock = httpServiceMock.createRouter();
|
||||
|
@ -51,37 +68,33 @@ describe('GET /api/features', () => {
|
|||
routeHandler = routerMock.get.mock.calls[0][1];
|
||||
});
|
||||
|
||||
it('returns a list of available features', async () => {
|
||||
it('returns a list of available features, sorted by their configured order', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
routeHandler(undefined as any, { query: {} } as any, mockResponse);
|
||||
|
||||
expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"app": Array [],
|
||||
"id": "feature_1",
|
||||
"name": "Feature 1",
|
||||
"privileges": Object {},
|
||||
},
|
||||
Object {
|
||||
"app": Array [
|
||||
"bar-app",
|
||||
],
|
||||
"id": "licensed_feature",
|
||||
"name": "Licensed Feature",
|
||||
"privileges": Object {},
|
||||
"validLicenses": Array [
|
||||
"gold",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
|
||||
const [call] = mockResponse.ok.mock.calls;
|
||||
const body = call[0]!.body as FeatureConfig[];
|
||||
|
||||
const features = body.map(feature => ({ id: feature.id, order: feature.order }));
|
||||
expect(features).toEqual([
|
||||
{
|
||||
id: 'feature_3',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: 'feature_2',
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: 'feature_1',
|
||||
order: undefined,
|
||||
},
|
||||
{
|
||||
id: 'licensed_feature',
|
||||
order: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`by default does not return features that arent allowed by current license`, async () => {
|
||||
|
@ -90,22 +103,26 @@ describe('GET /api/features', () => {
|
|||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
routeHandler(undefined as any, { query: {} } as any, mockResponse);
|
||||
|
||||
expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"app": Array [],
|
||||
"id": "feature_1",
|
||||
"name": "Feature 1",
|
||||
"privileges": Object {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
|
||||
const [call] = mockResponse.ok.mock.calls;
|
||||
const body = call[0]!.body as FeatureConfig[];
|
||||
|
||||
const features = body.map(feature => ({ id: feature.id, order: feature.order }));
|
||||
|
||||
expect(features).toEqual([
|
||||
{
|
||||
id: 'feature_3',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: 'feature_2',
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: 'feature_1',
|
||||
order: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`ignoreValidLicenses=false does not return features that arent allowed by current license`, async () => {
|
||||
|
@ -114,22 +131,26 @@ describe('GET /api/features', () => {
|
|||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
routeHandler(undefined as any, { query: { ignoreValidLicenses: false } } as any, mockResponse);
|
||||
|
||||
expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"app": Array [],
|
||||
"id": "feature_1",
|
||||
"name": "Feature 1",
|
||||
"privileges": Object {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
|
||||
const [call] = mockResponse.ok.mock.calls;
|
||||
const body = call[0]!.body as FeatureConfig[];
|
||||
|
||||
const features = body.map(feature => ({ id: feature.id, order: feature.order }));
|
||||
|
||||
expect(features).toEqual([
|
||||
{
|
||||
id: 'feature_3',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: 'feature_2',
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: 'feature_1',
|
||||
order: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`ignoreValidLicenses=true returns features that arent allowed by current license`, async () => {
|
||||
|
@ -138,32 +159,29 @@ describe('GET /api/features', () => {
|
|||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
routeHandler(undefined as any, { query: { ignoreValidLicenses: true } } as any, mockResponse);
|
||||
|
||||
expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"app": Array [],
|
||||
"id": "feature_1",
|
||||
"name": "Feature 1",
|
||||
"privileges": Object {},
|
||||
},
|
||||
Object {
|
||||
"app": Array [
|
||||
"bar-app",
|
||||
],
|
||||
"id": "licensed_feature",
|
||||
"name": "Licensed Feature",
|
||||
"privileges": Object {},
|
||||
"validLicenses": Array [
|
||||
"gold",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
|
||||
const [call] = mockResponse.ok.mock.calls;
|
||||
const body = call[0]!.body as FeatureConfig[];
|
||||
|
||||
const features = body.map(feature => ({ id: feature.id, order: feature.order }));
|
||||
|
||||
expect(features).toEqual([
|
||||
{
|
||||
id: 'feature_3',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: 'feature_2',
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: 'feature_1',
|
||||
order: undefined,
|
||||
},
|
||||
{
|
||||
id: 'licensed_feature',
|
||||
order: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,13 +31,19 @@ export function defineRoutes({ router, featureRegistry, getLegacyAPI }: RouteDef
|
|||
const allFeatures = featureRegistry.getAll();
|
||||
|
||||
return response.ok({
|
||||
body: allFeatures.filter(
|
||||
feature =>
|
||||
request.query.ignoreValidLicenses ||
|
||||
!feature.validLicenses ||
|
||||
!feature.validLicenses.length ||
|
||||
getLegacyAPI().xpackInfo.license.isOneOf(feature.validLicenses)
|
||||
),
|
||||
body: allFeatures
|
||||
.filter(
|
||||
feature =>
|
||||
request.query.ignoreValidLicenses ||
|
||||
!feature.validLicenses ||
|
||||
!feature.validLicenses.length ||
|
||||
getLegacyAPI().xpackInfo.license.isOneOf(feature.validLicenses)
|
||||
)
|
||||
.sort(
|
||||
(f1, f2) =>
|
||||
(f1.order ?? Number.MAX_SAFE_INTEGER) - (f2.order ?? Number.MAX_SAFE_INTEGER)
|
||||
)
|
||||
.map(feature => feature.toRaw()),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,17 +5,31 @@
|
|||
*/
|
||||
|
||||
import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
|
||||
import { Feature } from '.';
|
||||
import { SubFeaturePrivilegeGroupConfig } from '../common';
|
||||
|
||||
function createFeaturePrivilege(key: string, capabilities: string[] = []) {
|
||||
function createFeaturePrivilege(capabilities: string[] = []) {
|
||||
return {
|
||||
[key]: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
app: [],
|
||||
ui: [...capabilities],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
app: [],
|
||||
ui: [...capabilities],
|
||||
};
|
||||
}
|
||||
|
||||
function createSubFeaturePrivilege(privilegeId: string, capabilities: string[] = []) {
|
||||
return {
|
||||
id: privilegeId,
|
||||
name: `sub-feature privilege ${privilegeId}`,
|
||||
includeIn: 'none',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
app: [],
|
||||
ui: [...capabilities],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -27,14 +41,15 @@ describe('populateUICapabilities', () => {
|
|||
it('handles features with no registered capabilities', () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
new Feature({
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all'),
|
||||
all: createFeaturePrivilege(),
|
||||
read: createFeaturePrivilege(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {},
|
||||
|
@ -45,15 +60,16 @@ describe('populateUICapabilities', () => {
|
|||
it('augments the original uiCapabilities with registered feature capabilities', () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
new Feature({
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all', ['capability1', 'capability2']),
|
||||
all: createFeaturePrivilege(['capability1', 'capability2']),
|
||||
read: createFeaturePrivilege(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {},
|
||||
|
@ -67,18 +83,17 @@ describe('populateUICapabilities', () => {
|
|||
it('combines catalogue entries from multiple features', () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
new Feature({
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
catalogue: ['anotherFooEntry', 'anotherBarEntry'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz'),
|
||||
all: createFeaturePrivilege(['capability1', 'capability2']),
|
||||
read: createFeaturePrivilege(['capability3', 'capability4']),
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {
|
||||
|
@ -97,17 +112,16 @@ describe('populateUICapabilities', () => {
|
|||
it(`merges capabilities from all feature privileges`, () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
new Feature({
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz', ['capability1', 'capability5']),
|
||||
all: createFeaturePrivilege(['capability1', 'capability2']),
|
||||
read: createFeaturePrivilege(['capability3', 'capability4', 'capability5']),
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {},
|
||||
|
@ -121,44 +135,116 @@ describe('populateUICapabilities', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('supports merging multiple features with multiple privileges each', () => {
|
||||
it(`supports merging features with sub privileges`, () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
new Feature({
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz', ['capability1', 'capability5']),
|
||||
all: createFeaturePrivilege(['capability1', 'capability2']),
|
||||
read: createFeaturePrivilege(['capability3', 'capability4']),
|
||||
},
|
||||
},
|
||||
{
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'sub-feature-1',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
createSubFeaturePrivilege('privilege-1', ['capability5']),
|
||||
createSubFeaturePrivilege('privilege-2', ['capability6']),
|
||||
],
|
||||
} as SubFeaturePrivilegeGroupConfig,
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
createSubFeaturePrivilege('privilege-3', ['capability7']),
|
||||
createSubFeaturePrivilege('privilege-4', ['capability8']),
|
||||
],
|
||||
} as SubFeaturePrivilegeGroupConfig,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'sub-feature-2',
|
||||
privilegeGroups: [
|
||||
{
|
||||
name: 'Group Name',
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
createSubFeaturePrivilege('privilege-5', ['capability9', 'capability10']),
|
||||
],
|
||||
} as SubFeaturePrivilegeGroupConfig,
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
capability5: true,
|
||||
capability6: true,
|
||||
capability7: true,
|
||||
capability8: true,
|
||||
capability9: true,
|
||||
capability10: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('supports merging multiple features with multiple privileges each', () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
new Feature({
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
all: createFeaturePrivilege(['capability1', 'capability2']),
|
||||
read: createFeaturePrivilege(['capability3', 'capability4']),
|
||||
},
|
||||
}),
|
||||
new Feature({
|
||||
id: 'anotherNewFeature',
|
||||
name: 'another new feature',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
all: createFeaturePrivilege(['capability1', 'capability2']),
|
||||
read: createFeaturePrivilege(['capability3', 'capability4']),
|
||||
},
|
||||
},
|
||||
{
|
||||
}),
|
||||
new Feature({
|
||||
id: 'yetAnotherNewFeature',
|
||||
name: 'yet another new feature',
|
||||
navLinkId: 'yetAnotherNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('read', []),
|
||||
...createFeaturePrivilege('somethingInBetween', [
|
||||
'something1',
|
||||
'something2',
|
||||
'something3',
|
||||
]),
|
||||
all: createFeaturePrivilege(['capability1', 'capability2']),
|
||||
read: createFeaturePrivilege(['something1', 'something2', 'something3']),
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'sub-feature-1',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
createSubFeaturePrivilege('privilege-1', ['capability3']),
|
||||
createSubFeaturePrivilege('privilege-2', ['capability4']),
|
||||
],
|
||||
} as SubFeaturePrivilegeGroupConfig,
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
anotherNewFeature: {
|
||||
|
@ -173,11 +259,12 @@ describe('populateUICapabilities', () => {
|
|||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
capability5: true,
|
||||
},
|
||||
yetAnotherNewFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
something1: true,
|
||||
something2: true,
|
||||
something3: true,
|
||||
|
|
|
@ -39,7 +39,14 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities {
|
|||
};
|
||||
}
|
||||
|
||||
Object.values(feature.privileges).forEach(privilege => {
|
||||
const featurePrivileges = Object.values(feature.privileges ?? {});
|
||||
if (feature.subFeatures) {
|
||||
featurePrivileges.push(
|
||||
...feature.subFeatures.map(sf => sf.privilegeGroups.map(pg => pg.privileges)).flat(2)
|
||||
);
|
||||
}
|
||||
|
||||
featurePrivileges.forEach(privilege => {
|
||||
UIFeatureCapabilities[feature.id] = {
|
||||
...UIFeatureCapabilities[feature.id],
|
||||
...privilege.ui.reduce(
|
||||
|
|
|
@ -11,12 +11,15 @@ export const METRICS_FEATURE = {
|
|||
name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
order: 700,
|
||||
icon: 'metricsApp',
|
||||
navLinkId: 'metrics',
|
||||
app: ['infra', 'kibana'],
|
||||
catalogue: ['infraops'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['infra', 'kibana'],
|
||||
catalogue: ['infraops'],
|
||||
api: ['infra'],
|
||||
savedObject: {
|
||||
all: ['infrastructure-ui-source'],
|
||||
|
@ -25,6 +28,8 @@ export const METRICS_FEATURE = {
|
|||
ui: ['show', 'configureSource', 'save'],
|
||||
},
|
||||
read: {
|
||||
app: ['infra', 'kibana'],
|
||||
catalogue: ['infraops'],
|
||||
api: ['infra'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
@ -40,12 +45,15 @@ export const LOGS_FEATURE = {
|
|||
name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
order: 800,
|
||||
icon: 'logsApp',
|
||||
navLinkId: 'logs',
|
||||
app: ['infra', 'kibana'],
|
||||
catalogue: ['infralogging'],
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['infra', 'kibana'],
|
||||
catalogue: ['infralogging'],
|
||||
api: ['infra'],
|
||||
savedObject: {
|
||||
all: ['infrastructure-ui-source'],
|
||||
|
@ -54,6 +62,8 @@ export const LOGS_FEATURE = {
|
|||
ui: ['show', 'configureSource', 'save'],
|
||||
},
|
||||
read: {
|
||||
app: ['infra', 'kibana'],
|
||||
catalogue: ['infralogging'],
|
||||
api: ['infra'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
|
|
@ -88,6 +88,7 @@ export class IngestManagerPlugin implements Plugin {
|
|||
privileges: {
|
||||
all: {
|
||||
api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`],
|
||||
app: [PLUGIN_ID, 'kibana'],
|
||||
savedObject: {
|
||||
all: allSavedObjectTypes,
|
||||
read: [],
|
||||
|
@ -96,6 +97,7 @@ export class IngestManagerPlugin implements Plugin {
|
|||
},
|
||||
read: {
|
||||
api: [`${PLUGIN_ID}-read`],
|
||||
app: [PLUGIN_ID, 'kibana'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: allSavedObjectTypes,
|
||||
|
|
|
@ -70,12 +70,15 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
|
|||
defaultMessage: 'Machine Learning',
|
||||
}),
|
||||
icon: PLUGIN_ICON,
|
||||
order: 500,
|
||||
navLinkId: PLUGIN_ID,
|
||||
app: [PLUGIN_ID, 'kibana'],
|
||||
catalogue: [PLUGIN_ID],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
reserved: {
|
||||
privilege: {
|
||||
app: [PLUGIN_ID, 'kibana'],
|
||||
catalogue: [PLUGIN_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
|
|
@ -267,9 +267,11 @@ export class Plugin {
|
|||
navLinkId: 'monitoring',
|
||||
app: ['monitoring', 'kibana'],
|
||||
catalogue: ['monitoring'],
|
||||
privileges: {},
|
||||
privileges: null,
|
||||
reserved: {
|
||||
privilege: {
|
||||
app: ['monitoring', 'kibana'],
|
||||
catalogue: ['monitoring'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
|
|
@ -48,6 +48,11 @@ export interface SecurityLicenseFeatures {
|
|||
*/
|
||||
readonly allowRbac: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether we allow sub-feature privileges.
|
||||
*/
|
||||
readonly allowSubFeaturePrivileges: boolean;
|
||||
|
||||
/**
|
||||
* Describes the layout of the login form if it's displayed.
|
||||
*/
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('license features', function() {
|
|||
allowRoleFieldLevelSecurity: false,
|
||||
layout: 'error-es-unavailable',
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -40,6 +41,7 @@ describe('license features', function() {
|
|||
allowRoleFieldLevelSecurity: false,
|
||||
layout: 'error-xpack-unavailable',
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -62,6 +64,7 @@ describe('license features', function() {
|
|||
"allowRbac": false,
|
||||
"allowRoleDocumentLevelSecurity": false,
|
||||
"allowRoleFieldLevelSecurity": false,
|
||||
"allowSubFeaturePrivileges": false,
|
||||
"layout": "error-xpack-unavailable",
|
||||
"showLinks": false,
|
||||
"showLogin": true,
|
||||
|
@ -79,6 +82,7 @@ describe('license features', function() {
|
|||
"allowRbac": false,
|
||||
"allowRoleDocumentLevelSecurity": false,
|
||||
"allowRoleFieldLevelSecurity": false,
|
||||
"allowSubFeaturePrivileges": false,
|
||||
"showLinks": false,
|
||||
"showLogin": false,
|
||||
"showRoleMappingsManagement": false,
|
||||
|
@ -90,7 +94,7 @@ describe('license features', function() {
|
|||
}
|
||||
});
|
||||
|
||||
it('should show login page and other security elements, allow RBAC but forbid role mappings and document level security if license is basic.', () => {
|
||||
it('should show login page and other security elements, allow RBAC but forbid role mappings, DLS, and sub-feature privileges if license is basic.', () => {
|
||||
const mockRawLicense = licensingMock.createLicense({
|
||||
features: { security: { isEnabled: true, isAvailable: true } },
|
||||
});
|
||||
|
@ -108,6 +112,7 @@ describe('license features', function() {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRbac: true,
|
||||
allowSubFeaturePrivileges: false,
|
||||
});
|
||||
expect(getFeatureSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getFeatureSpy).toHaveBeenCalledWith('security');
|
||||
|
@ -129,10 +134,11 @@ describe('license features', function() {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow role mappings, but not DLS/FLS if license = gold', () => {
|
||||
it('should allow role mappings and sub-feature privileges, but not DLS/FLS if license = gold', () => {
|
||||
const mockRawLicense = licensingMock.createLicense({
|
||||
license: { mode: 'gold', type: 'gold' },
|
||||
features: { security: { isEnabled: true, isAvailable: true } },
|
||||
|
@ -149,10 +155,11 @@ describe('license features', function() {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRbac: true,
|
||||
allowSubFeaturePrivileges: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to login, allow RBAC, allow role mappings, and document level security if license >= platinum', () => {
|
||||
it('should allow to login, allow RBAC, role mappings, sub-feature privileges, and DLS if license >= platinum', () => {
|
||||
const mockRawLicense = licensingMock.createLicense({
|
||||
license: { mode: 'platinum', type: 'platinum' },
|
||||
features: { security: { isEnabled: true, isAvailable: true } },
|
||||
|
@ -169,6 +176,7 @@ describe('license features', function() {
|
|||
allowRoleDocumentLevelSecurity: true,
|
||||
allowRoleFieldLevelSecurity: true,
|
||||
allowRbac: true,
|
||||
allowSubFeaturePrivileges: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -74,6 +74,7 @@ export class SecurityLicenseService {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
layout:
|
||||
rawLicense !== undefined && !rawLicense?.isAvailable
|
||||
? 'error-xpack-unavailable'
|
||||
|
@ -90,16 +91,18 @@ export class SecurityLicenseService {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
};
|
||||
}
|
||||
|
||||
const showRoleMappingsManagement = rawLicense.hasAtLeast('gold');
|
||||
const isLicenseGoldOrBetter = rawLicense.hasAtLeast('gold');
|
||||
const isLicensePlatinumOrBetter = rawLicense.hasAtLeast('platinum');
|
||||
return {
|
||||
showLogin: true,
|
||||
allowLogin: true,
|
||||
showLinks: true,
|
||||
showRoleMappingsManagement,
|
||||
showRoleMappingsManagement: isLicenseGoldOrBetter,
|
||||
allowSubFeaturePrivileges: isLicenseGoldOrBetter,
|
||||
// Only platinum and trial licenses are compliant with field- and document-level security.
|
||||
allowRoleDocumentLevelSecurity: isLicensePlatinumOrBetter,
|
||||
allowRoleFieldLevelSecurity: isLicensePlatinumOrBetter,
|
||||
|
|
|
@ -8,8 +8,8 @@ export { ApiKey, ApiKeyToInvalidate } from './api_key';
|
|||
export { User, EditUser, getUserDisplayName } from './user';
|
||||
export { AuthenticatedUser, canUserChangePassword } from './authenticated_user';
|
||||
export { BuiltinESPrivileges } from './builtin_es_privileges';
|
||||
export { FeaturesPrivileges } from './features_privileges';
|
||||
export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges';
|
||||
export { FeaturesPrivileges } from './features_privileges';
|
||||
export {
|
||||
Role,
|
||||
RoleIndexPrivilege,
|
||||
|
@ -22,7 +22,6 @@ export {
|
|||
prepareRoleClone,
|
||||
getExtendedRoleDeprecationNotice,
|
||||
} from './role';
|
||||
export { KibanaPrivileges } from './kibana_privileges';
|
||||
export {
|
||||
InlineRoleTemplate,
|
||||
StoredRoleTemplate,
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* 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 { FeaturesPrivileges } from '../features_privileges';
|
||||
import { RawKibanaFeaturePrivileges } from '../raw_kibana_privileges';
|
||||
|
||||
export class KibanaFeaturePrivileges {
|
||||
constructor(private readonly featurePrivilegesMap: RawKibanaFeaturePrivileges) {}
|
||||
|
||||
public getAllPrivileges(): FeaturesPrivileges {
|
||||
return Object.entries(this.featurePrivilegesMap).reduce((acc, [featureId, privileges]) => {
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: Object.keys(privileges),
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
public getPrivileges(featureId: string): string[] {
|
||||
const featurePrivileges = this.featurePrivilegesMap[featureId];
|
||||
if (featurePrivileges == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(featurePrivileges);
|
||||
}
|
||||
|
||||
public getActions(featureId: string, privilege: string): string[] {
|
||||
if (!this.featurePrivilegesMap[featureId]) {
|
||||
return [];
|
||||
}
|
||||
return this.featurePrivilegesMap[featureId][privilege] || [];
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* 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 class KibanaGlobalPrivileges {
|
||||
constructor(private readonly globalPrivilegesMap: Record<string, string[]>) {}
|
||||
|
||||
public getAllPrivileges(): string[] {
|
||||
return Object.keys(this.globalPrivilegesMap);
|
||||
}
|
||||
|
||||
public getActions(privilege: string): string[] {
|
||||
return this.globalPrivilegesMap[privilege] || [];
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* 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 { RawKibanaPrivileges } from '../raw_kibana_privileges';
|
||||
import { KibanaFeaturePrivileges } from './feature_privileges';
|
||||
import { KibanaGlobalPrivileges } from './global_privileges';
|
||||
import { KibanaSpacesPrivileges } from './spaces_privileges';
|
||||
|
||||
export class KibanaPrivileges {
|
||||
constructor(private readonly rawKibanaPrivileges: RawKibanaPrivileges) {}
|
||||
|
||||
public getGlobalPrivileges() {
|
||||
return new KibanaGlobalPrivileges(this.rawKibanaPrivileges.global);
|
||||
}
|
||||
|
||||
public getSpacesPrivileges() {
|
||||
return new KibanaSpacesPrivileges(this.rawKibanaPrivileges.space);
|
||||
}
|
||||
|
||||
public getFeaturePrivileges() {
|
||||
return new KibanaFeaturePrivileges(this.rawKibanaPrivileges.features);
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* 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 class KibanaSpacesPrivileges {
|
||||
constructor(private readonly spacesPrivilegesMap: Record<string, string[]>) {}
|
||||
|
||||
public getAllPrivileges(): string[] {
|
||||
return Object.keys(this.spacesPrivilegesMap);
|
||||
}
|
||||
|
||||
public getActions(privilege: string): string[] {
|
||||
return this.spacesPrivilegesMap[privilege] || [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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 { Feature, FeatureConfig } from '../../../../../features/public';
|
||||
|
||||
export const createFeature = (
|
||||
config: Pick<FeatureConfig, 'id' | 'name' | 'subFeatures' | 'reserved' | 'privilegesTooltip'> & {
|
||||
excludeFromBaseAll?: boolean;
|
||||
excludeFromBaseRead?: boolean;
|
||||
}
|
||||
) => {
|
||||
const { excludeFromBaseAll, excludeFromBaseRead, ...rest } = config;
|
||||
return new Feature({
|
||||
icon: 'discoverApp',
|
||||
navLinkId: 'kibana:discover',
|
||||
app: [],
|
||||
catalogue: [],
|
||||
privileges: {
|
||||
all: {
|
||||
excludeFromBasePrivileges: excludeFromBaseAll,
|
||||
savedObject: {
|
||||
all: ['all-type'],
|
||||
read: ['read-type'],
|
||||
},
|
||||
ui: ['read-ui', 'all-ui', `read-${config.id}`, `all-${config.id}`],
|
||||
},
|
||||
read: {
|
||||
excludeFromBasePrivileges: excludeFromBaseRead,
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['read-type'],
|
||||
},
|
||||
ui: ['read-ui', `read-${config.id}`],
|
||||
},
|
||||
},
|
||||
...rest,
|
||||
});
|
||||
};
|
||||
|
||||
export const kibanaFeatures = [
|
||||
createFeature({
|
||||
id: 'no_sub_features',
|
||||
name: 'Feature 1: No Sub Features',
|
||||
}),
|
||||
createFeature({
|
||||
id: 'with_sub_features',
|
||||
name: 'Mutually Exclusive Sub Features',
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Cool Sub Feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cool_all',
|
||||
name: 'All',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: ['all-cool-type'],
|
||||
read: ['read-cool-type'],
|
||||
},
|
||||
ui: ['cool_read-ui', 'cool_all-ui'],
|
||||
},
|
||||
{
|
||||
id: 'cool_read',
|
||||
name: 'Read',
|
||||
includeIn: 'read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['read-cool-type'],
|
||||
},
|
||||
ui: ['cool_read-ui'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cool_toggle_1',
|
||||
name: 'Cool toggle 1',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['cool_toggle_1-ui'],
|
||||
},
|
||||
{
|
||||
id: 'cool_toggle_2',
|
||||
name: 'Cool toggle 2',
|
||||
includeIn: 'read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['cool_toggle_2-ui'],
|
||||
},
|
||||
{
|
||||
id: 'cool_excluded_toggle',
|
||||
name: 'Cool excluded toggle',
|
||||
includeIn: 'none',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['cool_excluded_toggle-ui'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
createFeature({
|
||||
id: 'with_excluded_sub_features',
|
||||
name: 'Excluded Sub Features',
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Excluded Sub Feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cool_toggle_1',
|
||||
name: 'Cool toggle 1',
|
||||
includeIn: 'none',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['cool_toggle_1-ui'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
createFeature({
|
||||
id: 'excluded_from_base',
|
||||
name: 'Excluded from base',
|
||||
excludeFromBaseAll: true,
|
||||
excludeFromBaseRead: true,
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Cool Sub Feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cool_all',
|
||||
name: 'All',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['cool_read-ui', 'cool_all-ui'],
|
||||
},
|
||||
{
|
||||
id: 'cool_read',
|
||||
name: 'Read',
|
||||
includeIn: 'read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['cool_read-ui'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cool_toggle_1',
|
||||
name: 'Cool toggle 2',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['cool_toggle_1-ui'],
|
||||
},
|
||||
{
|
||||
id: 'cool_toggle_2',
|
||||
name: 'Cool toggle 2',
|
||||
includeIn: 'read',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['cool_toggle_2-ui'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { Actions } from '../../../../server/authorization';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { privilegesFactory } from '../../../../server/authorization/privileges';
|
||||
import { Feature } from '../../../../../features/public';
|
||||
import { KibanaPrivileges } from '../model';
|
||||
import { SecurityLicenseFeatures } from '../../..';
|
||||
|
||||
export const createRawKibanaPrivileges = (
|
||||
features: Feature[],
|
||||
{ allowSubFeaturePrivileges = true } = {}
|
||||
) => {
|
||||
const featuresService = {
|
||||
getFeatures: () => features,
|
||||
};
|
||||
|
||||
const licensingService = {
|
||||
getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures),
|
||||
};
|
||||
|
||||
return privilegesFactory(
|
||||
new Actions('unit_test_version'),
|
||||
featuresService,
|
||||
licensingService
|
||||
).get();
|
||||
};
|
||||
|
||||
export const createKibanaPrivileges = (
|
||||
features: Feature[],
|
||||
{ allowSubFeaturePrivileges = true } = {}
|
||||
) => {
|
||||
return new KibanaPrivileges(
|
||||
createRawKibanaPrivileges(features, { allowSubFeaturePrivileges }),
|
||||
features
|
||||
);
|
||||
};
|
|
@ -10,16 +10,10 @@ import { act } from '@testing-library/react';
|
|||
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { Feature } from '../../../../../features/public';
|
||||
// These modules should be moved into a common directory
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { Actions } from '../../../../server/authorization/actions';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { privilegesFactory } from '../../../../server/authorization/privileges';
|
||||
import { Role } from '../../../../common/model';
|
||||
import { DocumentationLinksService } from '../documentation_links';
|
||||
import { EditRolePage } from './edit_role_page';
|
||||
import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section';
|
||||
import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section';
|
||||
|
||||
import { TransformErrorSection } from './privileges/kibana/transform_error_section';
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
|
@ -28,10 +22,12 @@ import { licenseMock } from '../../../../common/licensing/index.mock';
|
|||
import { userAPIClientMock } from '../../users/index.mock';
|
||||
import { rolesAPIClientMock, indicesAPIClientMock, privilegesAPIClientMock } from '../index.mock';
|
||||
import { Space } from '../../../../../spaces/public';
|
||||
import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section';
|
||||
import { createRawKibanaPrivileges } from '../__fixtures__/kibana_privileges';
|
||||
|
||||
const buildFeatures = () => {
|
||||
return [
|
||||
{
|
||||
new Feature({
|
||||
id: 'feature1',
|
||||
name: 'Feature 1',
|
||||
icon: 'addDataApp',
|
||||
|
@ -45,9 +41,17 @@ const buildFeatures = () => {
|
|||
read: [],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
app: ['feature1App'],
|
||||
ui: ['feature1-ui'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
}),
|
||||
new Feature({
|
||||
id: 'feature2',
|
||||
name: 'Feature 2',
|
||||
icon: 'addDataApp',
|
||||
|
@ -61,17 +65,19 @@ const buildFeatures = () => {
|
|||
read: ['config'],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
app: ['feature2App'],
|
||||
ui: ['feature2-ui'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['config'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
] as Feature[];
|
||||
};
|
||||
|
||||
const buildRawKibanaPrivileges = () => {
|
||||
return privilegesFactory(new Actions('unit_test_version'), {
|
||||
getFeatures: () => buildFeatures(),
|
||||
}).get();
|
||||
};
|
||||
|
||||
const buildBuiltinESPrivileges = () => {
|
||||
return {
|
||||
cluster: ['all', 'manage', 'monitor'],
|
||||
|
@ -144,7 +150,7 @@ function getProps({
|
|||
userAPIClient.getUsers.mockResolvedValue([]);
|
||||
|
||||
const privilegesAPIClient = privilegesAPIClientMock.create();
|
||||
privilegesAPIClient.getAll.mockResolvedValue(buildRawKibanaPrivileges());
|
||||
privilegesAPIClient.getAll.mockResolvedValue(createRawKibanaPrivileges(buildFeatures()));
|
||||
privilegesAPIClient.getBuiltIn.mockResolvedValue(buildBuiltinESPrivileges());
|
||||
|
||||
const license = licenseMock.create();
|
||||
|
@ -156,10 +162,6 @@ function getProps({
|
|||
const { fatalErrors } = coreMock.createSetup();
|
||||
const { http, docLinks, notifications } = coreMock.createStart();
|
||||
http.get.mockImplementation(async (path: any) => {
|
||||
if (path === '/api/features') {
|
||||
return buildFeatures();
|
||||
}
|
||||
|
||||
if (path === '/api/spaces/space') {
|
||||
return buildSpaces();
|
||||
}
|
||||
|
@ -175,6 +177,7 @@ function getProps({
|
|||
privilegesAPIClient,
|
||||
rolesAPIClient,
|
||||
userAPIClient,
|
||||
getFeatures: () => Promise.resolve(buildFeatures()),
|
||||
notifications,
|
||||
docLinks: new DocumentationLinksService(docLinks),
|
||||
fatalErrors,
|
||||
|
@ -200,10 +203,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1);
|
||||
expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1);
|
||||
|
@ -226,10 +226,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0);
|
||||
expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1);
|
||||
|
@ -240,10 +237,7 @@ describe('<EditRolePage />', () => {
|
|||
it('can render when creating a new role', async () => {
|
||||
const wrapper = mountWithIntl(<EditRolePage {...getProps({ action: 'edit' })} />);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0);
|
||||
|
@ -275,10 +269,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0);
|
||||
|
@ -301,10 +292,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0);
|
||||
|
||||
|
@ -333,10 +321,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(TransformErrorSection)).toHaveLength(1);
|
||||
expectReadOnlyFormButtons(wrapper);
|
||||
|
@ -360,10 +345,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1);
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
|
@ -387,10 +369,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0);
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
|
@ -403,10 +382,7 @@ describe('<EditRolePage />', () => {
|
|||
<EditRolePage {...getProps({ action: 'edit', spacesEnabled: false })} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
expectSaveFormButtons(wrapper);
|
||||
|
@ -438,10 +414,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1);
|
||||
expectSaveFormButtons(wrapper);
|
||||
|
@ -464,10 +437,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0);
|
||||
|
||||
|
@ -497,10 +467,7 @@ describe('<EditRolePage />', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(TransformErrorSection)).toHaveLength(1);
|
||||
expectReadOnlyFormButtons(wrapper);
|
||||
|
@ -522,10 +489,7 @@ describe('<EditRolePage />', () => {
|
|||
|
||||
const wrapper = mountWithIntl(<EditRolePage {...{ ...getProps({ action: 'edit' }), http }} />);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0);
|
||||
|
@ -540,13 +504,17 @@ describe('<EditRolePage />', () => {
|
|||
<EditRolePage {...{ ...getProps({ action: 'edit' }), indexPatterns }} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
await waitForRender(wrapper);
|
||||
|
||||
expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0);
|
||||
expectSaveFormButtons(wrapper);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForRender(wrapper: ReactWrapper<any>) {
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -37,11 +37,11 @@ import {
|
|||
IHttpFetchError,
|
||||
NotificationsStart,
|
||||
} from 'src/core/public';
|
||||
import { FeaturesPluginStart } from '../../../../../features/public';
|
||||
import { Feature } from '../../../../../features/common';
|
||||
import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public';
|
||||
import { Space } from '../../../../../spaces/public';
|
||||
import { Feature } from '../../../../../features/public';
|
||||
import {
|
||||
KibanaPrivileges,
|
||||
RawKibanaPrivileges,
|
||||
Role,
|
||||
BuiltinESPrivileges,
|
||||
|
@ -64,6 +64,7 @@ import { DocumentationLinksService } from '../documentation_links';
|
|||
import { IndicesAPIClient } from '../indices_api_client';
|
||||
import { RolesAPIClient } from '../roles_api_client';
|
||||
import { PrivilegesAPIClient } from '../privileges_api_client';
|
||||
import { KibanaPrivileges } from '../model';
|
||||
|
||||
interface Props {
|
||||
action: 'edit' | 'clone';
|
||||
|
@ -73,6 +74,7 @@ interface Props {
|
|||
indicesAPIClient: PublicMethodsOf<IndicesAPIClient>;
|
||||
rolesAPIClient: PublicMethodsOf<RolesAPIClient>;
|
||||
privilegesAPIClient: PublicMethodsOf<PrivilegesAPIClient>;
|
||||
getFeatures: FeaturesPluginStart['getFeatures'];
|
||||
docLinks: DocumentationLinksService;
|
||||
http: HttpStart;
|
||||
license: SecurityLicense;
|
||||
|
@ -231,11 +233,13 @@ function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup, spacesEnabled
|
|||
return spaces;
|
||||
}
|
||||
|
||||
function useFeatures(http: HttpStart, fatalErrors: FatalErrorsSetup) {
|
||||
function useFeatures(
|
||||
getFeatures: FeaturesPluginStart['getFeatures'],
|
||||
fatalErrors: FatalErrorsSetup
|
||||
) {
|
||||
const [features, setFeatures] = useState<Feature[] | null>(null);
|
||||
useEffect(() => {
|
||||
http
|
||||
.get('/api/features')
|
||||
getFeatures()
|
||||
.catch((err: IHttpFetchError) => {
|
||||
// Currently, the `/api/features` endpoint effectively requires the "Global All" kibana privilege (e.g., what
|
||||
// the `kibana_user` grants), because it returns information about all registered features (#35841). It's
|
||||
|
@ -246,14 +250,15 @@ function useFeatures(http: HttpStart, fatalErrors: FatalErrorsSetup) {
|
|||
// 404 here, and respond in a way that still allows the UI to render itself.
|
||||
const unauthorizedForFeatures = err.response?.status === 404;
|
||||
if (unauthorizedForFeatures) {
|
||||
return [];
|
||||
return [] as Feature[];
|
||||
}
|
||||
|
||||
fatalErrors.add(err);
|
||||
throw err;
|
||||
})
|
||||
.then(setFeatures);
|
||||
}, [http, fatalErrors]);
|
||||
.then(retrievedFeatures => {
|
||||
setFeatures(retrievedFeatures);
|
||||
});
|
||||
}, [fatalErrors, getFeatures]);
|
||||
|
||||
return features;
|
||||
}
|
||||
|
@ -268,6 +273,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
rolesAPIClient,
|
||||
indicesAPIClient,
|
||||
privilegesAPIClient,
|
||||
getFeatures,
|
||||
http,
|
||||
roleName,
|
||||
action,
|
||||
|
@ -287,7 +293,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications);
|
||||
const privileges = usePrivileges(privilegesAPIClient, fatalErrors);
|
||||
const spaces = useSpaces(http, fatalErrors, spacesEnabled);
|
||||
const features = useFeatures(http, fatalErrors);
|
||||
const features = useFeatures(getFeatures, fatalErrors);
|
||||
const [role, setRole] = useRole(
|
||||
rolesAPIClient,
|
||||
fatalErrors,
|
||||
|
@ -425,11 +431,11 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
<div>
|
||||
<EuiSpacer />
|
||||
<KibanaPrivilegesRegion
|
||||
kibanaPrivileges={new KibanaPrivileges(kibanaPrivileges)}
|
||||
kibanaPrivileges={new KibanaPrivileges(kibanaPrivileges, features)}
|
||||
spaces={spaces}
|
||||
spacesEnabled={spacesEnabled}
|
||||
features={features}
|
||||
uiCapabilities={uiCapabilities}
|
||||
canCustomizeSubFeaturePrivileges={license.getFeatures().allowSubFeaturePrivileges}
|
||||
editable={!isRoleReadOnly}
|
||||
role={role}
|
||||
onChange={onRoleChange}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { hasAssignedFeaturePrivileges, isGlobalPrivilegeDefinition } from './privilege_utils';
|
||||
import { isGlobalPrivilegeDefinition } from './privilege_utils';
|
||||
|
||||
describe('isGlobalPrivilegeDefinition', () => {
|
||||
it('returns true if no spaces are defined', () => {
|
||||
|
@ -47,39 +47,3 @@ describe('isGlobalPrivilegeDefinition', () => {
|
|||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasAssignedFeaturePrivileges', () => {
|
||||
it('returns false if no feature privileges are defined', () => {
|
||||
expect(
|
||||
hasAssignedFeaturePrivileges({
|
||||
spaces: [],
|
||||
base: [],
|
||||
feature: {},
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false if feature privileges are defined but not assigned', () => {
|
||||
expect(
|
||||
hasAssignedFeaturePrivileges({
|
||||
spaces: [],
|
||||
base: [],
|
||||
feature: {
|
||||
foo: [],
|
||||
},
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true if feature privileges are defined and assigned', () => {
|
||||
expect(
|
||||
hasAssignedFeaturePrivileges({
|
||||
spaces: [],
|
||||
base: [],
|
||||
feature: {
|
||||
foo: ['all'],
|
||||
},
|
||||
})
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,12 +16,3 @@ export function isGlobalPrivilegeDefinition(privilegeSpec: RoleKibanaPrivilege):
|
|||
}
|
||||
return privilegeSpec.spaces.includes('*');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the passed privilege spec defines feature privileges.
|
||||
* @param privilegeSpec
|
||||
*/
|
||||
export function hasAssignedFeaturePrivileges(privilegeSpec: RoleKibanaPrivilege): boolean {
|
||||
const featureKeys = Object.keys(privilegeSpec.feature);
|
||||
return featureKeys.length > 0 && featureKeys.some(key => privilegeSpec.feature[key].length > 0);
|
||||
}
|
||||
|
|
|
@ -5,33 +5,17 @@ exports[`<KibanaPrivileges> renders without crashing 1`] = `
|
|||
iconType="logoKibana"
|
||||
title="Kibana"
|
||||
>
|
||||
<InjectIntl(SpaceAwarePrivilegeSectionUI)
|
||||
<SpaceAwarePrivilegeSection
|
||||
canCustomizeSubFeaturePrivileges={true}
|
||||
editable={true}
|
||||
features={Array []}
|
||||
kibanaPrivileges={
|
||||
KibanaPrivileges {
|
||||
"rawKibanaPrivileges": Object {
|
||||
"features": Object {},
|
||||
"global": Object {},
|
||||
"reserved": Object {},
|
||||
"space": Object {},
|
||||
},
|
||||
"feature": Map {},
|
||||
"global": Map {},
|
||||
"spaces": Map {},
|
||||
}
|
||||
}
|
||||
onChange={[MockFunction]}
|
||||
privilegeCalculatorFactory={
|
||||
KibanaPrivilegeCalculatorFactory {
|
||||
"kibanaPrivileges": KibanaPrivileges {
|
||||
"rawKibanaPrivileges": Object {
|
||||
"features": Object {},
|
||||
"global": Object {},
|
||||
"reserved": Object {},
|
||||
"space": Object {},
|
||||
},
|
||||
},
|
||||
"rankedFeaturePrivileges": Object {},
|
||||
}
|
||||
}
|
||||
role={
|
||||
Object {
|
||||
"elasticsearch": Object {
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { ReactWrapper } from 'enzyme';
|
||||
|
||||
import {
|
||||
EuiTableRow,
|
||||
EuiCheckbox,
|
||||
EuiCheckboxProps,
|
||||
EuiButtonGroup,
|
||||
EuiButtonGroupProps,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { SubFeatureForm } from '../sub_feature_form';
|
||||
|
||||
export function getDisplayedFeaturePrivileges(wrapper: ReactWrapper<any>) {
|
||||
const allExpanderButtons = findTestSubject(wrapper, 'expandFeaturePrivilegeRow');
|
||||
allExpanderButtons.forEach(button => button.simulate('click'));
|
||||
|
||||
// each expanded row renders its own `EuiTableRow`, so there are 2 rows
|
||||
// for each feature: one for the primary feature privilege, and one for the sub privilege form
|
||||
const rows = wrapper.find(EuiTableRow);
|
||||
|
||||
return rows.reduce((acc, row) => {
|
||||
const subFeaturePrivileges = [];
|
||||
const subFeatureForm = row.find(SubFeatureForm);
|
||||
if (subFeatureForm.length > 0) {
|
||||
const { featureId } = subFeatureForm.props();
|
||||
const independentPrivileges = (subFeatureForm.find(EuiCheckbox) as ReactWrapper<
|
||||
EuiCheckboxProps
|
||||
>).reduce((acc2, checkbox) => {
|
||||
const { id: privilegeId, checked } = checkbox.props();
|
||||
return checked ? [...acc2, privilegeId] : acc2;
|
||||
}, [] as string[]);
|
||||
|
||||
const mutuallyExclusivePrivileges = (subFeatureForm.find(EuiButtonGroup) as ReactWrapper<
|
||||
EuiButtonGroupProps
|
||||
>).reduce((acc2, subPrivButtonGroup) => {
|
||||
const { idSelected: selectedSubPrivilege } = subPrivButtonGroup.props();
|
||||
return selectedSubPrivilege && selectedSubPrivilege !== 'none'
|
||||
? [...acc2, selectedSubPrivilege]
|
||||
: acc2;
|
||||
}, [] as string[]);
|
||||
|
||||
subFeaturePrivileges.push(...independentPrivileges, ...mutuallyExclusivePrivileges);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: {
|
||||
...acc[featureId],
|
||||
subFeaturePrivileges,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const buttonGroup = row.find(EuiButtonGroup);
|
||||
const { name, idSelected } = buttonGroup.props();
|
||||
expect(name).toBeDefined();
|
||||
expect(idSelected).toBeDefined();
|
||||
|
||||
const featureId = name!.substr(`featurePrivilege_`.length);
|
||||
const primaryFeaturePrivilege = idSelected!.substr(`${featureId}_`.length);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: {
|
||||
...acc[featureId],
|
||||
primaryFeaturePrivilege,
|
||||
},
|
||||
};
|
||||
}
|
||||
}, {} as Record<string, { primaryFeaturePrivilege: string; subFeaturePrivileges: string[] }>);
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FeatureTable can render without spaces 1`] = `
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "feature",
|
||||
"name": "Feature",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "privilege",
|
||||
"name": <span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Privilege"
|
||||
id="xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
<ChangeAllPrivilegesControl
|
||||
onChange={[Function]}
|
||||
privileges={
|
||||
Array [
|
||||
"all",
|
||||
"read",
|
||||
"none",
|
||||
]
|
||||
}
|
||||
/>
|
||||
</span>,
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={Array []}
|
||||
responsive={false}
|
||||
tableLayout="fixed"
|
||||
/>
|
||||
`;
|
|
@ -7,9 +7,11 @@ import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@e
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { KibanaPrivilege } from '../../../../model';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
interface Props {
|
||||
onChange: (privilege: string) => void;
|
||||
privileges: string[];
|
||||
privileges: KibanaPrivilege[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -24,7 +26,11 @@ export class ChangeAllPrivilegesControl extends Component<Props, State> {
|
|||
|
||||
public render() {
|
||||
const button = (
|
||||
<EuiLink onClick={this.onButtonClick} className={'secPrivilegeFeatureChangeAllLink'}>
|
||||
<EuiLink
|
||||
onClick={this.onButtonClick}
|
||||
className={'secPrivilegeFeatureChangeAllLink'}
|
||||
data-test-subj="changeAllPrivilegesButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.changeAllPrivilegesLink"
|
||||
defaultMessage="(change all)"
|
||||
|
@ -35,17 +41,31 @@ export class ChangeAllPrivilegesControl extends Component<Props, State> {
|
|||
const items = this.props.privileges.map(privilege => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key={privilege}
|
||||
key={privilege.id}
|
||||
data-test-subj={`changeAllPrivileges-${privilege.id}`}
|
||||
onClick={() => {
|
||||
this.onSelectPrivilege(privilege);
|
||||
this.onSelectPrivilege(privilege.id);
|
||||
}}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{_.capitalize(privilege)}
|
||||
{_.capitalize(privilege.id)}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
items.push(
|
||||
<EuiContextMenuItem
|
||||
key={NO_PRIVILEGE_VALUE}
|
||||
data-test-subj={`changeAllPrivileges-${NO_PRIVILEGE_VALUE}`}
|
||||
onClick={() => {
|
||||
this.onSelectPrivilege(NO_PRIVILEGE_VALUE);
|
||||
}}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{_.capitalize(NO_PRIVILEGE_VALUE)}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id={'changeAllFeaturePrivilegesPopover'}
|
||||
|
|
|
@ -3,145 +3,765 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator';
|
||||
import { FeatureTable } from './feature_table';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Feature, SubFeatureConfig } from '../../../../../../../../features/public';
|
||||
import { kibanaFeatures, createFeature } from '../../../../__fixtures__/kibana_features';
|
||||
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
import { getDisplayedFeaturePrivileges } from './__fixtures__';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { FeatureTableExpandedRow } from './feature_table_expanded_row';
|
||||
|
||||
const defaultPrivilegeDefinition = new KibanaPrivileges({
|
||||
global: {
|
||||
all: ['api:/*', 'ui:/*'],
|
||||
read: ['ui:/feature1/foo', 'ui:/feature2/foo'],
|
||||
},
|
||||
space: {
|
||||
all: ['api:/*', 'ui:/*'],
|
||||
read: ['ui:/feature1/foo', 'ui:/feature2/foo'],
|
||||
},
|
||||
features: {
|
||||
feature1: {
|
||||
all: ['ui:/feature1/foo', 'ui:/feature1/bar'],
|
||||
read: ['ui:/feature1/foo'],
|
||||
},
|
||||
feature2: {
|
||||
all: ['ui:/feature2/foo', 'api:/feature2/bar'],
|
||||
read: ['ui:/feature2/foo'],
|
||||
},
|
||||
feature3: {
|
||||
all: ['ui:/feature3/foo'],
|
||||
},
|
||||
feature4: {
|
||||
all: ['somethingObsecure:/foo'],
|
||||
},
|
||||
},
|
||||
reserved: {},
|
||||
});
|
||||
|
||||
interface BuildRoleOpts {
|
||||
globalPrivilege?: {
|
||||
base: string[];
|
||||
feature: FeaturesPrivileges;
|
||||
const createRole = (kibana: Role['kibana'] = []): Role => {
|
||||
return {
|
||||
name: 'my_role',
|
||||
elasticsearch: { cluster: [], run_as: [], indices: [] },
|
||||
kibana,
|
||||
};
|
||||
spacesPrivileges?: Array<{
|
||||
spaces: string[];
|
||||
base: string[];
|
||||
feature: FeaturesPrivileges;
|
||||
}>;
|
||||
}
|
||||
const buildRole = (options: BuildRoleOpts = {}) => {
|
||||
const role: Role = {
|
||||
name: 'unit test role',
|
||||
elasticsearch: {
|
||||
indices: [],
|
||||
cluster: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [],
|
||||
};
|
||||
|
||||
if (options.globalPrivilege) {
|
||||
role.kibana.push({
|
||||
spaces: ['*'],
|
||||
...options.globalPrivilege,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.spacesPrivileges) {
|
||||
role.kibana.push(...options.spacesPrivileges);
|
||||
}
|
||||
|
||||
return role;
|
||||
};
|
||||
|
||||
const buildFeatures = () => {
|
||||
return [];
|
||||
interface TestConfig {
|
||||
features: Feature[];
|
||||
role: Role;
|
||||
privilegeIndex: number;
|
||||
calculateDisplayedPrivileges: boolean;
|
||||
canCustomizeSubFeaturePrivileges: boolean;
|
||||
}
|
||||
const setup = (config: TestConfig) => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(config.features, {
|
||||
allowSubFeaturePrivileges: config.canCustomizeSubFeaturePrivileges,
|
||||
});
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, config.role);
|
||||
const onChange = jest.fn();
|
||||
const onChangeAll = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTable
|
||||
role={config.role}
|
||||
privilegeCalculator={calculator}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
onChange={onChange}
|
||||
onChangeAll={onChangeAll}
|
||||
canCustomizeSubFeaturePrivileges={config.canCustomizeSubFeaturePrivileges}
|
||||
privilegeIndex={config.privilegeIndex}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = config.calculateDisplayedPrivileges
|
||||
? getDisplayedFeaturePrivileges(wrapper)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
onChange,
|
||||
onChangeAll,
|
||||
displayedPrivileges,
|
||||
};
|
||||
};
|
||||
|
||||
describe('FeatureTable', () => {
|
||||
it('can render without spaces', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['marketing', 'default'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
[true, false].forEach(canCustomizeSubFeaturePrivileges => {
|
||||
describe(`with sub feature privileges ${
|
||||
canCustomizeSubFeaturePrivileges ? 'allowed' : 'disallowed'
|
||||
}`, () => {
|
||||
it('renders with no granted privileges for an empty role', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: [],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
]);
|
||||
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders with all included privileges granted at the space when space base privilege is "all"', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
]);
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 1,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges,
|
||||
});
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
...(canCustomizeSubFeaturePrivileges
|
||||
? {
|
||||
subFeaturePrivileges: [
|
||||
'with_sub_features_cool_toggle_1',
|
||||
'with_sub_features_cool_toggle_2',
|
||||
'cool_all',
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the most permissive primary feature privilege when multiple are assigned', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['read', 'minimal_all', 'all', 'minimal_read'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 1,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
...(canCustomizeSubFeaturePrivileges
|
||||
? {
|
||||
subFeaturePrivileges: [
|
||||
'with_sub_features_cool_toggle_1',
|
||||
'with_sub_features_cool_toggle_2',
|
||||
'cool_all',
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('allows all feature privileges to be toggled via "change all"', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
]);
|
||||
const { wrapper, onChangeAll } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: false,
|
||||
canCustomizeSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
findTestSubject(wrapper, 'changeAllPrivilegesButton').simulate('click');
|
||||
findTestSubject(wrapper, 'changeAllPrivileges-read').simulate('click');
|
||||
|
||||
expect(onChangeAll).toHaveBeenCalledWith(['read']);
|
||||
});
|
||||
|
||||
it('allows all feature privileges to be unassigned via "change all"', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
no_sub_features: ['read'],
|
||||
with_excluded_sub_features: ['all', 'something else'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { wrapper, onChangeAll } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: false,
|
||||
canCustomizeSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
findTestSubject(wrapper, 'changeAllPrivilegesButton').simulate('click');
|
||||
findTestSubject(wrapper, 'changeAllPrivileges-none').simulate('click');
|
||||
|
||||
expect(onChangeAll).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the most permissive sub-feature privilege when multiple are assigned in a mutually-exclusive group', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read', 'cool_all', 'cool_read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 1,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
|
||||
const calculator = new KibanaPrivilegeCalculatorFactory(defaultPrivilegeDefinition).getInstance(
|
||||
role
|
||||
);
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<FeatureTable
|
||||
role={role}
|
||||
kibanaPrivileges={defaultPrivilegeDefinition}
|
||||
calculatedPrivileges={calculator.calculateEffectivePrivileges()[0]}
|
||||
allowedPrivileges={calculator.calculateAllowedPrivileges()[0]}
|
||||
rankedFeaturePrivileges={{
|
||||
feature1: ['all', 'read'],
|
||||
feature2: ['all', 'read'],
|
||||
feature3: ['all'],
|
||||
feature4: ['all'],
|
||||
}}
|
||||
features={buildFeatures()}
|
||||
onChange={jest.fn()}
|
||||
onChangeAll={jest.fn()}
|
||||
spacesIndex={0}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'read',
|
||||
subFeaturePrivileges: ['cool_all'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can render for a specific spaces entry', () => {
|
||||
const role = buildRole();
|
||||
const calculator = new KibanaPrivilegeCalculatorFactory(defaultPrivilegeDefinition).getInstance(
|
||||
role
|
||||
);
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTable
|
||||
role={role}
|
||||
kibanaPrivileges={defaultPrivilegeDefinition}
|
||||
calculatedPrivileges={calculator.calculateEffectivePrivileges()[0]}
|
||||
allowedPrivileges={calculator.calculateAllowedPrivileges()[0]}
|
||||
rankedFeaturePrivileges={{
|
||||
feature1: ['all', 'read'],
|
||||
feature2: ['all', 'read'],
|
||||
feature3: ['all'],
|
||||
feature4: ['all'],
|
||||
}}
|
||||
features={buildFeatures()}
|
||||
onChange={jest.fn()}
|
||||
onChangeAll={jest.fn()}
|
||||
spacesIndex={-1}
|
||||
/>
|
||||
);
|
||||
it('renders a row expander only for features with sub-features', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
]);
|
||||
const { wrapper } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 1,
|
||||
calculateDisplayedPrivileges: false,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
|
||||
kibanaFeatures.forEach(feature => {
|
||||
const rowExpander = findTestSubject(wrapper, `expandFeaturePrivilegeRow-${feature.id}`);
|
||||
if (!feature.subFeatures || feature.subFeatures.length === 0) {
|
||||
expect(rowExpander).toHaveLength(0);
|
||||
} else {
|
||||
expect(rowExpander).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the <FeatureTableExpandedRow> when the row is expanded', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
]);
|
||||
const { wrapper } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 1,
|
||||
calculateDisplayedPrivileges: false,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
|
||||
expect(wrapper.find(FeatureTableExpandedRow)).toHaveLength(0);
|
||||
|
||||
findTestSubject(wrapper, 'expandFeaturePrivilegeRow')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.find(FeatureTableExpandedRow)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders with sub-feature privileges granted when primary feature privilege is "all"', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
unit_test: ['all'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const feature = createFeature({
|
||||
id: 'unit_test',
|
||||
name: 'Unit Test Feature',
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Some Sub Feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub-toggle-1',
|
||||
name: 'Sub Toggle 1',
|
||||
includeIn: 'all',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-toggle-1'],
|
||||
},
|
||||
{
|
||||
id: 'sub-toggle-2',
|
||||
name: 'Sub Toggle 2',
|
||||
includeIn: 'read',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-toggle-2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub-option-1',
|
||||
name: 'Sub Option 1',
|
||||
includeIn: 'all',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-option-1'],
|
||||
},
|
||||
{
|
||||
id: 'sub-toggle-2',
|
||||
name: 'Sub Toggle 2',
|
||||
includeIn: 'read',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-option-2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as SubFeatureConfig[],
|
||||
});
|
||||
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: [feature],
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
expect(displayedPrivileges).toEqual({
|
||||
unit_test: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
subFeaturePrivileges: ['unit_test_sub-toggle-1', 'unit_test_sub-toggle-2', 'sub-option-1'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders with some sub-feature privileges granted when primary feature privilege is "read"', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
unit_test: ['read'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const feature = createFeature({
|
||||
id: 'unit_test',
|
||||
name: 'Unit Test Feature',
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Some Sub Feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub-toggle-1',
|
||||
name: 'Sub Toggle 1',
|
||||
includeIn: 'all',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-toggle-1'],
|
||||
},
|
||||
{
|
||||
id: 'sub-toggle-2',
|
||||
name: 'Sub Toggle 2',
|
||||
includeIn: 'read',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-toggle-2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub-option-1',
|
||||
name: 'Sub Option 1',
|
||||
includeIn: 'all',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-option-1'],
|
||||
},
|
||||
{
|
||||
id: 'sub-toggle-2',
|
||||
name: 'Sub Toggle 2',
|
||||
includeIn: 'read',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-option-2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as SubFeatureConfig[],
|
||||
});
|
||||
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: [feature],
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
expect(displayedPrivileges).toEqual({
|
||||
unit_test: {
|
||||
primaryFeaturePrivilege: 'read',
|
||||
subFeaturePrivileges: ['unit_test_sub-toggle-2', 'sub-toggle-2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders with excluded sub-feature privileges not granted when primary feature privilege is "all"', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
unit_test: ['all'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const feature = createFeature({
|
||||
id: 'unit_test',
|
||||
name: 'Unit Test Feature',
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Some Sub Feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub-toggle-1',
|
||||
name: 'Sub Toggle 1',
|
||||
includeIn: 'none',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-toggle-1'],
|
||||
},
|
||||
{
|
||||
id: 'sub-toggle-2',
|
||||
name: 'Sub Toggle 2',
|
||||
includeIn: 'none',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-toggle-2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub-option-1',
|
||||
name: 'Sub Option 1',
|
||||
includeIn: 'all',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-option-1'],
|
||||
},
|
||||
{
|
||||
id: 'sub-toggle-2',
|
||||
name: 'Sub Toggle 2',
|
||||
includeIn: 'read',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-option-2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as SubFeatureConfig[],
|
||||
});
|
||||
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: [feature],
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
expect(displayedPrivileges).toEqual({
|
||||
unit_test: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
subFeaturePrivileges: ['unit_test_sub-toggle-2', 'sub-option-1'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders with excluded sub-feature privileges granted when explicitly assigned', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
unit_test: ['all', 'sub-toggle-1'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const feature = createFeature({
|
||||
id: 'unit_test',
|
||||
name: 'Unit Test Feature',
|
||||
subFeatures: [
|
||||
{
|
||||
name: 'Some Sub Feature',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub-toggle-1',
|
||||
name: 'Sub Toggle 1',
|
||||
includeIn: 'none',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-toggle-1'],
|
||||
},
|
||||
{
|
||||
id: 'sub-toggle-2',
|
||||
name: 'Sub Toggle 2',
|
||||
includeIn: 'none',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-toggle-2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupType: 'mutually_exclusive',
|
||||
privileges: [
|
||||
{
|
||||
id: 'sub-option-1',
|
||||
name: 'Sub Option 1',
|
||||
includeIn: 'all',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-option-1'],
|
||||
},
|
||||
{
|
||||
id: 'sub-toggle-2',
|
||||
name: 'Sub Toggle 2',
|
||||
includeIn: 'read',
|
||||
savedObject: { all: [], read: [] },
|
||||
ui: ['sub-option-2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as SubFeatureConfig[],
|
||||
});
|
||||
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: [feature],
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
expect(displayedPrivileges).toEqual({
|
||||
unit_test: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
subFeaturePrivileges: ['unit_test_sub-toggle-1', 'unit_test_sub-toggle-2', 'sub-option-1'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders with all included sub-feature privileges granted at the space when primary feature privileges are granted', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 1,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
subFeaturePrivileges: [
|
||||
'with_sub_features_cool_toggle_1',
|
||||
'with_sub_features_cool_toggle_2',
|
||||
'cool_all',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders with no privileges granted when minimal feature privileges are assigned, and sub-feature privileges are disallowed', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_all'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges: false,
|
||||
});
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders with no privileges granted when sub feature privileges are assigned, and sub-feature privileges are disallowed', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read', 'cool_all'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { displayedPrivileges } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: true,
|
||||
canCustomizeSubFeaturePrivileges: false,
|
||||
});
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,103 +4,112 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiInMemoryTable,
|
||||
EuiText,
|
||||
IconType,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Feature } from '../../../../../../../../features/public';
|
||||
import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model';
|
||||
import {
|
||||
AllowedPrivilege,
|
||||
CalculatedPrivilege,
|
||||
PrivilegeExplanation,
|
||||
} from '../kibana_privilege_calculator';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { PrivilegeDisplay } from '../space_aware_privilege_section/privilege_display';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { ChangeAllPrivilegesControl } from './change_all_privileges';
|
||||
import { FeatureTableExpandedRow } from './feature_table_expanded_row';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
import { FeatureTableCell } from '../feature_table_cell';
|
||||
import { KibanaPrivileges, SecuredFeature, KibanaPrivilege } from '../../../../model';
|
||||
|
||||
interface Props {
|
||||
role: Role;
|
||||
features: Feature[];
|
||||
calculatedPrivileges: CalculatedPrivilege;
|
||||
allowedPrivileges: AllowedPrivilege;
|
||||
rankedFeaturePrivileges: FeaturesPrivileges;
|
||||
privilegeCalculator: PrivilegeFormCalculator;
|
||||
kibanaPrivileges: KibanaPrivileges;
|
||||
spacesIndex: number;
|
||||
privilegeIndex: number;
|
||||
onChange: (featureId: string, privileges: string[]) => void;
|
||||
onChangeAll: (privileges: string[]) => void;
|
||||
canCustomizeSubFeaturePrivileges: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TableFeature extends Feature {
|
||||
hasAnyPrivilegeAssigned: boolean;
|
||||
interface State {
|
||||
expandedFeatures: string[];
|
||||
}
|
||||
|
||||
interface TableRow {
|
||||
feature: TableFeature;
|
||||
featureId: string;
|
||||
feature: SecuredFeature;
|
||||
inherited: KibanaPrivilege[];
|
||||
effective: KibanaPrivilege[];
|
||||
role: Role;
|
||||
}
|
||||
|
||||
export class FeatureTable extends Component<Props, {}> {
|
||||
export class FeatureTable extends Component<Props, State> {
|
||||
public static defaultProps = {
|
||||
spacesIndex: -1,
|
||||
privilegeIndex: -1,
|
||||
showLocks: true,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { role, features, calculatedPrivileges, rankedFeaturePrivileges } = this.props;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
expandedFeatures: [],
|
||||
};
|
||||
}
|
||||
|
||||
const items: TableRow[] = features
|
||||
public render() {
|
||||
const { role, kibanaPrivileges } = this.props;
|
||||
|
||||
const featurePrivileges = kibanaPrivileges.getSecuredFeatures();
|
||||
|
||||
const items: TableRow[] = featurePrivileges
|
||||
.sort((feature1, feature2) => {
|
||||
if (
|
||||
Object.keys(feature1.privileges).length === 0 &&
|
||||
Object.keys(feature2.privileges).length > 0
|
||||
) {
|
||||
if (feature1.reserved && !feature2.reserved) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(feature2.privileges).length === 0 &&
|
||||
Object.keys(feature1.privileges).length > 0
|
||||
) {
|
||||
if (feature2.reserved && !feature1.reserved) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
.map(feature => {
|
||||
const calculatedFeaturePrivileges = calculatedPrivileges.feature[feature.id];
|
||||
const hasAnyPrivilegeAssigned = Boolean(
|
||||
calculatedFeaturePrivileges &&
|
||||
calculatedFeaturePrivileges.actualPrivilege !== NO_PRIVILEGE_VALUE
|
||||
);
|
||||
return {
|
||||
feature: {
|
||||
...feature,
|
||||
hasAnyPrivilegeAssigned,
|
||||
},
|
||||
featureId: feature.id,
|
||||
feature,
|
||||
inherited: [],
|
||||
effective: [],
|
||||
role,
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: This simply grabs the available privileges from the first feature we encounter.
|
||||
// As of now, features can have 'all' and 'read' as available privileges. Once that assumption breaks,
|
||||
// this will need updating. This is a simplifying measure to enable the new UI.
|
||||
const availablePrivileges = Object.values(rankedFeaturePrivileges)[0];
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
responsive={false}
|
||||
columns={this.getColumns(availablePrivileges)}
|
||||
columns={this.getColumns()}
|
||||
itemId={'featureId'}
|
||||
itemIdToExpandedRowMap={this.state.expandedFeatures.reduce((acc, featureId) => {
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: (
|
||||
<FeatureTableExpandedRow
|
||||
feature={featurePrivileges.find(f => f.id === featureId)!}
|
||||
privilegeIndex={this.props.privilegeIndex}
|
||||
onChange={this.props.onChange}
|
||||
privilegeCalculator={this.props.privilegeCalculator}
|
||||
selectedFeaturePrivileges={
|
||||
this.props.role.kibana[this.props.privilegeIndex].feature[featureId] ?? []
|
||||
}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}, {})}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
|
@ -115,171 +124,157 @@ export class FeatureTable extends Component<Props, {}> {
|
|||
}
|
||||
};
|
||||
|
||||
private getColumns = (availablePrivileges: string[]) => [
|
||||
{
|
||||
field: 'feature',
|
||||
name: i18n.translate(
|
||||
'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle',
|
||||
{ defaultMessage: 'Feature' }
|
||||
),
|
||||
render: (feature: TableFeature) => {
|
||||
let tooltipElement = null;
|
||||
if (feature.privilegesTooltip) {
|
||||
const tooltipContent = (
|
||||
<EuiText>
|
||||
<p>{feature.privilegesTooltip}</p>
|
||||
</EuiText>
|
||||
);
|
||||
tooltipElement = (
|
||||
<EuiIconTip
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
type={'iInCircle'}
|
||||
color={'subdued'}
|
||||
content={tooltipContent}
|
||||
private getColumns = () => {
|
||||
const basePrivileges = this.props.kibanaPrivileges.getBasePrivileges(
|
||||
this.props.role.kibana[this.props.privilegeIndex]
|
||||
);
|
||||
|
||||
const columns = [];
|
||||
|
||||
if (this.props.canCustomizeSubFeaturePrivileges) {
|
||||
columns.push({
|
||||
width: '30px',
|
||||
isExpander: true,
|
||||
field: 'featureId',
|
||||
name: '',
|
||||
render: (featureId: string, record: TableRow) => {
|
||||
const { feature } = record;
|
||||
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
||||
if (!hasSubFeaturePrivileges) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
onClick={() => this.toggleExpandedFeature(featureId)}
|
||||
data-test-subj={`expandFeaturePrivilegeRow expandFeaturePrivilegeRow-${featureId}`}
|
||||
aria-label={this.state.expandedFeatures.includes(featureId) ? 'Collapse' : 'Expand'}
|
||||
iconType={this.state.expandedFeatures.includes(featureId) ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<EuiIcon size="m" type={feature.icon as IconType} className="secPrivilegeFeatureIcon" />
|
||||
{feature.name} {tooltipElement}
|
||||
</span>
|
||||
);
|
||||
columns.push(
|
||||
{
|
||||
field: 'feature',
|
||||
width: '200px',
|
||||
name: i18n.translate(
|
||||
'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Feature',
|
||||
}
|
||||
),
|
||||
render: (feature: SecuredFeature) => {
|
||||
return <FeatureTableCell feature={feature} />;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'privilege',
|
||||
name: (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle"
|
||||
defaultMessage="Privilege"
|
||||
/>
|
||||
{!this.props.disabled && (
|
||||
<ChangeAllPrivilegesControl
|
||||
privileges={[...availablePrivileges, NO_PRIVILEGE_VALUE]}
|
||||
onChange={this.onChangeAllFeaturePrivileges}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
render: (roleEntry: Role, record: TableRow) => {
|
||||
const { id: featureId, name: featureName, reserved, privileges } = record.feature;
|
||||
|
||||
if (reserved && Object.keys(privileges).length === 0) {
|
||||
return <EuiText size={'s'}>{reserved.description}</EuiText>;
|
||||
}
|
||||
|
||||
const featurePrivileges = this.props.kibanaPrivileges
|
||||
.getFeaturePrivileges()
|
||||
.getPrivileges(featureId);
|
||||
|
||||
if (featurePrivileges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enabledFeaturePrivileges = this.getEnabledFeaturePrivileges(
|
||||
featurePrivileges,
|
||||
featureId
|
||||
);
|
||||
|
||||
const privilegeExplanation = this.getPrivilegeExplanation(featureId);
|
||||
|
||||
const allowsNone = this.allowsNoneForPrivilegeAssignment(featureId);
|
||||
|
||||
const actualPrivilegeValue = privilegeExplanation.actualPrivilege;
|
||||
|
||||
const canChangePrivilege =
|
||||
!this.props.disabled && (allowsNone || enabledFeaturePrivileges.length > 1);
|
||||
|
||||
if (!canChangePrivilege) {
|
||||
const assignedBasePrivilege =
|
||||
this.props.role.kibana[this.props.spacesIndex].base.length > 0;
|
||||
|
||||
const excludedFromBasePrivilegsTooltip = (
|
||||
{
|
||||
field: 'privilege',
|
||||
width: '200px',
|
||||
name: (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.featureTable.excludedFromBasePrivilegsTooltip"
|
||||
defaultMessage='Use "Custom" privileges to grant access. {featureName} isn't part of the base privileges.'
|
||||
values={{ featureName }}
|
||||
id="xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle"
|
||||
defaultMessage="Privilege"
|
||||
/>
|
||||
{!this.props.disabled && (
|
||||
<ChangeAllPrivilegesControl
|
||||
privileges={basePrivileges}
|
||||
onChange={this.onChangeAllFeaturePrivileges}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
mobileOptions: {
|
||||
// Table isn't responsive, so skip rendering this for mobile. <ChangeAllPrivilegesControl /> isn't free...
|
||||
header: false,
|
||||
},
|
||||
render: (roleEntry: Role, record: TableRow) => {
|
||||
const { feature } = record;
|
||||
|
||||
if (feature.reserved) {
|
||||
return <EuiText size={'s'}>{feature.reserved.description}</EuiText>;
|
||||
}
|
||||
|
||||
const primaryFeaturePrivileges = feature.getPrimaryFeaturePrivileges();
|
||||
|
||||
if (primaryFeaturePrivileges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedPrivilegeId = this.props.privilegeCalculator.getDisplayedPrimaryFeaturePrivilegeId(
|
||||
feature.id,
|
||||
this.props.privilegeIndex
|
||||
);
|
||||
|
||||
const options = primaryFeaturePrivileges.map(privilege => {
|
||||
return {
|
||||
id: `${feature.id}_${privilege.id}`,
|
||||
label: privilege.name,
|
||||
isDisabled: this.props.disabled,
|
||||
};
|
||||
});
|
||||
|
||||
options.push({
|
||||
id: `${feature.id}_${NO_PRIVILEGE_VALUE}`,
|
||||
label: 'None',
|
||||
isDisabled: this.props.disabled,
|
||||
});
|
||||
|
||||
let warningIcon = <EuiIconTip type="empty" content={null} />;
|
||||
if (
|
||||
this.props.privilegeCalculator.hasCustomizedSubFeaturePrivileges(
|
||||
feature.id,
|
||||
this.props.privilegeIndex
|
||||
)
|
||||
) {
|
||||
warningIcon = (
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip"
|
||||
defaultMessage="Feature has customized sub-feature privileges. Expand this row for more information."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PrivilegeDisplay
|
||||
privilege={actualPrivilegeValue}
|
||||
explanation={privilegeExplanation}
|
||||
tooltipContent={
|
||||
assignedBasePrivilege && actualPrivilegeValue === NO_PRIVILEGE_VALUE
|
||||
? excludedFromBasePrivilegsTooltip
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>{warningIcon}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonGroup
|
||||
name={`featurePrivilege_${feature.id}`}
|
||||
data-test-subj={`primaryFeaturePrivilegeControl`}
|
||||
buttonSize="compressed"
|
||||
color={'primary'}
|
||||
isFullWidth={true}
|
||||
options={options}
|
||||
idSelected={`${feature.id}_${selectedPrivilegeId ?? NO_PRIVILEGE_VALUE}`}
|
||||
onChange={this.onChange(feature.id)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const options = availablePrivileges.map(priv => {
|
||||
return {
|
||||
id: `${featureId}_${priv}`,
|
||||
label: _.capitalize(priv),
|
||||
isDisabled: !enabledFeaturePrivileges.includes(priv),
|
||||
};
|
||||
});
|
||||
|
||||
options.push({
|
||||
id: `${featureId}_${NO_PRIVILEGE_VALUE}`,
|
||||
label: 'None',
|
||||
isDisabled: !allowsNone,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
name={`featurePrivilege_${featureId}`}
|
||||
options={options}
|
||||
idSelected={`${featureId}_${actualPrivilegeValue || NO_PRIVILEGE_VALUE}`}
|
||||
onChange={this.onChange(featureId)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
private getEnabledFeaturePrivileges = (featurePrivileges: string[], featureId: string) => {
|
||||
const { allowedPrivileges } = this.props;
|
||||
|
||||
if (this.isConfiguringGlobalPrivileges()) {
|
||||
// Global feature privileges are not limited by effective privileges.
|
||||
return featurePrivileges;
|
||||
}
|
||||
|
||||
const allowedFeaturePrivileges = allowedPrivileges.feature[featureId];
|
||||
if (allowedFeaturePrivileges == null) {
|
||||
throw new Error('Unable to get enabled feature privileges for a feature without privileges');
|
||||
}
|
||||
|
||||
return allowedFeaturePrivileges.privileges;
|
||||
},
|
||||
}
|
||||
);
|
||||
return columns;
|
||||
};
|
||||
|
||||
private getPrivilegeExplanation = (featureId: string): PrivilegeExplanation => {
|
||||
const { calculatedPrivileges } = this.props;
|
||||
const calculatedFeaturePrivileges = calculatedPrivileges.feature[featureId];
|
||||
if (calculatedFeaturePrivileges == null) {
|
||||
throw new Error('Unable to get privilege explanation for a feature without privileges');
|
||||
private toggleExpandedFeature = (featureId: string) => {
|
||||
if (this.state.expandedFeatures.includes(featureId)) {
|
||||
this.setState({
|
||||
expandedFeatures: this.state.expandedFeatures.filter(ef => ef !== featureId),
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
expandedFeatures: [...this.state.expandedFeatures, featureId],
|
||||
});
|
||||
}
|
||||
|
||||
return calculatedFeaturePrivileges;
|
||||
};
|
||||
|
||||
private allowsNoneForPrivilegeAssignment = (featureId: string): boolean => {
|
||||
const { allowedPrivileges } = this.props;
|
||||
const allowedFeaturePrivileges = allowedPrivileges.feature[featureId];
|
||||
if (allowedFeaturePrivileges == null) {
|
||||
throw new Error('Unable to determine if none is allowed for a feature without privileges');
|
||||
}
|
||||
|
||||
return allowedFeaturePrivileges.canUnassign;
|
||||
};
|
||||
|
||||
private onChangeAllFeaturePrivileges = (privilege: string) => {
|
||||
|
@ -289,7 +284,4 @@ export class FeatureTable extends Component<Props, {}> {
|
|||
this.props.onChangeAll([privilege]);
|
||||
}
|
||||
};
|
||||
|
||||
private isConfiguringGlobalPrivileges = () =>
|
||||
isGlobalPrivilegeDefinition(this.props.role.kibana[this.props.spacesIndex]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
|
||||
import { kibanaFeatures } from '../../../../__fixtures__/kibana_features';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { FeatureTableExpandedRow } from './feature_table_expanded_row';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
const createRole = (kibana: Role['kibana'] = []): Role => {
|
||||
return {
|
||||
name: 'my_role',
|
||||
elasticsearch: { cluster: [], run_as: [], indices: [] },
|
||||
kibana,
|
||||
};
|
||||
};
|
||||
|
||||
describe('FeatureTableExpandedRow', () => {
|
||||
it('indicates sub-feature privileges are being customized if a minimal feature privilege is set', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTableExpandedRow
|
||||
feature={feature}
|
||||
privilegeIndex={0}
|
||||
privilegeCalculator={calculator}
|
||||
selectedFeaturePrivileges={['minimal_read']}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('EuiSwitch[data-test-subj="customizeSubFeaturePrivileges"]').props()
|
||||
).toMatchObject({
|
||||
disabled: false,
|
||||
checked: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('indicates sub-feature privileges are not being customized if a primary feature privilege is set', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTableExpandedRow
|
||||
feature={feature}
|
||||
privilegeIndex={0}
|
||||
privilegeCalculator={calculator}
|
||||
selectedFeaturePrivileges={['read']}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('EuiSwitch[data-test-subj="customizeSubFeaturePrivileges"]').props()
|
||||
).toMatchObject({
|
||||
disabled: false,
|
||||
checked: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not allow customizing if a primary privilege is not set', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTableExpandedRow
|
||||
feature={feature}
|
||||
privilegeIndex={0}
|
||||
privilegeCalculator={calculator}
|
||||
selectedFeaturePrivileges={['read']}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('EuiSwitch[data-test-subj="customizeSubFeaturePrivileges"]').props()
|
||||
).toMatchObject({
|
||||
disabled: true,
|
||||
checked: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('switches to the minimal privilege when customizing privileges, including corresponding sub-feature privileges', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTableExpandedRow
|
||||
feature={feature}
|
||||
privilegeIndex={0}
|
||||
privilegeCalculator={calculator}
|
||||
selectedFeaturePrivileges={['read']}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
findTestSubject(wrapper, 'customizeSubFeaturePrivileges').simulate('click');
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('with_sub_features', [
|
||||
'minimal_read',
|
||||
'cool_read',
|
||||
'cool_toggle_2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('switches to the primary privilege when not customizing privileges, removing any other privileges', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read', 'cool_read', 'cool_toggle_2'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTableExpandedRow
|
||||
feature={feature}
|
||||
privilegeIndex={0}
|
||||
privilegeCalculator={calculator}
|
||||
selectedFeaturePrivileges={['minimal_read', 'cool_read', 'cool_toggle_2']}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
findTestSubject(wrapper, 'customizeSubFeaturePrivileges').simulate('click');
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('with_sub_features', ['read']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 React, { useState, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFlexItem, EuiFlexGroup, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
|
||||
import { SubFeatureForm } from './sub_feature_form';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
import { SecuredFeature } from '../../../../model';
|
||||
|
||||
interface Props {
|
||||
feature: SecuredFeature;
|
||||
privilegeCalculator: PrivilegeFormCalculator;
|
||||
privilegeIndex: number;
|
||||
selectedFeaturePrivileges: string[];
|
||||
disabled?: boolean;
|
||||
onChange: (featureId: string, featurePrivileges: string[]) => void;
|
||||
}
|
||||
|
||||
export const FeatureTableExpandedRow = ({
|
||||
feature,
|
||||
onChange,
|
||||
privilegeIndex,
|
||||
privilegeCalculator,
|
||||
selectedFeaturePrivileges,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
const [isCustomizing, setIsCustomizing] = useState(() => {
|
||||
return feature
|
||||
.getMinimalFeaturePrivileges()
|
||||
.some(p => selectedFeaturePrivileges.includes(p.id));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const hasMinimalFeaturePrivilegeSelected = feature
|
||||
.getMinimalFeaturePrivileges()
|
||||
.some(p => selectedFeaturePrivileges.includes(p.id));
|
||||
|
||||
if (!hasMinimalFeaturePrivilegeSelected && isCustomizing) {
|
||||
setIsCustomizing(false);
|
||||
}
|
||||
}, [feature, isCustomizing, selectedFeaturePrivileges]);
|
||||
|
||||
const onCustomizeSubFeatureChange = (e: EuiSwitchEvent) => {
|
||||
onChange(
|
||||
feature.id,
|
||||
privilegeCalculator.updateSelectedFeaturePrivilegesForCustomization(
|
||||
feature.id,
|
||||
privilegeIndex,
|
||||
e.target.checked
|
||||
)
|
||||
);
|
||||
setIsCustomizing(e.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel"
|
||||
defaultMessage="Customize sub-feature privileges"
|
||||
/>
|
||||
}
|
||||
checked={isCustomizing}
|
||||
onChange={onCustomizeSubFeatureChange}
|
||||
data-test-subj="customizeSubFeaturePrivileges"
|
||||
disabled={
|
||||
disabled ||
|
||||
!privilegeCalculator.canCustomizeSubFeaturePrivileges(feature.id, privilegeIndex)
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{feature.getSubFeatures().map(subFeature => {
|
||||
return (
|
||||
<EuiFlexItem key={subFeature.name}>
|
||||
<SubFeatureForm
|
||||
privilegeCalculator={privilegeCalculator}
|
||||
privilegeIndex={privilegeIndex}
|
||||
featureId={feature.id}
|
||||
subFeature={subFeature}
|
||||
onChange={updatedPrivileges => onChange(feature.id, updatedPrivileges)}
|
||||
selectedFeaturePrivileges={selectedFeaturePrivileges}
|
||||
disabled={disabled || !isCustomizing}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { kibanaFeatures } from '../../../../__fixtures__/kibana_features';
|
||||
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
|
||||
import { SecuredSubFeature } from '../../../../model';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { SubFeatureForm } from './sub_feature_form';
|
||||
import { EuiCheckbox, EuiButtonGroup } from '@elastic/eui';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
// Note: these tests are not concerned with the proper display of privileges,
|
||||
// as that is verified by the feature_table and privilege_space_form tests.
|
||||
|
||||
const createRole = (kibana: Role['kibana'] = []): Role => {
|
||||
return {
|
||||
name: 'my_role',
|
||||
elasticsearch: { cluster: [], run_as: [], indices: [] },
|
||||
kibana,
|
||||
};
|
||||
};
|
||||
|
||||
const featureId = 'with_sub_features';
|
||||
const subFeature = kibanaFeatures.find(kf => kf.id === featureId)!.subFeatures[0];
|
||||
const securedSubFeature = new SecuredSubFeature(subFeature.toRaw());
|
||||
|
||||
describe('SubFeatureForm', () => {
|
||||
it('renders disabled elements when requested', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: [],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SubFeatureForm
|
||||
featureId={featureId}
|
||||
subFeature={securedSubFeature}
|
||||
selectedFeaturePrivileges={[]}
|
||||
privilegeCalculator={calculator}
|
||||
privilegeIndex={0}
|
||||
onChange={jest.fn()}
|
||||
disabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const checkboxes = wrapper.find(EuiCheckbox);
|
||||
const buttonGroups = wrapper.find(EuiButtonGroup);
|
||||
|
||||
expect(checkboxes.everyWhere(checkbox => checkbox.props().disabled === true)).toBe(true);
|
||||
expect(buttonGroups.everyWhere(checkbox => checkbox.props().isDisabled === true)).toBe(true);
|
||||
});
|
||||
|
||||
it('fires onChange when an independent privilege is selected', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: [],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SubFeatureForm
|
||||
featureId={featureId}
|
||||
subFeature={securedSubFeature}
|
||||
selectedFeaturePrivileges={[]}
|
||||
privilegeCalculator={calculator}
|
||||
privilegeIndex={0}
|
||||
onChange={onChange}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const checkbox = wrapper.find('EuiCheckbox[id="with_sub_features_cool_toggle_1"] input');
|
||||
|
||||
act(() => {
|
||||
checkbox.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['cool_toggle_1']);
|
||||
});
|
||||
|
||||
it('fires onChange when an independent privilege is deselected', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['cool_toggle_1', 'cool_toggle_2'],
|
||||
},
|
||||
spaces: [],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SubFeatureForm
|
||||
featureId={featureId}
|
||||
subFeature={securedSubFeature}
|
||||
selectedFeaturePrivileges={['cool_toggle_1', 'cool_toggle_2']}
|
||||
privilegeCalculator={calculator}
|
||||
privilegeIndex={0}
|
||||
onChange={onChange}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const checkbox = wrapper.find('EuiCheckbox[id="with_sub_features_cool_toggle_1"] input');
|
||||
|
||||
act(() => {
|
||||
checkbox.simulate('change', { target: { checked: false } });
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['cool_toggle_2']);
|
||||
});
|
||||
|
||||
it('fires onChange when a mutually exclusive privilege is selected', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: [],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SubFeatureForm
|
||||
featureId={featureId}
|
||||
subFeature={securedSubFeature}
|
||||
selectedFeaturePrivileges={[]}
|
||||
privilegeCalculator={calculator}
|
||||
privilegeIndex={0}
|
||||
onChange={onChange}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const button = wrapper.find(EuiButtonGroup);
|
||||
|
||||
act(() => {
|
||||
button.props().onChange('cool_all');
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['cool_all']);
|
||||
});
|
||||
|
||||
it('fires onChange when switching between mutually exclusive options', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: [],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SubFeatureForm
|
||||
featureId={featureId}
|
||||
subFeature={securedSubFeature}
|
||||
selectedFeaturePrivileges={['cool_all', 'cool_toggle_1']}
|
||||
privilegeCalculator={calculator}
|
||||
privilegeIndex={0}
|
||||
onChange={onChange}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const button = wrapper.find(EuiButtonGroup);
|
||||
|
||||
act(() => {
|
||||
button.props().onChange('cool_read');
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['cool_toggle_1', 'cool_read']);
|
||||
});
|
||||
|
||||
it('fires onChange when a mutually exclusive privilege is deselected', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['cool_all'],
|
||||
},
|
||||
spaces: [],
|
||||
},
|
||||
]);
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SubFeatureForm
|
||||
featureId={featureId}
|
||||
subFeature={securedSubFeature}
|
||||
selectedFeaturePrivileges={['cool_all']}
|
||||
privilegeCalculator={calculator}
|
||||
privilegeIndex={0}
|
||||
onChange={onChange}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const button = wrapper.find(EuiButtonGroup);
|
||||
|
||||
act(() => {
|
||||
button.props().onChange('none');
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiCheckbox, EuiButtonGroup } from '@elastic/eui';
|
||||
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
import {
|
||||
SecuredSubFeature,
|
||||
SubFeaturePrivilegeGroup,
|
||||
SubFeaturePrivilege,
|
||||
} from '../../../../model';
|
||||
|
||||
interface Props {
|
||||
featureId: string;
|
||||
subFeature: SecuredSubFeature;
|
||||
selectedFeaturePrivileges: string[];
|
||||
privilegeCalculator: PrivilegeFormCalculator;
|
||||
privilegeIndex: number;
|
||||
onChange: (selectedPrivileges: string[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const SubFeatureForm = (props: Props) => {
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">{props.subFeature.name}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{props.subFeature.getPrivilegeGroups().map(renderPrivilegeGroup)}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
function renderPrivilegeGroup(privilegeGroup: SubFeaturePrivilegeGroup, index: number) {
|
||||
switch (privilegeGroup.groupType) {
|
||||
case 'independent':
|
||||
return renderIndependentPrivilegeGroup(privilegeGroup, index);
|
||||
case 'mutually_exclusive':
|
||||
return renderMutuallyExclusivePrivilegeGroup(privilegeGroup, index);
|
||||
default:
|
||||
throw new Error(`Unsupported privilege group type: ${privilegeGroup.groupType}`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderIndependentPrivilegeGroup(
|
||||
privilegeGroup: SubFeaturePrivilegeGroup,
|
||||
index: number
|
||||
) {
|
||||
return (
|
||||
<div key={index}>
|
||||
{privilegeGroup.privileges.map((privilege: SubFeaturePrivilege) => {
|
||||
const isGranted = props.privilegeCalculator.isIndependentSubFeaturePrivilegeGranted(
|
||||
props.featureId,
|
||||
privilege.id,
|
||||
props.privilegeIndex
|
||||
);
|
||||
return (
|
||||
<EuiCheckbox
|
||||
key={privilege.id}
|
||||
id={`${props.featureId}_${privilege.id}`}
|
||||
label={privilege.name}
|
||||
data-test-subj="independentSubFeaturePrivilegeControl"
|
||||
onChange={e => {
|
||||
const { checked } = e.target;
|
||||
if (checked) {
|
||||
props.onChange([...props.selectedFeaturePrivileges, privilege.id]);
|
||||
} else {
|
||||
props.onChange(props.selectedFeaturePrivileges.filter(sp => sp !== privilege.id));
|
||||
}
|
||||
}}
|
||||
checked={isGranted}
|
||||
disabled={props.disabled}
|
||||
compressed={true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderMutuallyExclusivePrivilegeGroup(
|
||||
privilegeGroup: SubFeaturePrivilegeGroup,
|
||||
index: number
|
||||
) {
|
||||
const firstSelectedPrivilege = props.privilegeCalculator.getSelectedMutuallyExclusiveSubFeaturePrivilege(
|
||||
props.featureId,
|
||||
privilegeGroup,
|
||||
props.privilegeIndex
|
||||
);
|
||||
|
||||
const options = [
|
||||
...privilegeGroup.privileges.map((privilege, privilegeIndex) => {
|
||||
return {
|
||||
id: privilege.id,
|
||||
label: privilege.name,
|
||||
isDisabled: props.disabled,
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
options.push({
|
||||
id: NO_PRIVILEGE_VALUE,
|
||||
label: 'None',
|
||||
isDisabled: props.disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
key={index}
|
||||
buttonSize="compressed"
|
||||
data-test-subj="mutexSubFeaturePrivilegeControl"
|
||||
options={options}
|
||||
idSelected={firstSelectedPrivilege?.id ?? NO_PRIVILEGE_VALUE}
|
||||
isDisabled={props.disabled}
|
||||
onChange={selectedPrivilegeId => {
|
||||
// Deselect all privileges which belong to this mutually-exclusive group
|
||||
const privilegesWithoutGroupEntries = props.selectedFeaturePrivileges.filter(
|
||||
sp => !privilegeGroup.privileges.some(privilege => privilege.id === sp)
|
||||
);
|
||||
// fire on-change with the newly selected privilege
|
||||
if (selectedPrivilegeId === NO_PRIVILEGE_VALUE) {
|
||||
props.onChange(privilegesWithoutGroupEntries);
|
||||
} else {
|
||||
props.onChange([...privilegesWithoutGroupEntries, selectedPrivilegeId]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { createFeature } from '../../../../__fixtures__/kibana_features';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { FeatureTableCell } from '.';
|
||||
import { SecuredFeature } from '../../../../model';
|
||||
import { EuiIcon, EuiIconTip } from '@elastic/eui';
|
||||
|
||||
describe('FeatureTableCell', () => {
|
||||
it('renders an icon and feature name', () => {
|
||||
const feature = createFeature({
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTableCell feature={new SecuredFeature(feature.toRaw())} />
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`);
|
||||
expect(wrapper.find(EuiIcon).props()).toMatchObject({
|
||||
type: feature.icon,
|
||||
});
|
||||
expect(wrapper.find(EuiIconTip)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders an icon and feature name with tooltip when configured', () => {
|
||||
const feature = createFeature({
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
privilegesTooltip: 'This is my awesome tooltip content',
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<FeatureTableCell feature={new SecuredFeature(feature.toRaw())} />
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`);
|
||||
expect(
|
||||
wrapper
|
||||
.find(EuiIcon)
|
||||
.first()
|
||||
.props()
|
||||
).toMatchObject({
|
||||
type: feature.icon,
|
||||
});
|
||||
expect(wrapper.find(EuiIconTip).props().content).toMatchInlineSnapshot(`
|
||||
<EuiText>
|
||||
<p>
|
||||
This is my awesome tooltip content
|
||||
</p>
|
||||
</EuiText>
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiText, EuiIconTip, EuiIcon, IconType } from '@elastic/eui';
|
||||
import { SecuredFeature } from '../../../../model';
|
||||
|
||||
interface Props {
|
||||
feature: SecuredFeature;
|
||||
}
|
||||
|
||||
export const FeatureTableCell = ({ feature }: Props) => {
|
||||
let tooltipElement = null;
|
||||
if (feature.getPrivilegesTooltip()) {
|
||||
const tooltipContent = (
|
||||
<EuiText>
|
||||
<p>{feature.getPrivilegesTooltip()}</p>
|
||||
</EuiText>
|
||||
);
|
||||
tooltipElement = (
|
||||
<EuiIconTip
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
type={'iInCircle'}
|
||||
color={'subdued'}
|
||||
content={tooltipContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<EuiIcon size="m" type={feature.icon as IconType} className="secPrivilegeFeatureIcon" />
|
||||
{feature.name} {tooltipElement}
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { rawKibanaPrivileges } from './raw_kibana_privileges';
|
||||
export { FeatureTableCell } from './feature_table_cell';
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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 { FeaturesPrivileges, Role } from '../../../../../../../../common/model';
|
||||
|
||||
export interface BuildRoleOpts {
|
||||
spacesPrivileges?: Array<{
|
||||
spaces: string[];
|
||||
base: string[];
|
||||
feature: FeaturesPrivileges;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const buildRole = (options: BuildRoleOpts = {}) => {
|
||||
const role: Role = {
|
||||
name: 'unit test role',
|
||||
elasticsearch: {
|
||||
indices: [],
|
||||
cluster: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [],
|
||||
};
|
||||
|
||||
if (options.spacesPrivileges) {
|
||||
role.kibana.push(...options.spacesPrivileges);
|
||||
} else {
|
||||
role.kibana.push({
|
||||
spaces: [],
|
||||
base: [],
|
||||
feature: {},
|
||||
});
|
||||
}
|
||||
|
||||
return role;
|
||||
};
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* 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 unrestrictedBasePrivileges = {
|
||||
base: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: true,
|
||||
},
|
||||
};
|
||||
export const unrestrictedFeaturePrivileges = {
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: true,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: true,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: true,
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const fullyRestrictedBasePrivileges = {
|
||||
base: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const fullyRestrictedFeaturePrivileges = {
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* 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 { KibanaPrivileges } from '../../../../../../../../common/model';
|
||||
|
||||
export const defaultPrivilegeDefinition = new KibanaPrivileges({
|
||||
global: {
|
||||
all: ['api:/*', 'ui:/*'],
|
||||
read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/*', 'ui:/feature4/foo'],
|
||||
},
|
||||
space: {
|
||||
all: [
|
||||
'api:/feature1/*',
|
||||
'ui:/feature1/*',
|
||||
'api:/feature2/*',
|
||||
'ui:/feature2/*',
|
||||
'ui:/feature3/foo',
|
||||
'ui:/feature3/foo/*',
|
||||
'ui:/feature4/foo',
|
||||
],
|
||||
read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/bar', 'ui:/feature4/foo'],
|
||||
},
|
||||
features: {
|
||||
feature1: {
|
||||
all: ['ui:/feature1/foo', 'ui:/feature1/bar'],
|
||||
read: ['ui:/feature1/foo'],
|
||||
},
|
||||
feature2: {
|
||||
all: ['ui:/feature2/foo', 'api:/feature2/bar'],
|
||||
read: ['ui:/feature2/foo'],
|
||||
},
|
||||
feature3: {
|
||||
all: ['ui:/feature3/foo', 'ui:/feature3/foo/*'],
|
||||
},
|
||||
feature4: {
|
||||
all: ['somethingObscure:/feature4/foo', 'ui:/feature4/foo'],
|
||||
read: ['ui:/feature4/foo'],
|
||||
},
|
||||
},
|
||||
reserved: {},
|
||||
});
|
|
@ -1,313 +0,0 @@
|
|||
/*
|
||||
* 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 { KibanaPrivileges, Role } from '../../../../../../../common/model';
|
||||
import {
|
||||
buildRole,
|
||||
defaultPrivilegeDefinition,
|
||||
fullyRestrictedBasePrivileges,
|
||||
fullyRestrictedFeaturePrivileges,
|
||||
unrestrictedBasePrivileges,
|
||||
unrestrictedFeaturePrivileges,
|
||||
} from './__fixtures__';
|
||||
import { KibanaAllowedPrivilegesCalculator } from './kibana_allowed_privileges_calculator';
|
||||
import { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory';
|
||||
|
||||
const buildAllowedPrivilegesCalculator = (
|
||||
role: Role,
|
||||
kibanaPrivilege: KibanaPrivileges = defaultPrivilegeDefinition
|
||||
) => {
|
||||
return new KibanaAllowedPrivilegesCalculator(kibanaPrivilege, role);
|
||||
};
|
||||
|
||||
const buildEffectivePrivilegesCalculator = (
|
||||
role: Role,
|
||||
kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition
|
||||
) => {
|
||||
const factory = new KibanaPrivilegeCalculatorFactory(kibanaPrivileges);
|
||||
return factory.getInstance(role);
|
||||
};
|
||||
|
||||
describe('AllowedPrivileges', () => {
|
||||
it('allows all privileges when none are currently assigned', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivilegesCalculator(role);
|
||||
const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role);
|
||||
|
||||
const result = allowedPrivilegesCalculator.calculateAllowedPrivileges(
|
||||
effectivePrivileges.calculateEffectivePrivileges(true)
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...unrestrictedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...unrestrictedFeaturePrivileges,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows all global base privileges, but just "all" for everything else when global is set to "all"', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivilegesCalculator(role);
|
||||
const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role);
|
||||
|
||||
const result = allowedPrivilegesCalculator.calculateAllowedPrivileges(
|
||||
effectivePrivileges.calculateEffectivePrivileges(true)
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...fullyRestrictedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
...fullyRestrictedBasePrivileges,
|
||||
...fullyRestrictedFeaturePrivileges,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`allows feature privileges to be set to "all" or "read" when global base is "read"`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivilegesCalculator(role);
|
||||
const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role);
|
||||
|
||||
const result = allowedPrivilegesCalculator.calculateAllowedPrivileges(
|
||||
effectivePrivileges.calculateEffectivePrivileges(true)
|
||||
);
|
||||
|
||||
const expectedFeaturePrivileges = {
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: true, // feature 3 has no "read" privilege governed by global "all"
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...expectedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
base: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
...expectedFeaturePrivileges,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`allows feature privileges to be set to "all" or "read" when space base is "read"`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivilegesCalculator(role);
|
||||
const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role);
|
||||
|
||||
const result = allowedPrivilegesCalculator.calculateAllowedPrivileges(
|
||||
effectivePrivileges.calculateEffectivePrivileges(true)
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...unrestrictedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
base: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: true,
|
||||
},
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: true, // feature 3 has no "read" privilege governed by space "all"
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`allows space base privilege to be set to "all" or "read" when space base is already "all"`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivilegesCalculator(role);
|
||||
const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role);
|
||||
|
||||
const result = allowedPrivilegesCalculator.calculateAllowedPrivileges(
|
||||
effectivePrivileges.calculateEffectivePrivileges(true)
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`restricts space feature privileges when global feature privileges are set`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
feature2: ['read'],
|
||||
feature4: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivilegesCalculator(role);
|
||||
const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role);
|
||||
|
||||
const result = allowedPrivilegesCalculator.calculateAllowedPrivileges(
|
||||
effectivePrivileges.calculateEffectivePrivileges(true)
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...unrestrictedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
base: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: true,
|
||||
},
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: true, // feature 3 has no "read" privilege governed by space "all"
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,155 +0,0 @@
|
|||
/*
|
||||
* 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 { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import {
|
||||
areActionsFullyCovered,
|
||||
compareActions,
|
||||
} from '../../../../../../../common/privilege_calculator_utils';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import {
|
||||
AllowedPrivilege,
|
||||
CalculatedPrivilege,
|
||||
PRIVILEGE_SOURCE,
|
||||
} from './kibana_privilege_calculator_types';
|
||||
|
||||
export class KibanaAllowedPrivilegesCalculator {
|
||||
// reference to the global privilege definition
|
||||
private globalPrivilege: RoleKibanaPrivilege;
|
||||
|
||||
// list of privilege actions that comprise the global base privilege
|
||||
private readonly assignedGlobalBaseActions: string[];
|
||||
|
||||
constructor(private readonly kibanaPrivileges: KibanaPrivileges, private readonly role: Role) {
|
||||
this.globalPrivilege = this.locateGlobalPrivilege(role);
|
||||
this.assignedGlobalBaseActions = this.globalPrivilege.base[0]
|
||||
? kibanaPrivileges.getGlobalPrivileges().getActions(this.globalPrivilege.base[0])
|
||||
: [];
|
||||
}
|
||||
|
||||
public calculateAllowedPrivileges(
|
||||
effectivePrivileges: CalculatedPrivilege[]
|
||||
): AllowedPrivilege[] {
|
||||
const { kibana = [] } = this.role;
|
||||
return kibana.map((privilegeSpec, index) =>
|
||||
this.calculateAllowedPrivilege(privilegeSpec, effectivePrivileges[index])
|
||||
);
|
||||
}
|
||||
|
||||
private calculateAllowedPrivilege(
|
||||
privilegeSpec: RoleKibanaPrivilege,
|
||||
effectivePrivileges: CalculatedPrivilege
|
||||
): AllowedPrivilege {
|
||||
const result: AllowedPrivilege = {
|
||||
base: {
|
||||
privileges: [],
|
||||
canUnassign: true,
|
||||
},
|
||||
feature: {},
|
||||
};
|
||||
|
||||
if (isGlobalPrivilegeDefinition(privilegeSpec)) {
|
||||
// nothing can impede global privileges
|
||||
result.base.canUnassign = true;
|
||||
result.base.privileges = this.kibanaPrivileges.getGlobalPrivileges().getAllPrivileges();
|
||||
} else {
|
||||
// space base privileges are restricted based on the assigned global privileges
|
||||
const spacePrivileges = this.kibanaPrivileges.getSpacesPrivileges().getAllPrivileges();
|
||||
result.base.canUnassign = this.assignedGlobalBaseActions.length === 0;
|
||||
result.base.privileges = spacePrivileges.filter(privilege => {
|
||||
// always allowed to assign the calculated effective privilege
|
||||
if (privilege === effectivePrivileges.base.actualPrivilege) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const privilegeActions = this.getBaseActions(PRIVILEGE_SOURCE.SPACE_BASE, privilege);
|
||||
return !areActionsFullyCovered(this.assignedGlobalBaseActions, privilegeActions);
|
||||
});
|
||||
}
|
||||
|
||||
const allFeaturePrivileges = this.kibanaPrivileges.getFeaturePrivileges().getAllPrivileges();
|
||||
result.feature = Object.entries(allFeaturePrivileges).reduce(
|
||||
(acc, [featureId, featurePrivileges]) => {
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: this.getAllowedFeaturePrivileges(
|
||||
effectivePrivileges,
|
||||
featureId,
|
||||
featurePrivileges
|
||||
),
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getAllowedFeaturePrivileges(
|
||||
effectivePrivileges: CalculatedPrivilege,
|
||||
featureId: string,
|
||||
candidateFeaturePrivileges: string[]
|
||||
): { privileges: string[]; canUnassign: boolean } {
|
||||
const effectiveFeaturePrivilegeExplanation = effectivePrivileges.feature[featureId];
|
||||
if (effectiveFeaturePrivilegeExplanation == null) {
|
||||
throw new Error('To calculate allowed feature privileges, we need the effective privileges');
|
||||
}
|
||||
|
||||
const effectiveFeatureActions = this.getFeatureActions(
|
||||
featureId,
|
||||
effectiveFeaturePrivilegeExplanation.actualPrivilege
|
||||
);
|
||||
|
||||
const privileges = [];
|
||||
if (effectiveFeaturePrivilegeExplanation.actualPrivilege !== NO_PRIVILEGE_VALUE) {
|
||||
// Always allowed to assign the calculated effective privilege
|
||||
privileges.push(effectiveFeaturePrivilegeExplanation.actualPrivilege);
|
||||
}
|
||||
|
||||
privileges.push(
|
||||
...candidateFeaturePrivileges.filter(privilegeId => {
|
||||
const candidateActions = this.getFeatureActions(featureId, privilegeId);
|
||||
return compareActions(effectiveFeatureActions, candidateActions) > 0;
|
||||
})
|
||||
);
|
||||
|
||||
const result = {
|
||||
privileges: privileges.sort(),
|
||||
canUnassign: effectiveFeaturePrivilegeExplanation.actualPrivilege === NO_PRIVILEGE_VALUE,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getBaseActions(source: PRIVILEGE_SOURCE, privilegeId: string) {
|
||||
switch (source) {
|
||||
case PRIVILEGE_SOURCE.GLOBAL_BASE:
|
||||
return this.assignedGlobalBaseActions;
|
||||
case PRIVILEGE_SOURCE.SPACE_BASE:
|
||||
return this.kibanaPrivileges.getSpacesPrivileges().getActions(privilegeId);
|
||||
default:
|
||||
throw new Error(
|
||||
`Cannot get base actions for unsupported privilege source ${PRIVILEGE_SOURCE[source]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getFeatureActions(featureId: string, privilegeId: string): string[] {
|
||||
return this.kibanaPrivileges.getFeaturePrivileges().getActions(featureId, privilegeId);
|
||||
}
|
||||
|
||||
private locateGlobalPrivilege(role: Role) {
|
||||
const spacePrivileges = role.kibana;
|
||||
return (
|
||||
spacePrivileges.find(privileges => isGlobalPrivilegeDefinition(privileges)) || {
|
||||
spaces: [] as string[],
|
||||
base: [] as string[],
|
||||
feature: {},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,321 +0,0 @@
|
|||
/*
|
||||
* 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 { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { buildRole, defaultPrivilegeDefinition } from './__fixtures__';
|
||||
import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator';
|
||||
import { PRIVILEGE_SOURCE, PrivilegeExplanation } from './kibana_privilege_calculator_types';
|
||||
|
||||
const buildEffectiveBasePrivilegeCalculator = (
|
||||
role: Role,
|
||||
kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition
|
||||
) => {
|
||||
const globalPrivilegeSpec =
|
||||
role.kibana.find(k => isGlobalPrivilegeDefinition(k)) ||
|
||||
({
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {},
|
||||
} as RoleKibanaPrivilege);
|
||||
|
||||
const globalActions = globalPrivilegeSpec.base[0]
|
||||
? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0])
|
||||
: [];
|
||||
|
||||
return new KibanaBasePrivilegeCalculator(kibanaPrivileges, globalPrivilegeSpec, globalActions);
|
||||
};
|
||||
|
||||
describe('getMostPermissiveBasePrivilege', () => {
|
||||
describe('without ignoring assigned', () => {
|
||||
it('returns "none" when no privileges are granted', () => {
|
||||
const role = buildRole();
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[0],
|
||||
false
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
|
||||
defaultPrivilegeDefinition
|
||||
.getGlobalPrivileges()
|
||||
.getAllPrivileges()
|
||||
.forEach(globalBasePrivilege => {
|
||||
it(`returns "${globalBasePrivilege}" when assigned directly at the global privilege`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [globalBasePrivilege],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[0],
|
||||
false
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: globalBasePrivilege,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
});
|
||||
|
||||
defaultPrivilegeDefinition
|
||||
.getSpacesPrivileges()
|
||||
.getAllPrivileges()
|
||||
.forEach(spaceBasePrivilege => {
|
||||
it(`returns "${spaceBasePrivilege}" when assigned directly at the space base privilege`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [spaceBasePrivilege],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[0],
|
||||
false
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: spaceBasePrivilege,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the global privilege when no space base is defined', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[1],
|
||||
false
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
|
||||
it('returns the global privilege when it supercedes the space privilege', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[1],
|
||||
false
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ignoring assigned', () => {
|
||||
it('returns "none" when no privileges are granted', () => {
|
||||
const role = buildRole();
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[0],
|
||||
true
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
|
||||
defaultPrivilegeDefinition
|
||||
.getGlobalPrivileges()
|
||||
.getAllPrivileges()
|
||||
.forEach(globalBasePrivilege => {
|
||||
it(`returns "none" when "${globalBasePrivilege}" assigned directly at the global privilege`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [globalBasePrivilege],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[0],
|
||||
true
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
});
|
||||
|
||||
defaultPrivilegeDefinition
|
||||
.getSpacesPrivileges()
|
||||
.getAllPrivileges()
|
||||
.forEach(spaceBasePrivilege => {
|
||||
it(`returns "none" when "${spaceBasePrivilege}" when assigned directly at the space base privilege`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [spaceBasePrivilege],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[0],
|
||||
true
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the global privilege when no space base is defined', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[1],
|
||||
true
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
|
||||
it('returns the global privilege when it supercedes the space privilege, without indicating override', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[1],
|
||||
true
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
|
||||
it('returns the global privilege even though it would ordinarly be overriden by space base privilege', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[1],
|
||||
true
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
} as PrivilegeExplanation);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* 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 { KibanaPrivileges, RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import { compareActions } from '../../../../../../../common/privilege_calculator_utils';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { PRIVILEGE_SOURCE, PrivilegeExplanation } from './kibana_privilege_calculator_types';
|
||||
|
||||
export class KibanaBasePrivilegeCalculator {
|
||||
constructor(
|
||||
private readonly kibanaPrivileges: KibanaPrivileges,
|
||||
private readonly globalPrivilege: RoleKibanaPrivilege,
|
||||
private readonly assignedGlobalBaseActions: string[]
|
||||
) {}
|
||||
|
||||
public getMostPermissiveBasePrivilege(
|
||||
privilegeSpec: RoleKibanaPrivilege,
|
||||
ignoreAssigned: boolean
|
||||
): PrivilegeExplanation {
|
||||
const assignedPrivilege = privilegeSpec.base[0] || NO_PRIVILEGE_VALUE;
|
||||
|
||||
// If this is the global privilege definition, then there is nothing to supercede it.
|
||||
if (isGlobalPrivilegeDefinition(privilegeSpec)) {
|
||||
if (assignedPrivilege === NO_PRIVILEGE_VALUE || ignoreAssigned) {
|
||||
return {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
actualPrivilege: assignedPrivilege,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, check to see if the global privilege supercedes this one.
|
||||
const baseActions = [
|
||||
...this.kibanaPrivileges.getSpacesPrivileges().getActions(assignedPrivilege),
|
||||
];
|
||||
|
||||
const globalSupercedes =
|
||||
this.hasAssignedGlobalBasePrivilege() &&
|
||||
(compareActions(this.assignedGlobalBaseActions, baseActions) < 0 || ignoreAssigned);
|
||||
|
||||
if (globalSupercedes) {
|
||||
const wasDirectlyAssigned = !ignoreAssigned && baseActions.length > 0;
|
||||
|
||||
return {
|
||||
actualPrivilege: this.globalPrivilege.base[0],
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
...this.buildSupercededFields(
|
||||
wasDirectlyAssigned,
|
||||
assignedPrivilege,
|
||||
PRIVILEGE_SOURCE.SPACE_BASE
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (!ignoreAssigned) {
|
||||
return {
|
||||
actualPrivilege: assignedPrivilege,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
};
|
||||
}
|
||||
|
||||
private hasAssignedGlobalBasePrivilege() {
|
||||
return this.assignedGlobalBaseActions.length > 0;
|
||||
}
|
||||
|
||||
private buildSupercededFields(
|
||||
isSuperceding: boolean,
|
||||
supersededPrivilege?: string,
|
||||
supersededPrivilegeSource?: PRIVILEGE_SOURCE
|
||||
) {
|
||||
if (!isSuperceding) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
supersededPrivilege,
|
||||
supersededPrivilegeSource,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,959 +0,0 @@
|
|||
/*
|
||||
* 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 { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { buildRole, BuildRoleOpts, defaultPrivilegeDefinition } from './__fixtures__';
|
||||
import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator';
|
||||
import { KibanaFeaturePrivilegeCalculator } from './kibana_feature_privilege_calculator';
|
||||
import { PRIVILEGE_SOURCE } from './kibana_privilege_calculator_types';
|
||||
|
||||
const buildEffectiveBasePrivilegeCalculator = (
|
||||
role: Role,
|
||||
kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition
|
||||
) => {
|
||||
const globalPrivilegeSpec =
|
||||
role.kibana.find(k => isGlobalPrivilegeDefinition(k)) ||
|
||||
({
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {},
|
||||
} as RoleKibanaPrivilege);
|
||||
|
||||
const globalActions = globalPrivilegeSpec.base[0]
|
||||
? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0])
|
||||
: [];
|
||||
|
||||
return new KibanaBasePrivilegeCalculator(kibanaPrivileges, globalPrivilegeSpec, globalActions);
|
||||
};
|
||||
|
||||
const buildEffectiveFeaturePrivilegeCalculator = (
|
||||
role: Role,
|
||||
kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition
|
||||
) => {
|
||||
const globalPrivilegeSpec =
|
||||
role.kibana.find(k => isGlobalPrivilegeDefinition(k)) ||
|
||||
({
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {},
|
||||
} as RoleKibanaPrivilege);
|
||||
|
||||
const globalActions = globalPrivilegeSpec.base[0]
|
||||
? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0])
|
||||
: [];
|
||||
|
||||
const rankedFeaturePrivileges = kibanaPrivileges.getFeaturePrivileges().getAllPrivileges();
|
||||
|
||||
return new KibanaFeaturePrivilegeCalculator(
|
||||
kibanaPrivileges,
|
||||
globalPrivilegeSpec,
|
||||
globalActions,
|
||||
rankedFeaturePrivileges
|
||||
);
|
||||
};
|
||||
|
||||
interface TestOpts {
|
||||
only?: boolean;
|
||||
role?: BuildRoleOpts;
|
||||
privilegeIndex?: number;
|
||||
ignoreAssigned?: boolean;
|
||||
result: Record<string, any>;
|
||||
feature?: string;
|
||||
}
|
||||
|
||||
function runTest(
|
||||
description: string,
|
||||
{
|
||||
role: roleOpts = {},
|
||||
result = {},
|
||||
privilegeIndex = 0,
|
||||
ignoreAssigned = false,
|
||||
only = false,
|
||||
feature = 'feature1',
|
||||
}: TestOpts
|
||||
) {
|
||||
const fn = only ? it.only : it;
|
||||
fn(description, () => {
|
||||
const role = buildRole(roleOpts);
|
||||
const basePrivilegeCalculator = buildEffectiveBasePrivilegeCalculator(role);
|
||||
const featurePrivilegeCalculator = buildEffectiveFeaturePrivilegeCalculator(role);
|
||||
|
||||
const baseExplanation = basePrivilegeCalculator.getMostPermissiveBasePrivilege(
|
||||
role.kibana[privilegeIndex],
|
||||
// If calculations wish to ignoreAssigned, then we still need to know what the real effective base privilege is
|
||||
// without ignoring assigned, in order to calculate the correct feature privileges.
|
||||
false
|
||||
);
|
||||
|
||||
const actualResult = featurePrivilegeCalculator.getMostPermissiveFeaturePrivilege(
|
||||
role.kibana[privilegeIndex],
|
||||
baseExplanation,
|
||||
feature,
|
||||
ignoreAssigned
|
||||
);
|
||||
|
||||
expect(actualResult).toEqual(result);
|
||||
});
|
||||
}
|
||||
|
||||
describe('getMostPermissiveFeaturePrivilege', () => {
|
||||
describe('for global feature privileges, without ignoring assigned', () => {
|
||||
runTest('returns "none" when no privileges are granted', {
|
||||
result: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "read" when assigned directly to the feature', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "read" when assigned as the global base privilege', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
});
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global base privilege, which overrides assigned feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the feature privilege, which does not override assigned global base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('for global feature privileges, ignoring assigned', () => {
|
||||
runTest('returns "none" when no privileges are granted', {
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "none" when "read" is assigned directly to the feature', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "read" when assigned as the global base privilege', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
});
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global base privilege, which overrides assigned feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "read" when "all" assigned as the feature privilege, which does not override assigned global base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('for space feature privileges, without ignoring assigned', () => {
|
||||
runTest('returns "none" when no privileges are granted', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
result: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "read" when assigned directly to the feature', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "read" when assigned as the global base privilege', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
});
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global base privilege, which overrides assigned global feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global base privilege, which overrides assigned space feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global feature privilege, which does not override assigned global base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the space feature privilege, which does not override assigned global base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the space base privilege, which does not override assigned global base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global base privilege, which overrides assigned space feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest('returns "all" when assigned everywhere, without indicating override', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase: false,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "all" when assigned at global feature, overriding space feature', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
},
|
||||
});
|
||||
|
||||
describe('feature with "all" excluded from base privileges', () => {
|
||||
runTest('returns "read" when "all" assigned as the global base privilege', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
feature: 'feature4',
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
});
|
||||
|
||||
runTest(
|
||||
'returns "read" when "all" assigned as the global base privilege, which does not override assigned space feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature4: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
feature: 'feature4',
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the feature privilege, which is more permissive than the base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature4: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
feature: 'feature4',
|
||||
privilegeIndex: 1,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for space feature privileges, ignoring assigned', () => {
|
||||
runTest('returns "none" when no privileges are granted', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "none" when "read" assigned directly to the feature', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "read" when assigned as the global base privilege', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
});
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global base privilege, which overrides assigned global feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global base privilege, which normally overrides assigned space feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global feature privilege, which does not override assigned global base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "read" when "all" assigned as the space feature privilege, which normally overrides assigned global base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the space base privilege, which does not override assigned global base privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest(
|
||||
'returns "all" when assigned as the global base privilege, which normally overrides assigned space feature privilege',
|
||||
{
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
runTest('returns "all" when assigned everywhere, without indicating override', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
});
|
||||
|
||||
runTest('returns "all" when assigned at global feature, normally overriding space feature', {
|
||||
role: {
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['marketing'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privilegeIndex: 1,
|
||||
ignoreAssigned: true,
|
||||
result: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,209 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
FeaturesPrivileges,
|
||||
KibanaPrivileges,
|
||||
RoleKibanaPrivilege,
|
||||
} from '../../../../../../../common/model';
|
||||
import { areActionsFullyCovered } from '../../../../../../../common/privilege_calculator_utils';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import {
|
||||
PRIVILEGE_SOURCE,
|
||||
PrivilegeExplanation,
|
||||
PrivilegeScenario,
|
||||
} from './kibana_privilege_calculator_types';
|
||||
|
||||
export class KibanaFeaturePrivilegeCalculator {
|
||||
constructor(
|
||||
private readonly kibanaPrivileges: KibanaPrivileges,
|
||||
private readonly globalPrivilege: RoleKibanaPrivilege,
|
||||
private readonly assignedGlobalBaseActions: string[],
|
||||
private readonly rankedFeaturePrivileges: FeaturesPrivileges
|
||||
) {}
|
||||
|
||||
public getMostPermissiveFeaturePrivilege(
|
||||
privilegeSpec: RoleKibanaPrivilege,
|
||||
basePrivilegeExplanation: PrivilegeExplanation,
|
||||
featureId: string,
|
||||
ignoreAssigned: boolean
|
||||
): PrivilegeExplanation {
|
||||
const scenarios = this.buildFeaturePrivilegeScenarios(
|
||||
privilegeSpec,
|
||||
basePrivilegeExplanation,
|
||||
featureId,
|
||||
ignoreAssigned
|
||||
);
|
||||
|
||||
const featurePrivileges = this.rankedFeaturePrivileges[featureId] || [];
|
||||
|
||||
// inspect feature privileges in ranked order (most permissive -> least permissive)
|
||||
for (const featurePrivilege of featurePrivileges) {
|
||||
const actions = this.kibanaPrivileges
|
||||
.getFeaturePrivileges()
|
||||
.getActions(featureId, featurePrivilege);
|
||||
|
||||
// check if any of the scenarios satisfy the privilege - first one wins.
|
||||
for (const scenario of scenarios) {
|
||||
if (areActionsFullyCovered(scenario.actions, actions)) {
|
||||
return {
|
||||
actualPrivilege: featurePrivilege,
|
||||
actualPrivilegeSource: scenario.actualPrivilegeSource,
|
||||
isDirectlyAssigned: scenario.isDirectlyAssigned,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase:
|
||||
scenario.directlyAssignedFeaturePrivilegeMorePermissiveThanBase,
|
||||
...this.buildSupercededFields(
|
||||
!scenario.isDirectlyAssigned,
|
||||
scenario.supersededPrivilege,
|
||||
scenario.supersededPrivilegeSource
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isGlobal = isGlobalPrivilegeDefinition(privilegeSpec);
|
||||
return {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: isGlobal
|
||||
? PRIVILEGE_SOURCE.GLOBAL_FEATURE
|
||||
: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
};
|
||||
}
|
||||
|
||||
private buildFeaturePrivilegeScenarios(
|
||||
privilegeSpec: RoleKibanaPrivilege,
|
||||
basePrivilegeExplanation: PrivilegeExplanation,
|
||||
featureId: string,
|
||||
ignoreAssigned: boolean
|
||||
): PrivilegeScenario[] {
|
||||
const scenarios: PrivilegeScenario[] = [];
|
||||
|
||||
const isGlobalPrivilege = isGlobalPrivilegeDefinition(privilegeSpec);
|
||||
|
||||
const assignedGlobalFeaturePrivilege = this.getAssignedFeaturePrivilege(
|
||||
this.globalPrivilege,
|
||||
featureId
|
||||
);
|
||||
|
||||
const assignedFeaturePrivilege = this.getAssignedFeaturePrivilege(privilegeSpec, featureId);
|
||||
const hasAssignedFeaturePrivilege =
|
||||
!ignoreAssigned && assignedFeaturePrivilege !== NO_PRIVILEGE_VALUE;
|
||||
|
||||
scenarios.push({
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
actions: [...this.assignedGlobalBaseActions],
|
||||
...this.buildSupercededFields(
|
||||
hasAssignedFeaturePrivilege,
|
||||
assignedFeaturePrivilege,
|
||||
isGlobalPrivilege ? PRIVILEGE_SOURCE.GLOBAL_FEATURE : PRIVILEGE_SOURCE.SPACE_FEATURE
|
||||
),
|
||||
});
|
||||
|
||||
if (!isGlobalPrivilege || !ignoreAssigned) {
|
||||
scenarios.push({
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
actions: this.getFeatureActions(featureId, assignedGlobalFeaturePrivilege),
|
||||
isDirectlyAssigned: isGlobalPrivilege && hasAssignedFeaturePrivilege,
|
||||
...this.buildSupercededFields(
|
||||
hasAssignedFeaturePrivilege && !isGlobalPrivilege,
|
||||
assignedFeaturePrivilege,
|
||||
PRIVILEGE_SOURCE.SPACE_FEATURE
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (isGlobalPrivilege) {
|
||||
return this.rankScenarios(scenarios);
|
||||
}
|
||||
|
||||
// Otherwise, this is a space feature privilege
|
||||
|
||||
const includeSpaceBaseScenario =
|
||||
basePrivilegeExplanation.actualPrivilegeSource === PRIVILEGE_SOURCE.SPACE_BASE ||
|
||||
basePrivilegeExplanation.supersededPrivilegeSource === PRIVILEGE_SOURCE.SPACE_BASE;
|
||||
|
||||
const spaceBasePrivilege =
|
||||
basePrivilegeExplanation.supersededPrivilege || basePrivilegeExplanation.actualPrivilege;
|
||||
|
||||
if (includeSpaceBaseScenario) {
|
||||
scenarios.push({
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
actions: this.getBaseActions(PRIVILEGE_SOURCE.SPACE_BASE, spaceBasePrivilege),
|
||||
...this.buildSupercededFields(
|
||||
hasAssignedFeaturePrivilege,
|
||||
assignedFeaturePrivilege,
|
||||
PRIVILEGE_SOURCE.SPACE_FEATURE
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!ignoreAssigned) {
|
||||
const actions = this.getFeatureActions(
|
||||
featureId,
|
||||
this.getAssignedFeaturePrivilege(privilegeSpec, featureId)
|
||||
);
|
||||
const directlyAssignedFeaturePrivilegeMorePermissiveThanBase = !areActionsFullyCovered(
|
||||
this.assignedGlobalBaseActions,
|
||||
actions
|
||||
);
|
||||
scenarios.push({
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase,
|
||||
actions,
|
||||
});
|
||||
}
|
||||
|
||||
return this.rankScenarios(scenarios);
|
||||
}
|
||||
|
||||
private rankScenarios(scenarios: PrivilegeScenario[]): PrivilegeScenario[] {
|
||||
return scenarios.sort(
|
||||
(scenario1, scenario2) => scenario1.actualPrivilegeSource - scenario2.actualPrivilegeSource
|
||||
);
|
||||
}
|
||||
|
||||
private getBaseActions(source: PRIVILEGE_SOURCE, privilegeId: string) {
|
||||
switch (source) {
|
||||
case PRIVILEGE_SOURCE.GLOBAL_BASE:
|
||||
return this.assignedGlobalBaseActions;
|
||||
case PRIVILEGE_SOURCE.SPACE_BASE:
|
||||
return this.kibanaPrivileges.getSpacesPrivileges().getActions(privilegeId);
|
||||
default:
|
||||
throw new Error(
|
||||
`Cannot get base actions for unsupported privilege source ${PRIVILEGE_SOURCE[source]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getFeatureActions(featureId: string, privilegeId: string) {
|
||||
return this.kibanaPrivileges.getFeaturePrivileges().getActions(featureId, privilegeId);
|
||||
}
|
||||
|
||||
private getAssignedFeaturePrivilege(privilegeSpec: RoleKibanaPrivilege, featureId: string) {
|
||||
const featureEntry = privilegeSpec.feature[featureId] || [];
|
||||
return featureEntry[0] || NO_PRIVILEGE_VALUE;
|
||||
}
|
||||
|
||||
private buildSupercededFields(
|
||||
isSuperceding: boolean,
|
||||
supersededPrivilege?: string,
|
||||
supersededPrivilegeSource?: PRIVILEGE_SOURCE
|
||||
) {
|
||||
if (!isSuperceding) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
supersededPrivilege,
|
||||
supersededPrivilegeSource,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,940 +0,0 @@
|
|||
/*
|
||||
* 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 { KibanaPrivileges, Role } from '../../../../../../../common/model';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import {
|
||||
buildRole,
|
||||
defaultPrivilegeDefinition,
|
||||
fullyRestrictedBasePrivileges,
|
||||
fullyRestrictedFeaturePrivileges,
|
||||
unrestrictedBasePrivileges,
|
||||
unrestrictedFeaturePrivileges,
|
||||
} from './__fixtures__';
|
||||
import {
|
||||
AllowedPrivilege,
|
||||
PRIVILEGE_SOURCE,
|
||||
PrivilegeExplanation,
|
||||
} from './kibana_privilege_calculator_types';
|
||||
import { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory';
|
||||
|
||||
const buildEffectivePrivileges = (
|
||||
role: Role,
|
||||
kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition
|
||||
) => {
|
||||
const factory = new KibanaPrivilegeCalculatorFactory(kibanaPrivileges);
|
||||
return factory.getInstance(role);
|
||||
};
|
||||
|
||||
interface BuildExpectedFeaturePrivilegesOption {
|
||||
features: string[];
|
||||
privilegeExplanation: PrivilegeExplanation;
|
||||
}
|
||||
|
||||
const buildExpectedFeaturePrivileges = (options: BuildExpectedFeaturePrivilegesOption[]) => {
|
||||
return {
|
||||
feature: options.reduce((acc1, option) => {
|
||||
return {
|
||||
...acc1,
|
||||
...option.features.reduce((acc2, featureId) => {
|
||||
return {
|
||||
...acc2,
|
||||
[featureId]: option.privilegeExplanation,
|
||||
};
|
||||
}, {}),
|
||||
};
|
||||
}, {}),
|
||||
};
|
||||
};
|
||||
|
||||
describe('calculateEffectivePrivileges', () => {
|
||||
it(`returns an empty array for an empty role`, () => {
|
||||
const role = buildRole();
|
||||
role.kibana = [];
|
||||
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
expect(calculatedPrivileges).toHaveLength(0);
|
||||
});
|
||||
|
||||
it(`calculates "none" for all privileges when nothing is assigned`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['foo', 'bar'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
expect(calculatedPrivileges).toEqual([
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3', 'feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe(`with global base privilege of "all"`, () => {
|
||||
it(`calculates global feature privilege of all for features 1-3 and read for feature 4`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
|
||||
expect(calculatedPrivileges).toEqual([
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`calculates space base and feature privilege of all for features 1-3 and read for feature 4`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
|
||||
const calculatedSpacePrivileges = calculatedPrivileges[1];
|
||||
|
||||
expect(calculatedSpacePrivileges).toEqual({
|
||||
base: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
describe(`and with feature privileges assigned`, () => {
|
||||
it('returns the base privileges when they are more permissive', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
feature2: ['read'],
|
||||
feature3: ['read'],
|
||||
feature4: ['read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['read'],
|
||||
feature2: ['read'],
|
||||
feature3: ['read'],
|
||||
feature4: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
|
||||
expect(calculatedPrivileges).toEqual([
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`with global base privilege of "read"`, () => {
|
||||
it(`it calculates space base and feature privileges when none are provided`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
|
||||
expect(calculatedPrivileges).toEqual([
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('and with feature privileges assigned', () => {
|
||||
it('returns the feature privileges when they are more permissive', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
feature2: ['all'],
|
||||
feature3: ['all'],
|
||||
feature4: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
feature2: ['all'],
|
||||
feature3: ['all'],
|
||||
feature4: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
|
||||
expect(calculatedPrivileges).toEqual([
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3', 'feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3', 'feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with both global and space base privileges assigned', () => {
|
||||
it(`does not override space base of "all" when global base is "read"`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
|
||||
expect(calculatedPrivileges).toEqual([
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`calculates "all" for space base and space features when superceded by global "all"`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
|
||||
expect(calculatedPrivileges).toEqual([
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`does not override feature privileges when they are more permissive`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
feature2: ['all'],
|
||||
feature3: ['all'],
|
||||
feature4: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const effectivePrivileges = buildEffectivePrivileges(role);
|
||||
const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges();
|
||||
|
||||
expect(calculatedPrivileges).toEqual([
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature3'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: NO_PRIVILEGE_VALUE,
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
features: ['feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
base: {
|
||||
actualPrivilege: 'read',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
},
|
||||
...buildExpectedFeaturePrivileges([
|
||||
{
|
||||
features: ['feature1', 'feature2', 'feature3', 'feature4'],
|
||||
privilegeExplanation: {
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE,
|
||||
isDirectlyAssigned: true,
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase: true,
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateAllowedPrivileges', () => {
|
||||
it('allows all privileges when none are currently assigned', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const privilegeCalculator = buildEffectivePrivileges(role);
|
||||
|
||||
const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...unrestrictedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...unrestrictedFeaturePrivileges,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows all global base privileges, but just "all" for everything else when global is set to "all"', () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const privilegeCalculator = buildEffectivePrivileges(role);
|
||||
|
||||
const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...fullyRestrictedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
...fullyRestrictedBasePrivileges,
|
||||
...fullyRestrictedFeaturePrivileges,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`allows feature privileges to be set to "all" or "read" when global base is "read"`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const privilegeCalculator = buildEffectivePrivileges(role);
|
||||
|
||||
const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges();
|
||||
|
||||
const expectedFeaturePrivileges = {
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: true, // feature 3 has no "read" privilege governed by global "all"
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...expectedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
base: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
...expectedFeaturePrivileges,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`allows feature privileges to be set to "all" or "read" when space base is "read"`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const privilegeCalculator = buildEffectivePrivileges(role);
|
||||
|
||||
const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...unrestrictedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
base: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: true,
|
||||
},
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: true, // feature 3 has no "read" privilege governed by space "all"
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`allows space base privilege to be set to "all" or "read" when space base is already "all"`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const privilegeCalculator = buildEffectivePrivileges(role);
|
||||
|
||||
const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`restricts space feature privileges when global feature privileges are set`, () => {
|
||||
const role = buildRole({
|
||||
spacesPrivileges: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
feature2: ['read'],
|
||||
feature4: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const privilegeCalculator = buildEffectivePrivileges(role);
|
||||
|
||||
const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
...unrestrictedBasePrivileges,
|
||||
...unrestrictedFeaturePrivileges,
|
||||
},
|
||||
{
|
||||
base: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: true,
|
||||
},
|
||||
feature: {
|
||||
feature1: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature2: {
|
||||
privileges: ['all', 'read'],
|
||||
canUnassign: false,
|
||||
},
|
||||
feature3: {
|
||||
privileges: ['all'],
|
||||
canUnassign: true, // feature 3 has no "read" privilege governed by space "all"
|
||||
},
|
||||
feature4: {
|
||||
privileges: ['all'],
|
||||
canUnassign: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,113 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
FeaturesPrivileges,
|
||||
KibanaPrivileges,
|
||||
Role,
|
||||
RoleKibanaPrivilege,
|
||||
} from '../../../../../../../common/model';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { KibanaAllowedPrivilegesCalculator } from './kibana_allowed_privileges_calculator';
|
||||
import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator';
|
||||
import { KibanaFeaturePrivilegeCalculator } from './kibana_feature_privilege_calculator';
|
||||
import { AllowedPrivilege, CalculatedPrivilege } from './kibana_privilege_calculator_types';
|
||||
|
||||
export class KibanaPrivilegeCalculator {
|
||||
private allowedPrivilegesCalculator: KibanaAllowedPrivilegesCalculator;
|
||||
|
||||
private effectiveBasePrivilegesCalculator: KibanaBasePrivilegeCalculator;
|
||||
|
||||
private effectiveFeaturePrivilegesCalculator: KibanaFeaturePrivilegeCalculator;
|
||||
|
||||
constructor(
|
||||
private readonly kibanaPrivileges: KibanaPrivileges,
|
||||
private readonly role: Role,
|
||||
public readonly rankedFeaturePrivileges: FeaturesPrivileges
|
||||
) {
|
||||
const globalPrivilege = this.locateGlobalPrivilege(role);
|
||||
|
||||
const assignedGlobalBaseActions: string[] = globalPrivilege.base[0]
|
||||
? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilege.base[0])
|
||||
: [];
|
||||
|
||||
this.allowedPrivilegesCalculator = new KibanaAllowedPrivilegesCalculator(
|
||||
kibanaPrivileges,
|
||||
role
|
||||
);
|
||||
|
||||
this.effectiveBasePrivilegesCalculator = new KibanaBasePrivilegeCalculator(
|
||||
kibanaPrivileges,
|
||||
globalPrivilege,
|
||||
assignedGlobalBaseActions
|
||||
);
|
||||
|
||||
this.effectiveFeaturePrivilegesCalculator = new KibanaFeaturePrivilegeCalculator(
|
||||
kibanaPrivileges,
|
||||
globalPrivilege,
|
||||
assignedGlobalBaseActions,
|
||||
rankedFeaturePrivileges
|
||||
);
|
||||
}
|
||||
|
||||
public calculateEffectivePrivileges(ignoreAssigned: boolean = false): CalculatedPrivilege[] {
|
||||
const { kibana = [] } = this.role;
|
||||
return kibana.map(privilegeSpec =>
|
||||
this.calculateEffectivePrivilege(privilegeSpec, ignoreAssigned)
|
||||
);
|
||||
}
|
||||
|
||||
public calculateAllowedPrivileges(): AllowedPrivilege[] {
|
||||
const effectivePrivs = this.calculateEffectivePrivileges(true);
|
||||
return this.allowedPrivilegesCalculator.calculateAllowedPrivileges(effectivePrivs);
|
||||
}
|
||||
|
||||
private calculateEffectivePrivilege(
|
||||
privilegeSpec: RoleKibanaPrivilege,
|
||||
ignoreAssigned: boolean
|
||||
): CalculatedPrivilege {
|
||||
const result: CalculatedPrivilege = {
|
||||
base: this.effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(
|
||||
privilegeSpec,
|
||||
ignoreAssigned
|
||||
),
|
||||
feature: {},
|
||||
reserved: privilegeSpec._reserved,
|
||||
};
|
||||
|
||||
// If calculations wish to ignoreAssigned, then we still need to know what the real effective base privilege is
|
||||
// without ignoring assigned, in order to calculate the correct feature privileges.
|
||||
const effectiveBase = ignoreAssigned
|
||||
? this.effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(privilegeSpec, false)
|
||||
: result.base;
|
||||
|
||||
const allFeaturePrivileges = this.kibanaPrivileges.getFeaturePrivileges().getAllPrivileges();
|
||||
result.feature = Object.keys(allFeaturePrivileges).reduce((acc, featureId) => {
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: this.effectiveFeaturePrivilegesCalculator.getMostPermissiveFeaturePrivilege(
|
||||
privilegeSpec,
|
||||
effectiveBase,
|
||||
featureId,
|
||||
ignoreAssigned
|
||||
),
|
||||
};
|
||||
}, {});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private locateGlobalPrivilege(role: Role) {
|
||||
const spacePrivileges = role.kibana;
|
||||
return (
|
||||
spacePrivileges.find(privileges => isGlobalPrivilegeDefinition(privileges)) || {
|
||||
spaces: [] as string[],
|
||||
base: [] as string[],
|
||||
feature: {},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Describes the source of a privilege.
|
||||
*/
|
||||
export enum PRIVILEGE_SOURCE {
|
||||
/** Privilege is assigned directly to the entity */
|
||||
SPACE_FEATURE = 10,
|
||||
|
||||
/** Privilege is derived from space base privilege */
|
||||
SPACE_BASE = 20,
|
||||
|
||||
/** Privilege is derived from global feature privilege */
|
||||
GLOBAL_FEATURE = 30,
|
||||
|
||||
/** Privilege is derived from global base privilege */
|
||||
GLOBAL_BASE = 40,
|
||||
}
|
||||
|
||||
export interface PrivilegeExplanation {
|
||||
actualPrivilege: string;
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE;
|
||||
isDirectlyAssigned: boolean;
|
||||
supersededPrivilege?: string;
|
||||
supersededPrivilegeSource?: PRIVILEGE_SOURCE;
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase?: boolean;
|
||||
}
|
||||
|
||||
export interface CalculatedPrivilege {
|
||||
base: PrivilegeExplanation;
|
||||
feature: {
|
||||
[featureId: string]: PrivilegeExplanation | undefined;
|
||||
};
|
||||
reserved: undefined | string[];
|
||||
}
|
||||
|
||||
export interface PrivilegeScenario {
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE;
|
||||
isDirectlyAssigned: boolean;
|
||||
supersededPrivilege?: string;
|
||||
supersededPrivilegeSource?: PRIVILEGE_SOURCE;
|
||||
actions: string[];
|
||||
directlyAssignedFeaturePrivilegeMorePermissiveThanBase?: boolean;
|
||||
}
|
||||
|
||||
export interface AllowedPrivilege {
|
||||
base: {
|
||||
privileges: string[];
|
||||
canUnassign: boolean;
|
||||
};
|
||||
feature: {
|
||||
[featureId: string]:
|
||||
| {
|
||||
privileges: string[];
|
||||
canUnassign: boolean;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
FeaturesPrivileges,
|
||||
KibanaPrivileges,
|
||||
Role,
|
||||
copyRole,
|
||||
} from '../../../../../../../common/model';
|
||||
import { compareActions } from '../../../../../../../common/privilege_calculator_utils';
|
||||
import { KibanaPrivilegeCalculator } from './kibana_privilege_calculator';
|
||||
|
||||
export class KibanaPrivilegeCalculatorFactory {
|
||||
/** All feature privileges, sorted from most permissive => least permissive. */
|
||||
public readonly rankedFeaturePrivileges: FeaturesPrivileges;
|
||||
|
||||
constructor(private readonly kibanaPrivileges: KibanaPrivileges) {
|
||||
this.rankedFeaturePrivileges = {};
|
||||
const featurePrivilegeSet = kibanaPrivileges.getFeaturePrivileges().getAllPrivileges();
|
||||
|
||||
Object.entries(featurePrivilegeSet).forEach(([featureId, privileges]) => {
|
||||
this.rankedFeaturePrivileges[featureId] = privileges.sort((privilege1, privilege2) => {
|
||||
const privilege1Actions = kibanaPrivileges
|
||||
.getFeaturePrivileges()
|
||||
.getActions(featureId, privilege1);
|
||||
const privilege2Actions = kibanaPrivileges
|
||||
.getFeaturePrivileges()
|
||||
.getActions(featureId, privilege2);
|
||||
return compareActions(privilege1Actions, privilege2Actions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an KibanaPrivilegeCalculator instance for the specified role.
|
||||
* @param role
|
||||
*/
|
||||
public getInstance(role: Role) {
|
||||
const roleCopy = copyRole(role);
|
||||
|
||||
this.sortPrivileges(roleCopy);
|
||||
return new KibanaPrivilegeCalculator(
|
||||
this.kibanaPrivileges,
|
||||
roleCopy,
|
||||
this.rankedFeaturePrivileges
|
||||
);
|
||||
}
|
||||
|
||||
private sortPrivileges(role: Role) {
|
||||
role.kibana.forEach(privilege => {
|
||||
privilege.base.sort((privilege1, privilege2) => {
|
||||
const privilege1Actions = this.kibanaPrivileges
|
||||
.getSpacesPrivileges()
|
||||
.getActions(privilege1);
|
||||
|
||||
const privilege2Actions = this.kibanaPrivileges
|
||||
.getSpacesPrivileges()
|
||||
.getActions(privilege2);
|
||||
|
||||
return compareActions(privilege1Actions, privilege2Actions);
|
||||
});
|
||||
|
||||
Object.entries(privilege.feature).forEach(([featureId, featurePrivs]) => {
|
||||
featurePrivs.sort((privilege1, privilege2) => {
|
||||
const privilege1Actions = this.kibanaPrivileges
|
||||
.getFeaturePrivileges()
|
||||
.getActions(featureId, privilege1);
|
||||
|
||||
const privilege2Actions = this.kibanaPrivileges
|
||||
.getFeaturePrivileges()
|
||||
.getActions(featureId, privilege2);
|
||||
|
||||
return compareActions(privilege1Actions, privilege2Actions);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,12 +6,13 @@
|
|||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../common/model';
|
||||
import { Role } from '../../../../../../common/model';
|
||||
import { RoleValidator } from '../../validate_role';
|
||||
import { KibanaPrivilegesRegion } from './kibana_privileges_region';
|
||||
import { SimplePrivilegeSection } from './simple_privilege_section';
|
||||
import { SpaceAwarePrivilegeSection } from './space_aware_privilege_section';
|
||||
import { TransformErrorSection } from './transform_error_section';
|
||||
import { SpaceAwarePrivilegeSection } from './space_aware_privilege_section';
|
||||
import { KibanaPrivileges } from '../../../model';
|
||||
|
||||
const buildProps = (customProps = {}) => {
|
||||
return {
|
||||
|
@ -39,12 +40,15 @@ const buildProps = (customProps = {}) => {
|
|||
},
|
||||
],
|
||||
features: [],
|
||||
kibanaPrivileges: new KibanaPrivileges({
|
||||
global: {},
|
||||
space: {},
|
||||
features: {},
|
||||
reserved: {},
|
||||
}),
|
||||
kibanaPrivileges: new KibanaPrivileges(
|
||||
{
|
||||
global: {},
|
||||
space: {},
|
||||
features: {},
|
||||
reserved: {},
|
||||
},
|
||||
[]
|
||||
),
|
||||
intl: null as any,
|
||||
uiCapabilities: {
|
||||
navLinks: {},
|
||||
|
@ -57,6 +61,7 @@ const buildProps = (customProps = {}) => {
|
|||
editable: true,
|
||||
onChange: jest.fn(),
|
||||
validator: new RoleValidator(),
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
...customProps,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,21 +7,20 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { Space } from '../../../../../../../spaces/public';
|
||||
import { Feature } from '../../../../../../../features/public';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from './kibana_privilege_calculator';
|
||||
import { Role } from '../../../../../../common/model';
|
||||
import { RoleValidator } from '../../validate_role';
|
||||
import { CollapsiblePanel } from '../../collapsible_panel';
|
||||
import { SimplePrivilegeSection } from './simple_privilege_section';
|
||||
import { SpaceAwarePrivilegeSection } from './space_aware_privilege_section';
|
||||
import { TransformErrorSection } from './transform_error_section';
|
||||
import { KibanaPrivileges } from '../../../model';
|
||||
|
||||
interface Props {
|
||||
role: Role;
|
||||
spacesEnabled: boolean;
|
||||
canCustomizeSubFeaturePrivileges: boolean;
|
||||
spaces?: Space[];
|
||||
uiCapabilities: Capabilities;
|
||||
features: Feature[];
|
||||
editable: boolean;
|
||||
kibanaPrivileges: KibanaPrivileges;
|
||||
onChange: (role: Role) => void;
|
||||
|
@ -42,31 +41,28 @@ export class KibanaPrivilegesRegion extends Component<Props, {}> {
|
|||
kibanaPrivileges,
|
||||
role,
|
||||
spacesEnabled,
|
||||
canCustomizeSubFeaturePrivileges,
|
||||
spaces = [],
|
||||
uiCapabilities,
|
||||
onChange,
|
||||
editable,
|
||||
validator,
|
||||
features,
|
||||
} = this.props;
|
||||
|
||||
if (role._transform_error && role._transform_error.includes('kibana')) {
|
||||
return <TransformErrorSection />;
|
||||
}
|
||||
|
||||
const privilegeCalculatorFactory = new KibanaPrivilegeCalculatorFactory(kibanaPrivileges);
|
||||
|
||||
if (spacesEnabled) {
|
||||
return (
|
||||
<SpaceAwarePrivilegeSection
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
privilegeCalculatorFactory={privilegeCalculatorFactory}
|
||||
spaces={spaces}
|
||||
uiCapabilities={uiCapabilities}
|
||||
features={features}
|
||||
onChange={onChange}
|
||||
editable={editable}
|
||||
canCustomizeSubFeaturePrivileges={canCustomizeSubFeaturePrivileges}
|
||||
validator={validator}
|
||||
/>
|
||||
);
|
||||
|
@ -74,11 +70,10 @@ export class KibanaPrivilegesRegion extends Component<Props, {}> {
|
|||
return (
|
||||
<SimplePrivilegeSection
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
features={features}
|
||||
role={role}
|
||||
privilegeCalculatorFactory={privilegeCalculatorFactory}
|
||||
onChange={onChange}
|
||||
editable={editable}
|
||||
canCustomizeSubFeaturePrivileges={canCustomizeSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,5 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory';
|
||||
export * from './kibana_privilege_calculator_types';
|
||||
export { PrivilegeFormCalculator } from './privilege_form_calculator';
|
|
@ -0,0 +1,833 @@
|
|||
/*
|
||||
* 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 { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
|
||||
import { kibanaFeatures } from '../../../../__fixtures__/kibana_features';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { PrivilegeFormCalculator } from './privilege_form_calculator';
|
||||
|
||||
const createRole = (kibana: Role['kibana'] = []): Role => {
|
||||
return {
|
||||
name: 'unit test role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana,
|
||||
};
|
||||
};
|
||||
|
||||
describe('PrivilegeFormCalculator', () => {
|
||||
describe('#getBasePrivilege', () => {
|
||||
it(`returns undefined when no base privilege is assigned`, () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getBasePrivilege(0)).toBeUndefined();
|
||||
});
|
||||
|
||||
it(`ignores unknown base privileges`, () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['unknown'],
|
||||
feature: {},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getBasePrivilege(0)).toBeUndefined();
|
||||
});
|
||||
|
||||
it(`returns the assigned base privilege`, () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getBasePrivilege(0)).toMatchObject({
|
||||
id: 'read',
|
||||
});
|
||||
});
|
||||
|
||||
it(`returns the most permissive base privilege when multiple are assigned`, () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read', 'all'],
|
||||
feature: {},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getBasePrivilege(0)).toMatchObject({
|
||||
id: 'all',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDisplayedPrimaryFeaturePrivilegeId', () => {
|
||||
it('returns undefined when no privileges are assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.getDisplayedPrimaryFeaturePrivilegeId('with_sub_features', 0)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the effective privilege id when a base privilege is assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getDisplayedPrimaryFeaturePrivilegeId('with_sub_features', 0)).toEqual(
|
||||
'all'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the most permissive assigned primary feature privilege id', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['read', 'all', 'minimal_read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getDisplayedPrimaryFeaturePrivilegeId('with_sub_features', 0)).toEqual(
|
||||
'all'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the primary version of the minimal privilege id when assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getDisplayedPrimaryFeaturePrivilegeId('with_sub_features', 0)).toEqual(
|
||||
'read'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasCustomizedSubFeaturePrivileges', () => {
|
||||
it('returns false when no privileges are assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.hasCustomizedSubFeaturePrivileges('with_sub_features', 0)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when there are no sub-feature privileges assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.hasCustomizedSubFeaturePrivileges('with_sub_features', 0)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when the assigned sub-features are also granted by other assigned privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all', 'cool_all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.hasCustomizedSubFeaturePrivileges('with_sub_features', 0)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when the assigned sub-features are not also granted by other assigned privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['read', 'cool_all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.hasCustomizedSubFeaturePrivileges('with_sub_features', 0)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true when a minimal primary feature privilege is assigned, whose corresponding primary grants sub-feature privileges which are not assigned ', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.hasCustomizedSubFeaturePrivileges('with_sub_features', 0)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when a minimal primary feature privilege is assigned, whose corresponding primary grants sub-feature privileges which are all assigned ', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read', 'cool_read', 'cool_toggle_2'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.hasCustomizedSubFeaturePrivileges('with_sub_features', 0)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when a minimal primary feature privilege is assigned, whose corresponding primary does not grant all assigned sub-feature privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: [
|
||||
'minimal_read',
|
||||
'cool_read',
|
||||
'cool_toggle_2',
|
||||
'cool_excluded_toggle',
|
||||
],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.hasCustomizedSubFeaturePrivileges('with_sub_features', 0)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when a base privilege is assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.hasCustomizedSubFeaturePrivileges('with_sub_features', 0)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEffectivePrimaryFeaturePrivilege', () => {
|
||||
it('returns undefined when no privileges are assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.getEffectivePrimaryFeaturePrivilege('with_sub_features', 0)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the most permissive feature privilege granted by the assigned base privilege', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectivePrimaryFeaturePrivilege('with_sub_features', 0)).toMatchObject({
|
||||
id: 'read',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the most permissive feature privilege granted by the assigned feature privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {
|
||||
with_sub_features: ['read', 'all', 'minimal_all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectivePrimaryFeaturePrivilege('with_sub_features', 0)).toMatchObject({
|
||||
id: 'all',
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers `read` primary over `mininal_all`', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_all', 'read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectivePrimaryFeaturePrivilege('with_sub_features', 0)).toMatchObject({
|
||||
id: 'read',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the minimal primary feature privilege when assigned and not superseded', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectivePrimaryFeaturePrivilege('with_sub_features', 0)).toMatchObject({
|
||||
id: 'minimal_all',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores unknown privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['unknown'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.getEffectivePrimaryFeaturePrivilege('with_sub_features', 0)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isIndependentSubFeaturePrivilegeGranted', () => {
|
||||
it('returns false when no privileges are assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.isIndependentSubFeaturePrivilegeGranted('with_sub_features', 'cool_toggle_1', 0)
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when an excluded sub-feature privilege is not directly assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.isIndependentSubFeaturePrivilegeGranted(
|
||||
'with_sub_features',
|
||||
'cool_excluded_toggle',
|
||||
0
|
||||
)
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when an excluded sub-feature privilege is directly assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: ['all', 'cool_excluded_toggle'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.isIndependentSubFeaturePrivilegeGranted(
|
||||
'with_sub_features',
|
||||
'cool_excluded_toggle',
|
||||
0
|
||||
)
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true when a sub-feature privilege is directly assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: ['all', 'cool_toggle_1'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.isIndependentSubFeaturePrivilegeGranted('with_sub_features', 'cool_toggle_1', 0)
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true when a sub-feature privilege is inherited', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.isIndependentSubFeaturePrivilegeGranted('with_sub_features', 'cool_toggle_1', 0)
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getSelectedMutuallyExclusiveSubFeaturePrivilege', () => {
|
||||
it('returns undefined when no privileges are assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||
const coolSubFeature = feature.getSubFeatures().find(sf => sf.name === 'Cool Sub Feature')!;
|
||||
const subFeatureGroup = coolSubFeature
|
||||
.getPrivilegeGroups()
|
||||
.find(pg => pg.groupType === 'mutually_exclusive')!;
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.getSelectedMutuallyExclusiveSubFeaturePrivilege(
|
||||
'with_sub_features',
|
||||
subFeatureGroup,
|
||||
0
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the inherited privilege when not directly assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||
const coolSubFeature = feature.getSubFeatures().find(sf => sf.name === 'Cool Sub Feature')!;
|
||||
const subFeatureGroup = coolSubFeature
|
||||
.getPrivilegeGroups()
|
||||
.find(pg => pg.groupType === 'mutually_exclusive')!;
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.getSelectedMutuallyExclusiveSubFeaturePrivilege(
|
||||
'with_sub_features',
|
||||
subFeatureGroup,
|
||||
0
|
||||
)
|
||||
).toMatchObject({
|
||||
id: 'cool_all',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the the most permissive effective privilege', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all', 'cool_read', 'cool_all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||
const coolSubFeature = feature.getSubFeatures().find(sf => sf.name === 'Cool Sub Feature')!;
|
||||
const subFeatureGroup = coolSubFeature
|
||||
.getPrivilegeGroups()
|
||||
.find(pg => pg.groupType === 'mutually_exclusive')!;
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(
|
||||
calculator.getSelectedMutuallyExclusiveSubFeaturePrivilege(
|
||||
'with_sub_features',
|
||||
subFeatureGroup,
|
||||
0
|
||||
)
|
||||
).toMatchObject({
|
||||
id: 'cool_all',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#canCustomizeSubFeaturePrivileges', () => {
|
||||
it('returns false if no privileges are assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.canCustomizeSubFeaturePrivileges('with_sub_features', 0)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false if a base privilege is assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.canCustomizeSubFeaturePrivileges('with_sub_features', 0)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true if a minimal privilege is assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.canCustomizeSubFeaturePrivileges('with_sub_features', 0)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true if a primary feature privilege is assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.canCustomizeSubFeaturePrivileges('with_sub_features', 0)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateSelectedFeaturePrivilegesForCustomization', () => {
|
||||
it('returns the privileges unmodified if no primary feature privilege is assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['some-privilege'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
expect(
|
||||
calculator.updateSelectedFeaturePrivilegesForCustomization('with_sub_features', 0, true)
|
||||
).toEqual(['some-privilege']);
|
||||
|
||||
expect(
|
||||
calculator.updateSelectedFeaturePrivilegesForCustomization('with_sub_features', 0, false)
|
||||
).toEqual(['some-privilege']);
|
||||
});
|
||||
|
||||
it('switches to the minimal privilege when customizing, but explicitly grants the sub-feature privileges which were originally inherited', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
expect(
|
||||
calculator.updateSelectedFeaturePrivilegesForCustomization('with_sub_features', 0, true)
|
||||
).toEqual(['minimal_read', 'cool_read', 'cool_toggle_2']);
|
||||
});
|
||||
|
||||
it('switches to the non-minimal privilege when customizing, removing all other privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read', 'cool_read', 'cool_toggle_2'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
expect(
|
||||
calculator.updateSelectedFeaturePrivilegesForCustomization('with_sub_features', 0, false)
|
||||
).toEqual(['read']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasSupersededInheritedPrivileges', () => {
|
||||
// More exhaustive testing is done at the UI layer: `privilege_space_table.test.tsx`
|
||||
it('returns false for the global privilege definition', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
expect(calculator.hasSupersededInheritedPrivileges(1)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when the global privilege is not more permissive', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
expect(calculator.hasSupersededInheritedPrivileges(0)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when the global feature privilege is more permissive', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['read'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
expect(calculator.hasSupersededInheritedPrivileges(0)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true when the global base privilege is more permissive', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
expect(calculator.hasSupersededInheritedPrivileges(0)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when only the global base privilege is assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||
|
||||
expect(calculator.hasSupersededInheritedPrivileges(0)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
* 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 { Role } from '../../../../../../../common/model';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { KibanaPrivileges, SubFeaturePrivilegeGroup } from '../../../../model';
|
||||
|
||||
/**
|
||||
* Calculator responsible for determining the displayed and effective privilege values for the following interfaces:
|
||||
* - <PrivilegeSpaceForm> and children
|
||||
* - <PrivilegeSpaceTable> and children
|
||||
*/
|
||||
export class PrivilegeFormCalculator {
|
||||
constructor(private readonly kibanaPrivileges: KibanaPrivileges, private readonly role: Role) {}
|
||||
|
||||
/**
|
||||
* Returns the assigned base privilege.
|
||||
* If more than one base privilege is assigned, the most permissive privilege will be returned.
|
||||
* If no base privileges are assigned, then this will return `undefined`.
|
||||
*
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
public getBasePrivilege(privilegeIndex: number) {
|
||||
const entry = this.role.kibana[privilegeIndex];
|
||||
|
||||
const basePrivileges = this.kibanaPrivileges.getBasePrivileges(entry);
|
||||
return basePrivileges.find(bp => entry.base.includes(bp.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the *displayed* Primary Feature Privilege for the indicated feature and privilege index.
|
||||
* If the effective primary feature privilege is a "minimal" version, then this returns the corresponding non-minimal version.
|
||||
*
|
||||
* @example
|
||||
* The following kibana privilege entry will return `read`:
|
||||
* ```ts
|
||||
* const entry = {
|
||||
* base: [],
|
||||
* feature: {
|
||||
* some_feature: ['minimal_read'],
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param featureId the feature id to get the Primary Feature KibanaPrivilege for.
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
public getDisplayedPrimaryFeaturePrivilegeId(featureId: string, privilegeIndex: number) {
|
||||
return this.getDisplayedPrimaryFeaturePrivilege(featureId, privilegeIndex)?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the indicated feature has sub-feature privilege assignments which differ from the "displayed" primary feature privilege.
|
||||
*
|
||||
* @param featureId the feature id
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
public hasCustomizedSubFeaturePrivileges(featureId: string, privilegeIndex: number) {
|
||||
const feature = this.kibanaPrivileges.getSecuredFeature(featureId);
|
||||
|
||||
const displayedPrimary = this.getDisplayedPrimaryFeaturePrivilege(featureId, privilegeIndex);
|
||||
|
||||
const formPrivileges = this.kibanaPrivileges.createCollectionFromRoleKibanaPrivileges([
|
||||
this.role.kibana[privilegeIndex],
|
||||
]);
|
||||
|
||||
return feature.getSubFeaturePrivileges().some(sfp => {
|
||||
const isGranted = formPrivileges.grantsPrivilege(sfp);
|
||||
const isGrantedByDisplayedPrimary = displayedPrimary?.grantsPrivilege(sfp) ?? isGranted;
|
||||
|
||||
return isGranted !== isGrantedByDisplayedPrimary;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most permissive effective Primary Feature KibanaPrivilege, including the minimal versions.
|
||||
*
|
||||
* @param featureId the feature id
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
public getEffectivePrimaryFeaturePrivilege(featureId: string, privilegeIndex: number) {
|
||||
const feature = this.kibanaPrivileges.getSecuredFeature(featureId);
|
||||
|
||||
const basePrivilege = this.getBasePrivilege(privilegeIndex);
|
||||
|
||||
const selectedFeaturePrivileges = this.getSelectedFeaturePrivileges(featureId, privilegeIndex);
|
||||
|
||||
return feature
|
||||
.getPrimaryFeaturePrivileges({ includeMinimalFeaturePrivileges: true })
|
||||
.find(fp => {
|
||||
return selectedFeaturePrivileges.includes(fp.id) || basePrivilege?.grantsPrivilege(fp);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the indicated sub-feature privilege is granted.
|
||||
*
|
||||
* @param featureId the feature id
|
||||
* @param privilegeId the sub feature privilege id
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
public isIndependentSubFeaturePrivilegeGranted(
|
||||
featureId: string,
|
||||
privilegeId: string,
|
||||
privilegeIndex: number
|
||||
) {
|
||||
const kibanaPrivilege = this.role.kibana[privilegeIndex];
|
||||
|
||||
const feature = this.kibanaPrivileges.getSecuredFeature(featureId);
|
||||
const subFeaturePrivilege = feature
|
||||
.getSubFeaturePrivileges()
|
||||
.find(ap => ap.id === privilegeId)!;
|
||||
|
||||
const assignedPrivileges = this.kibanaPrivileges.createCollectionFromRoleKibanaPrivileges([
|
||||
kibanaPrivilege,
|
||||
]);
|
||||
|
||||
return assignedPrivileges.grantsPrivilege(subFeaturePrivilege);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most permissive effective privilege within the indicated mutually-exclusive sub feature privilege group.
|
||||
*
|
||||
* @param featureId the feature id
|
||||
* @param subFeatureGroup the mutually-exclusive sub feature group
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
public getSelectedMutuallyExclusiveSubFeaturePrivilege(
|
||||
featureId: string,
|
||||
subFeatureGroup: SubFeaturePrivilegeGroup,
|
||||
privilegeIndex: number
|
||||
) {
|
||||
const kibanaPrivilege = this.role.kibana[privilegeIndex];
|
||||
const assignedPrivileges = this.kibanaPrivileges.createCollectionFromRoleKibanaPrivileges([
|
||||
kibanaPrivilege,
|
||||
]);
|
||||
|
||||
return subFeatureGroup.privileges.find(p => {
|
||||
return assignedPrivileges.grantsPrivilege(p);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the indicated feature is capable of having its sub-feature privileges customized.
|
||||
*
|
||||
* @param featureId the feature id
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
public canCustomizeSubFeaturePrivileges(featureId: string, privilegeIndex: number) {
|
||||
const selectedFeaturePrivileges = this.getSelectedFeaturePrivileges(featureId, privilegeIndex);
|
||||
const feature = this.kibanaPrivileges.getSecuredFeature(featureId);
|
||||
|
||||
return feature
|
||||
.getPrimaryFeaturePrivileges({ includeMinimalFeaturePrivileges: true })
|
||||
.some(apfp => selectedFeaturePrivileges.includes(apfp.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an updated set of feature privileges based on the toggling of the "Customize sub-feature privileges" control.
|
||||
*
|
||||
* @param featureId the feature id
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
* @param willBeCustomizing flag indicating if this feature is about to have its sub-feature privileges customized or not
|
||||
*/
|
||||
public updateSelectedFeaturePrivilegesForCustomization(
|
||||
featureId: string,
|
||||
privilegeIndex: number,
|
||||
willBeCustomizing: boolean
|
||||
) {
|
||||
const primary = this.getDisplayedPrimaryFeaturePrivilege(featureId, privilegeIndex);
|
||||
const selectedFeaturePrivileges = this.getSelectedFeaturePrivileges(featureId, privilegeIndex);
|
||||
|
||||
if (!primary) {
|
||||
return selectedFeaturePrivileges;
|
||||
}
|
||||
|
||||
const nextPrivileges = [];
|
||||
|
||||
if (willBeCustomizing) {
|
||||
const feature = this.kibanaPrivileges.getSecuredFeature(featureId);
|
||||
|
||||
const startingPrivileges = feature
|
||||
.getSubFeaturePrivileges()
|
||||
.filter(ap => primary.grantsPrivilege(ap))
|
||||
.map(p => p.id);
|
||||
|
||||
nextPrivileges.push(primary.getMinimalPrivilegeId(), ...startingPrivileges);
|
||||
} else {
|
||||
nextPrivileges.push(primary.id);
|
||||
}
|
||||
|
||||
return nextPrivileges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the indicated privilege entry is less permissive than the configured "global" entry for the role.
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
public hasSupersededInheritedPrivileges(privilegeIndex: number) {
|
||||
const global = this.locateGlobalPrivilege(this.role);
|
||||
|
||||
const entry = this.role.kibana[privilegeIndex];
|
||||
|
||||
if (isGlobalPrivilegeDefinition(entry) || !global) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const globalPrivileges = this.kibanaPrivileges.createCollectionFromRoleKibanaPrivileges([
|
||||
global,
|
||||
]);
|
||||
|
||||
const formPrivileges = this.kibanaPrivileges.createCollectionFromRoleKibanaPrivileges([entry]);
|
||||
|
||||
const hasAssignedBasePrivileges = this.kibanaPrivileges
|
||||
.getBasePrivileges(entry)
|
||||
.some(base => entry.base.includes(base.id));
|
||||
|
||||
const featuresWithDirectlyAssignedPrivileges = this.kibanaPrivileges
|
||||
.getSecuredFeatures()
|
||||
.filter(feature =>
|
||||
feature
|
||||
.getAllPrivileges()
|
||||
.some(privilege => entry.feature[feature.id]?.includes(privilege.id))
|
||||
);
|
||||
|
||||
const hasSupersededBasePrivileges =
|
||||
hasAssignedBasePrivileges &&
|
||||
this.kibanaPrivileges
|
||||
.getBasePrivileges(entry)
|
||||
.some(
|
||||
privilege =>
|
||||
globalPrivileges.grantsPrivilege(privilege) &&
|
||||
!formPrivileges.grantsPrivilege(privilege)
|
||||
);
|
||||
|
||||
const hasSupersededFeaturePrivileges = featuresWithDirectlyAssignedPrivileges.some(feature =>
|
||||
feature
|
||||
.getAllPrivileges()
|
||||
.some(fp => globalPrivileges.grantsPrivilege(fp) && !formPrivileges.grantsPrivilege(fp))
|
||||
);
|
||||
|
||||
return hasSupersededBasePrivileges || hasSupersededFeaturePrivileges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the *displayed* Primary Feature Privilege for the indicated feature and privilege index.
|
||||
* If the effective primary feature privilege is a "minimal" version, then this returns the corresponding non-minimal version.
|
||||
*
|
||||
* @example
|
||||
* The following kibana privilege entry will return `read`:
|
||||
* ```ts
|
||||
* const entry = {
|
||||
* base: [],
|
||||
* feature: {
|
||||
* some_feature: ['minimal_read'],
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param featureId the feature id to get the Primary Feature KibanaPrivilege for.
|
||||
* @param privilegeIndex the index of the kibana privileges role component
|
||||
*/
|
||||
private getDisplayedPrimaryFeaturePrivilege(featureId: string, privilegeIndex: number) {
|
||||
const feature = this.kibanaPrivileges.getSecuredFeature(featureId);
|
||||
|
||||
const basePrivilege = this.getBasePrivilege(privilegeIndex);
|
||||
|
||||
const selectedFeaturePrivileges = this.getSelectedFeaturePrivileges(featureId, privilegeIndex);
|
||||
|
||||
return feature.getPrimaryFeaturePrivileges().find(fp => {
|
||||
const correspondingMinimalPrivilegeId = fp.getMinimalPrivilegeId();
|
||||
|
||||
const correspendingMinimalPrivilege = feature
|
||||
.getMinimalFeaturePrivileges()
|
||||
.find(mp => mp.id === correspondingMinimalPrivilegeId)!;
|
||||
|
||||
// There are two cases where the minimal privileges aren't available:
|
||||
// 1. The feature has no registered sub-features
|
||||
// 2. Sub-feature privileges cannot be customized. When this is the case, the minimal privileges aren't registered with ES,
|
||||
// so they end up represented in the UI as an empty privilege. Empty privileges cannot be granted other privileges, so if we
|
||||
// encounter a minimal privilege that isn't granted by it's correspending primary, then we know we've encountered this scenario.
|
||||
const hasMinimalPrivileges =
|
||||
feature.subFeatures.length > 0 && fp.grantsPrivilege(correspendingMinimalPrivilege);
|
||||
return (
|
||||
selectedFeaturePrivileges.includes(fp.id) ||
|
||||
(hasMinimalPrivileges &&
|
||||
selectedFeaturePrivileges.includes(correspondingMinimalPrivilegeId)) ||
|
||||
basePrivilege?.grantsPrivilege(fp)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private getSelectedFeaturePrivileges(featureId: string, privilegeIndex: number) {
|
||||
return this.role.kibana[privilegeIndex].feature[featureId] ?? [];
|
||||
}
|
||||
|
||||
private locateGlobalPrivilege(role: Role) {
|
||||
return role.kibana.find(entry => isGlobalPrivilegeDefinition(entry));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 { ReactWrapper } from 'enzyme';
|
||||
|
||||
import { EuiTableRow } from '@elastic/eui';
|
||||
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { Role, RoleKibanaPrivilege } from '../../../../../../../../common/model';
|
||||
import { PrivilegeSummaryExpandedRow } from '../privilege_summary_expanded_row';
|
||||
import { FeatureTableCell } from '../../feature_table_cell';
|
||||
|
||||
interface DisplayedFeaturePrivileges {
|
||||
[featureId: string]: {
|
||||
[spaceGroup: string]: {
|
||||
primaryFeaturePrivilege: string;
|
||||
subFeaturesPrivileges: {
|
||||
[subFeatureName: string]: string[];
|
||||
};
|
||||
hasCustomizedSubFeaturePrivileges: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const getSpaceKey = (entry: RoleKibanaPrivilege) => entry.spaces.join(', ');
|
||||
|
||||
export function getDisplayedFeaturePrivileges(
|
||||
wrapper: ReactWrapper<any>,
|
||||
role: Role
|
||||
): DisplayedFeaturePrivileges {
|
||||
const allExpanderButtons = findTestSubject(wrapper, 'expandPrivilegeSummaryRow');
|
||||
allExpanderButtons.forEach(button => button.simulate('click'));
|
||||
|
||||
// each expanded row renders its own `EuiTableRow`, so there are 2 rows
|
||||
// for each feature: one for the primary feature privilege, and one for the sub privilege form
|
||||
const rows = wrapper.find(EuiTableRow);
|
||||
|
||||
return rows.reduce((acc, row) => {
|
||||
const expandedRow = row.find(PrivilegeSummaryExpandedRow);
|
||||
if (expandedRow.length > 0) {
|
||||
return {
|
||||
...acc,
|
||||
...getDisplayedSubFeaturePrivileges(acc, expandedRow, role),
|
||||
};
|
||||
} else {
|
||||
const feature = row.find(FeatureTableCell).props().feature;
|
||||
|
||||
const primaryFeaturePrivileges = findTestSubject(row, 'privilegeColumn');
|
||||
|
||||
expect(primaryFeaturePrivileges).toHaveLength(role.kibana.length);
|
||||
|
||||
acc[feature.id] = acc[feature.id] ?? {};
|
||||
|
||||
primaryFeaturePrivileges.forEach((primary, index) => {
|
||||
const key = getSpaceKey(role.kibana[index]);
|
||||
|
||||
acc[feature.id][key] = {
|
||||
...acc[feature.id][key],
|
||||
primaryFeaturePrivilege: primary.text().trim(),
|
||||
hasCustomizedSubFeaturePrivileges:
|
||||
findTestSubject(primary, 'additionalPrivilegesGranted').length > 0,
|
||||
};
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
}, {} as DisplayedFeaturePrivileges);
|
||||
}
|
||||
|
||||
function getDisplayedSubFeaturePrivileges(
|
||||
displayedFeatures: DisplayedFeaturePrivileges,
|
||||
expandedRow: ReactWrapper<any>,
|
||||
role: Role
|
||||
) {
|
||||
const { feature } = expandedRow.props();
|
||||
|
||||
const subFeatureEntries = findTestSubject(expandedRow as ReactWrapper<any>, 'subFeatureEntry');
|
||||
|
||||
displayedFeatures[feature.id] = displayedFeatures[feature.id] ?? {};
|
||||
|
||||
subFeatureEntries.forEach(subFeatureEntry => {
|
||||
const subFeatureName = findTestSubject(subFeatureEntry, 'subFeatureName').text();
|
||||
|
||||
const entryElements = findTestSubject(subFeatureEntry as ReactWrapper<any>, 'entry', '|=');
|
||||
|
||||
expect(entryElements).toHaveLength(role.kibana.length);
|
||||
|
||||
role.kibana.forEach((entry, index) => {
|
||||
const key = getSpaceKey(entry);
|
||||
const element = findTestSubject(expandedRow as ReactWrapper<any>, `entry-${index}`);
|
||||
|
||||
const independentPrivileges = element
|
||||
.find('EuiFlexGroup[data-test-subj="independentPrivilege"]')
|
||||
.reduce((acc2, flexGroup) => {
|
||||
const privilegeName = findTestSubject(flexGroup, 'privilegeName').text();
|
||||
const isGranted = flexGroup.exists('EuiIconTip[type="check"]');
|
||||
if (isGranted) {
|
||||
return [...acc2, privilegeName];
|
||||
}
|
||||
return acc2;
|
||||
}, [] as string[]);
|
||||
|
||||
const mutuallyExclusivePrivileges = element
|
||||
.find('EuiFlexGroup[data-test-subj="mutexPrivilege"]')
|
||||
.reduce((acc2, flexGroup) => {
|
||||
const privilegeName = findTestSubject(flexGroup, 'privilegeName').text();
|
||||
const isGranted = flexGroup.exists('EuiIconTip[type="check"]');
|
||||
|
||||
if (isGranted) {
|
||||
return [...acc2, privilegeName];
|
||||
}
|
||||
return acc2;
|
||||
}, [] as string[]);
|
||||
|
||||
displayedFeatures[feature.id][key] = {
|
||||
...displayedFeatures[feature.id][key],
|
||||
subFeaturesPrivileges: {
|
||||
...displayedFeatures[feature.id][key].subFeaturesPrivileges,
|
||||
[subFeatureName]: [...independentPrivileges, ...mutuallyExclusivePrivileges],
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return displayedFeatures;
|
||||
}
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { KibanaPrivileges } from './kibana_privileges';
|
||||
export { PrivilegeSummary } from './privilege_summary';
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
|
||||
import { kibanaFeatures } from '../../../../__fixtures__/kibana_features';
|
||||
import { RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import { PrivilegeSummary } from '.';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { PrivilegeSummaryTable } from './privilege_summary_table';
|
||||
|
||||
const createRole = (roleKibanaPrivileges: RoleKibanaPrivilege[]) => ({
|
||||
name: 'some-role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: roleKibanaPrivileges,
|
||||
});
|
||||
|
||||
const spaces = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default Space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
];
|
||||
|
||||
describe('PrivilegeSummary', () => {
|
||||
it('initially renders a button', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['default'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummary
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(findTestSubject(wrapper, 'viewPrivilegeSummaryButton')).toHaveLength(1);
|
||||
expect(wrapper.find(PrivilegeSummaryTable)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('clicking the button renders the privilege summary table', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['default'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummary
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={true}
|
||||
/>
|
||||
);
|
||||
|
||||
findTestSubject(wrapper, 'viewPrivilegeSummaryButton').simulate('click');
|
||||
expect(wrapper.find(PrivilegeSummaryTable)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 React, { useState, Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiModal,
|
||||
EuiButtonEmpty,
|
||||
EuiOverlayMask,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { Space } from '../../../../../../../../spaces/common/model/space';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { PrivilegeSummaryTable } from './privilege_summary_table';
|
||||
import { KibanaPrivileges } from '../../../../model';
|
||||
|
||||
interface Props {
|
||||
role: Role;
|
||||
spaces: Space[];
|
||||
kibanaPrivileges: KibanaPrivileges;
|
||||
canCustomizeSubFeaturePrivileges: boolean;
|
||||
}
|
||||
export const PrivilegeSummary = (props: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButtonEmpty onClick={() => setIsOpen(true)} data-test-subj="viewPrivilegeSummaryButton">
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.privilegeSummary.viewSummaryButtonText"
|
||||
defaultMessage="View privilege summary"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
{isOpen && (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={() => setIsOpen(false)} maxWidth={false}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.privilegeSummary.modalHeaderTitle"
|
||||
defaultMessage="Privilege summary"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<PrivilegeSummaryTable
|
||||
role={props.role}
|
||||
spaces={props.spaces}
|
||||
kibanaPrivileges={props.kibanaPrivileges}
|
||||
canCustomizeSubFeaturePrivileges={props.canCustomizeSubFeaturePrivileges}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton onClick={() => setIsOpen(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.privilegeSummary.closeSummaryButtonText"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
* 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 { Role } from '../../../../../../../common/model';
|
||||
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
|
||||
import { kibanaFeatures } from '../../../../__fixtures__/kibana_features';
|
||||
import { PrivilegeSummaryCalculator } from './privilege_summary_calculator';
|
||||
|
||||
const createRole = (kibana: Role['kibana'] = []): Role => {
|
||||
return {
|
||||
name: 'unit test role',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
kibana,
|
||||
};
|
||||
};
|
||||
describe('PrivilegeSummaryCalculator', () => {
|
||||
describe('#getEffectiveFeaturePrivileges', () => {
|
||||
it('returns an empty privilege set when nothing is assigned', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectiveFeaturePrivileges(role.kibana[0])).toEqual({
|
||||
excluded_from_base: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates effective privileges when inherited from the global privilege', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectiveFeaturePrivileges(role.kibana[0])).toEqual({
|
||||
excluded_from_base: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: ['cool_all', 'cool_read', 'cool_toggle_1', 'cool_toggle_2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates effective privileges when there are non-superseded sub-feature privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['cool_excluded_toggle'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: [],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectiveFeaturePrivileges(role.kibana[0])).toEqual({
|
||||
excluded_from_base: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: true,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [
|
||||
'cool_all',
|
||||
'cool_read',
|
||||
'cool_toggle_1',
|
||||
'cool_toggle_2',
|
||||
'cool_excluded_toggle',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates privileges for all features for a space entry', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {
|
||||
excluded_from_base: ['all'],
|
||||
no_sub_features: ['read'],
|
||||
with_excluded_sub_features: ['all'],
|
||||
with_sub_features: ['minimal_read', 'cool_excluded_toggle'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectiveFeaturePrivileges(role.kibana[0])).toEqual({
|
||||
excluded_from_base: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: ['cool_all', 'cool_read', 'cool_toggle_1', 'cool_toggle_2'],
|
||||
},
|
||||
no_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: true,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [
|
||||
'cool_all',
|
||||
'cool_read',
|
||||
'cool_toggle_1',
|
||||
'cool_toggle_2',
|
||||
'cool_excluded_toggle',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates privileges for all features for a global entry', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectiveFeaturePrivileges(role.kibana[0])).toEqual({
|
||||
excluded_from_base: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: ['cool_all', 'cool_read', 'cool_toggle_1', 'cool_toggle_2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates privileges for a single feature at a space entry', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_excluded_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['foo'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectiveFeaturePrivileges(role.kibana[0])).toEqual({
|
||||
excluded_from_base: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates privileges for a single feature at the global entry', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_excluded_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(kibanaPrivileges, role);
|
||||
expect(calculator.getEffectiveFeaturePrivileges(role.kibana[0])).toEqual({
|
||||
excluded_from_base: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: expect.objectContaining({
|
||||
id: 'all',
|
||||
}),
|
||||
subFeature: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primary: undefined,
|
||||
subFeature: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { KibanaPrivileges, PrimaryFeaturePrivilege, SecuredFeature } from '../../../../model';
|
||||
import { PrivilegeCollection } from '../../../../model/privilege_collection';
|
||||
|
||||
export interface EffectiveFeaturePrivileges {
|
||||
[featureId: string]: {
|
||||
primary?: PrimaryFeaturePrivilege;
|
||||
subFeature: string[];
|
||||
hasCustomizedSubFeaturePrivileges: boolean;
|
||||
};
|
||||
}
|
||||
export class PrivilegeSummaryCalculator {
|
||||
constructor(private readonly kibanaPrivileges: KibanaPrivileges, private readonly role: Role) {}
|
||||
|
||||
public getEffectiveFeaturePrivileges(entry: RoleKibanaPrivilege): EffectiveFeaturePrivileges {
|
||||
const assignedPrivileges = this.collectAssignedPrivileges(entry);
|
||||
|
||||
const features = this.kibanaPrivileges.getSecuredFeatures();
|
||||
|
||||
return features.reduce((acc, feature) => {
|
||||
const displayedPrimaryFeaturePrivilege = this.getDisplayedPrimaryFeaturePrivilege(
|
||||
assignedPrivileges,
|
||||
feature
|
||||
);
|
||||
|
||||
const effectiveSubPrivileges = feature
|
||||
.getSubFeaturePrivileges()
|
||||
.filter(ap => assignedPrivileges.grantsPrivilege(ap));
|
||||
|
||||
const hasCustomizedSubFeaturePrivileges = this.hasCustomizedSubFeaturePrivileges(
|
||||
feature,
|
||||
displayedPrimaryFeaturePrivilege,
|
||||
entry
|
||||
);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[feature.id]: {
|
||||
primary: displayedPrimaryFeaturePrivilege,
|
||||
hasCustomizedSubFeaturePrivileges,
|
||||
subFeature: effectiveSubPrivileges.map(p => p.id),
|
||||
},
|
||||
};
|
||||
}, {} as EffectiveFeaturePrivileges);
|
||||
}
|
||||
|
||||
private hasCustomizedSubFeaturePrivileges(
|
||||
feature: SecuredFeature,
|
||||
displayedPrimaryFeaturePrivilege: PrimaryFeaturePrivilege | undefined,
|
||||
entry: RoleKibanaPrivilege
|
||||
) {
|
||||
const formPrivileges = this.collectAssignedPrivileges(entry);
|
||||
|
||||
return feature.getSubFeaturePrivileges().some(sfp => {
|
||||
const isGranted = formPrivileges.grantsPrivilege(sfp);
|
||||
const isGrantedByDisplayedPrimary =
|
||||
displayedPrimaryFeaturePrivilege?.grantsPrivilege(sfp) ?? isGranted;
|
||||
|
||||
// if displayed primary is derived from base, then excluded sub-feature-privs should not count.
|
||||
return isGranted !== isGrantedByDisplayedPrimary;
|
||||
});
|
||||
}
|
||||
|
||||
private getDisplayedPrimaryFeaturePrivilege(
|
||||
assignedPrivileges: PrivilegeCollection,
|
||||
feature: SecuredFeature
|
||||
) {
|
||||
const primaryFeaturePrivileges = feature.getPrimaryFeaturePrivileges();
|
||||
const minimalPrimaryFeaturePrivileges = feature.getMinimalFeaturePrivileges();
|
||||
|
||||
const hasMinimalPrivileges = feature.subFeatures.length > 0;
|
||||
|
||||
const effectivePrivilege = primaryFeaturePrivileges.find(pfp => {
|
||||
const isPrimaryGranted = assignedPrivileges.grantsPrivilege(pfp);
|
||||
if (!isPrimaryGranted && hasMinimalPrivileges) {
|
||||
const correspondingMinimal = minimalPrimaryFeaturePrivileges.find(
|
||||
mpfp => mpfp.id === pfp.getMinimalPrivilegeId()
|
||||
)!;
|
||||
|
||||
return assignedPrivileges.grantsPrivilege(correspondingMinimal);
|
||||
}
|
||||
return isPrimaryGranted;
|
||||
});
|
||||
|
||||
return effectivePrivilege;
|
||||
}
|
||||
|
||||
private collectAssignedPrivileges(entry: RoleKibanaPrivilege) {
|
||||
if (isGlobalPrivilegeDefinition(entry)) {
|
||||
return this.kibanaPrivileges.createCollectionFromRoleKibanaPrivileges([entry]);
|
||||
}
|
||||
|
||||
const globalPrivilege = this.locateGlobalPrivilege(this.role);
|
||||
return this.kibanaPrivileges.createCollectionFromRoleKibanaPrivileges(
|
||||
globalPrivilege ? [globalPrivilege, entry] : [entry]
|
||||
);
|
||||
}
|
||||
|
||||
private locateGlobalPrivilege(role: Role) {
|
||||
return role.kibana.find(entry => isGlobalPrivilegeDefinition(entry));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIconTip } from '@elastic/eui';
|
||||
import { SecuredFeature, SubFeaturePrivilegeGroup, SubFeaturePrivilege } from '../../../../model';
|
||||
import { EffectiveFeaturePrivileges } from './privilege_summary_calculator';
|
||||
|
||||
interface Props {
|
||||
feature: SecuredFeature;
|
||||
effectiveFeaturePrivileges: Array<EffectiveFeaturePrivileges['featureId']>;
|
||||
}
|
||||
|
||||
export const PrivilegeSummaryExpandedRow = (props: Props) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{props.feature.getSubFeatures().map(subFeature => {
|
||||
return (
|
||||
<EuiFlexItem key={subFeature.name} data-test-subj="subFeatureEntry">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" data-test-subj="subFeatureName">
|
||||
{subFeature.name}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{props.effectiveFeaturePrivileges.map((privs, index) => {
|
||||
return (
|
||||
<EuiFlexItem key={index} data-test-subj={`entry-${index}`}>
|
||||
{subFeature.getPrivilegeGroups().map(renderPrivilegeGroup(privs.subFeature))}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
function renderPrivilegeGroup(effectiveSubFeaturePrivileges: string[]) {
|
||||
return (privilegeGroup: SubFeaturePrivilegeGroup, index: number) => {
|
||||
switch (privilegeGroup.groupType) {
|
||||
case 'independent':
|
||||
return renderIndependentPrivilegeGroup(
|
||||
effectiveSubFeaturePrivileges,
|
||||
privilegeGroup,
|
||||
index
|
||||
);
|
||||
case 'mutually_exclusive':
|
||||
return renderMutuallyExclusivePrivilegeGroup(
|
||||
effectiveSubFeaturePrivileges,
|
||||
privilegeGroup,
|
||||
index
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unsupported privilege group type: ${privilegeGroup.groupType}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function renderIndependentPrivilegeGroup(
|
||||
effectiveSubFeaturePrivileges: string[],
|
||||
privilegeGroup: SubFeaturePrivilegeGroup,
|
||||
index: number
|
||||
) {
|
||||
return (
|
||||
<div key={index}>
|
||||
{privilegeGroup.privileges.map((privilege: SubFeaturePrivilege) => {
|
||||
const isGranted = effectiveSubFeaturePrivileges.includes(privilege.id);
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" data-test-subj="independentPrivilege" key={privilege.id}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
type={isGranted ? 'check' : 'cross'}
|
||||
color={isGranted ? 'primary' : 'danger'}
|
||||
content={
|
||||
isGranted
|
||||
? i18n.translate(
|
||||
'xpack.security.management.editRole.privilegeSummary.privilegeGrantedIconTip',
|
||||
{ defaultMessage: 'Privilege is granted' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.security.management.editRole.privilegeSummary.privilegeNotGrantedIconTip',
|
||||
{ defaultMessage: 'Privilege is not granted' }
|
||||
)
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" data-test-subj="privilegeName">
|
||||
{privilege.name}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderMutuallyExclusivePrivilegeGroup(
|
||||
effectiveSubFeaturePrivileges: string[],
|
||||
privilegeGroup: SubFeaturePrivilegeGroup,
|
||||
index: number
|
||||
) {
|
||||
const firstSelectedPrivilege = privilegeGroup.privileges.find(p =>
|
||||
effectiveSubFeaturePrivileges.includes(p.id)
|
||||
)?.name;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" key={index} data-test-subj="mutexPrivilege">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
type={firstSelectedPrivilege ? 'check' : 'cross'}
|
||||
color={firstSelectedPrivilege ? 'primary' : 'danger'}
|
||||
content={firstSelectedPrivilege ? 'Privilege is granted' : 'Privilege is not granted'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" data-test-subj="privilegeName">
|
||||
{firstSelectedPrivilege ?? 'None'}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,922 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
|
||||
import { kibanaFeatures } from '../../../../__fixtures__/kibana_features';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { PrivilegeSummaryTable } from './privilege_summary_table';
|
||||
import { RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import { getDisplayedFeaturePrivileges } from './__fixtures__';
|
||||
|
||||
const createRole = (roleKibanaPrivileges: RoleKibanaPrivilege[]) => ({
|
||||
name: 'some-role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: roleKibanaPrivileges,
|
||||
});
|
||||
|
||||
const spaces = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default Space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space-1',
|
||||
name: 'First Space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space-2',
|
||||
name: 'Second Space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
];
|
||||
|
||||
const maybeExpectSubFeaturePrivileges = (expect: boolean, subFeaturesPrivileges: unknown) => {
|
||||
return expect ? { subFeaturesPrivileges } : {};
|
||||
};
|
||||
|
||||
const expectNoPrivileges = (displayedPrivileges: any, expectSubFeatures: boolean) => {
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(expectSubFeatures, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(expectSubFeatures, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(expectSubFeatures, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('PrivilegeSummaryTable', () => {
|
||||
[true, false].forEach(allowSubFeaturePrivileges => {
|
||||
describe(`when sub feature privileges are ${
|
||||
allowSubFeaturePrivileges ? 'allowed' : 'disallowed'
|
||||
}`, () => {
|
||||
it('ignores unknown base privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['idk_what_this_means'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expectNoPrivileges(displayedPrivileges, allowSubFeaturePrivileges);
|
||||
});
|
||||
|
||||
it('ignores unknown feature privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['this_doesnt_exist_either'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expectNoPrivileges(displayedPrivileges, allowSubFeaturePrivileges);
|
||||
});
|
||||
|
||||
it('ignores unknown features', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
unknown_feature: ['this_doesnt_exist_either'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expectNoPrivileges(displayedPrivileges, allowSubFeaturePrivileges);
|
||||
});
|
||||
|
||||
it('renders effective privileges for the global base privilege', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 1', 'Cool toggle 2', 'All'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders effective privileges for a global feature privilege', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 1', 'Cool toggle 2', 'All'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders effective privileges for the space base privilege', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['default', 'space-1'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 1', 'Cool toggle 2', 'All'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders effective privileges for a space feature privilege', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read'],
|
||||
},
|
||||
spaces: ['default', 'space-1'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: allowSubFeaturePrivileges,
|
||||
primaryFeaturePrivilege: allowSubFeaturePrivileges ? 'Read' : 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders effective privileges for global base + space base privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['default', 'space-1'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 2', 'Read'],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 1', 'Cool toggle 2', 'All'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders effective privileges for global base + space feature privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read', 'cool_all'],
|
||||
},
|
||||
spaces: ['default', 'space-1'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 2', 'Read'],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: allowSubFeaturePrivileges,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 2', 'All'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders effective privileges for global feature + space base privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read', 'cool_all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
spaces: ['default', 'space-1'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: allowSubFeaturePrivileges,
|
||||
primaryFeaturePrivilege: allowSubFeaturePrivileges ? 'Read' : 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['All'],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: allowSubFeaturePrivileges,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 2', 'All'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders effective privileges for global feature + space feature privileges', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read', 'cool_all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['all'],
|
||||
},
|
||||
spaces: ['default', 'space-1'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: allowSubFeaturePrivileges,
|
||||
primaryFeaturePrivilege: allowSubFeaturePrivileges ? 'Read' : 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['All'],
|
||||
}),
|
||||
},
|
||||
'default, space-1': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 1', 'Cool toggle 2', 'All'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders effective privileges for a complex setup', () => {
|
||||
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, {
|
||||
allowSubFeaturePrivileges,
|
||||
});
|
||||
|
||||
const role = createRole([
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
{
|
||||
base: ['read', 'all'],
|
||||
feature: {},
|
||||
spaces: ['default'],
|
||||
},
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
with_sub_features: ['minimal_read'],
|
||||
with_excluded_sub_features: ['all', 'cool_toggle_1'],
|
||||
no_sub_features: ['all'],
|
||||
excluded_from_base: ['minimal_all', 'cool_toggle_1'],
|
||||
},
|
||||
spaces: ['space-1', 'space-2'],
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeSummaryTable
|
||||
spaces={spaces}
|
||||
kibanaPrivileges={kibanaPrivileges}
|
||||
role={role}
|
||||
canCustomizeSubFeaturePrivileges={allowSubFeaturePrivileges}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role);
|
||||
|
||||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
default: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'space-1, space-2': {
|
||||
hasCustomizedSubFeaturePrivileges: allowSubFeaturePrivileges,
|
||||
primaryFeaturePrivilege: allowSubFeaturePrivileges ? 'All' : 'None',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 2'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
no_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
},
|
||||
default: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
},
|
||||
'space-1, space-2': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
},
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
default: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': [],
|
||||
}),
|
||||
},
|
||||
'space-1, space-2': {
|
||||
hasCustomizedSubFeaturePrivileges: allowSubFeaturePrivileges,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Excluded Sub Feature': ['Cool toggle 1'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
with_sub_features: {
|
||||
'*': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 2', 'Read'],
|
||||
}),
|
||||
},
|
||||
default: {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'All',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 1', 'Cool toggle 2', 'All'],
|
||||
}),
|
||||
},
|
||||
'space-1, space-2': {
|
||||
hasCustomizedSubFeaturePrivileges: false,
|
||||
primaryFeaturePrivilege: 'Read',
|
||||
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
|
||||
'Cool Sub Feature': ['Cool toggle 2', 'Read'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiButtonIcon,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { Space } from '../../../../../../../../spaces/common/model/space';
|
||||
import { Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { FeatureTableCell } from '../feature_table_cell';
|
||||
import { SpaceColumnHeader } from './space_column_header';
|
||||
import { PrivilegeSummaryExpandedRow } from './privilege_summary_expanded_row';
|
||||
import { SecuredFeature, KibanaPrivileges } from '../../../../model';
|
||||
import {
|
||||
PrivilegeSummaryCalculator,
|
||||
EffectiveFeaturePrivileges,
|
||||
} from './privilege_summary_calculator';
|
||||
|
||||
interface Props {
|
||||
role: Role;
|
||||
spaces: Space[];
|
||||
kibanaPrivileges: KibanaPrivileges;
|
||||
canCustomizeSubFeaturePrivileges: boolean;
|
||||
}
|
||||
|
||||
function getColumnKey(entry: RoleKibanaPrivilege) {
|
||||
return `privilege_entry_${entry.spaces.join('|')}`;
|
||||
}
|
||||
|
||||
export const PrivilegeSummaryTable = (props: Props) => {
|
||||
const [expandedFeatures, setExpandedFeatures] = useState<string[]>([]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(props.kibanaPrivileges, props.role);
|
||||
|
||||
const toggleExpandedFeature = (featureId: string) => {
|
||||
if (expandedFeatures.includes(featureId)) {
|
||||
setExpandedFeatures(expandedFeatures.filter(ef => ef !== featureId));
|
||||
} else {
|
||||
setExpandedFeatures([...expandedFeatures, featureId]);
|
||||
}
|
||||
};
|
||||
|
||||
const featureColumn: EuiBasicTableColumn<any> = {
|
||||
name: 'Feature',
|
||||
field: 'feature',
|
||||
render: (feature: any) => {
|
||||
return <FeatureTableCell feature={feature} />;
|
||||
},
|
||||
};
|
||||
const rowExpanderColumn: EuiBasicTableColumn<any> = {
|
||||
align: 'right',
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
field: 'featureId',
|
||||
name: '',
|
||||
render: (featureId: string, record: any) => {
|
||||
const feature = record.feature as SecuredFeature;
|
||||
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
||||
if (!hasSubFeaturePrivileges) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleExpandedFeature(featureId)}
|
||||
data-test-subj={`expandPrivilegeSummaryRow`}
|
||||
aria-label={expandedFeatures.includes(featureId) ? 'Collapse' : 'Expand'}
|
||||
iconType={expandedFeatures.includes(featureId) ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const rawKibanaPrivileges = [...props.role.kibana].sort((entry1, entry2) => {
|
||||
if (isGlobalPrivilegeDefinition(entry1)) {
|
||||
return -1;
|
||||
}
|
||||
if (isGlobalPrivilegeDefinition(entry2)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const privilegeColumns = rawKibanaPrivileges.map(entry => {
|
||||
const key = getColumnKey(entry);
|
||||
return {
|
||||
name: <SpaceColumnHeader entry={entry} spaces={props.spaces} />,
|
||||
field: key,
|
||||
render: (kibanaPrivilege: EffectiveFeaturePrivileges, record: { featureId: string }) => {
|
||||
const { primary, hasCustomizedSubFeaturePrivileges } = kibanaPrivilege[record.featureId];
|
||||
let iconTip = null;
|
||||
if (hasCustomizedSubFeaturePrivileges) {
|
||||
iconTip = (
|
||||
<EuiIconTip
|
||||
size="s"
|
||||
type="iInCircle"
|
||||
content={
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.privilegeSummary.additionalPrivilegesGrantedIconTip"
|
||||
defaultMessage="Additional privileges granted. Expand this row for more information."
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
iconTip = <EuiIcon size="s" type="empty" />;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
data-test-subj={`privilegeColumn ${
|
||||
hasCustomizedSubFeaturePrivileges ? 'additionalPrivilegesGranted' : ''
|
||||
}`}
|
||||
>
|
||||
{primary?.name ?? 'None'} {iconTip}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<any>> = [];
|
||||
if (props.canCustomizeSubFeaturePrivileges) {
|
||||
columns.push(rowExpanderColumn);
|
||||
}
|
||||
columns.push(featureColumn, ...privilegeColumns);
|
||||
|
||||
const privileges = rawKibanaPrivileges.reduce((acc, entry) => {
|
||||
return {
|
||||
...acc,
|
||||
[getColumnKey(entry)]: calculator.getEffectiveFeaturePrivileges(entry),
|
||||
};
|
||||
}, {} as Record<string, EffectiveFeaturePrivileges>);
|
||||
|
||||
const items = props.kibanaPrivileges.getSecuredFeatures().map(feature => {
|
||||
return {
|
||||
feature,
|
||||
featureId: feature.id,
|
||||
...privileges,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
itemId="featureId"
|
||||
rowProps={record => {
|
||||
return {
|
||||
'data-test-subj': `summaryTableRow-${record.featureId}`,
|
||||
};
|
||||
}}
|
||||
itemIdToExpandedRowMap={expandedFeatures.reduce((acc, featureId) => {
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: (
|
||||
<PrivilegeSummaryExpandedRow
|
||||
feature={props.kibanaPrivileges.getSecuredFeature(featureId)}
|
||||
effectiveFeaturePrivileges={Object.values(privileges).map(p => p[featureId])}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}, {})}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { SpaceColumnHeader } from './space_column_header';
|
||||
import { SpacesPopoverList } from '../../../spaces_popover_list';
|
||||
import { SpaceAvatar } from '../../../../../../../../spaces/public';
|
||||
|
||||
const spaces = [
|
||||
{
|
||||
id: '*',
|
||||
name: 'Global',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space-1',
|
||||
name: 'Space 1',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space-2',
|
||||
name: 'Space 2',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space-3',
|
||||
name: 'Space 3',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space-4',
|
||||
name: 'Space 4',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
{
|
||||
id: 'space-5',
|
||||
name: 'Space 5',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
];
|
||||
|
||||
describe('SpaceColumnHeader', () => {
|
||||
it('renders the Global privilege definition with a special label and popover control', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SpaceColumnHeader
|
||||
spaces={spaces}
|
||||
entry={{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(SpacesPopoverList)).toHaveLength(1);
|
||||
// Snapshot includes space avatar (The first "G"), followed by the "Global" label,
|
||||
// followed by the (all spaces) text as part of the SpacesPopoverList
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"G Global(all spaces)"`);
|
||||
});
|
||||
|
||||
it('renders a placeholder space when the requested space no longer exists', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SpaceColumnHeader
|
||||
spaces={spaces}
|
||||
entry={{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: ['space-1', 'missing-space', 'space-3'],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(SpacesPopoverList)).toHaveLength(0);
|
||||
|
||||
const avatars = wrapper.find(SpaceAvatar);
|
||||
expect(avatars).toHaveLength(3);
|
||||
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"S1 m S3 "`);
|
||||
});
|
||||
|
||||
it('renders a space privilege definition with an avatar for each space in the group', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SpaceColumnHeader
|
||||
spaces={spaces}
|
||||
entry={{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: ['space-1', 'space-2', 'space-3', 'space-4'],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(SpacesPopoverList)).toHaveLength(0);
|
||||
|
||||
const avatars = wrapper.find(SpaceAvatar);
|
||||
expect(avatars).toHaveLength(4);
|
||||
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"S1 S2 S3 S4 "`);
|
||||
});
|
||||
|
||||
it('renders a space privilege definition with an avatar for the first 4 spaces in the group, with the popover control showing the rest', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SpaceColumnHeader
|
||||
spaces={spaces}
|
||||
entry={{
|
||||
base: [],
|
||||
feature: {},
|
||||
spaces: ['space-1', 'space-2', 'space-3', 'space-4', 'space-5'],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(SpacesPopoverList)).toHaveLength(1);
|
||||
|
||||
const avatars = wrapper.find(SpaceAvatar);
|
||||
expect(avatars).toHaveLength(4);
|
||||
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"S1 S2 S3 S4 +1 more"`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 React, { Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Space, SpaceAvatar } from '../../../../../../../../spaces/public';
|
||||
import { RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { SpacesPopoverList } from '../../../spaces_popover_list';
|
||||
|
||||
interface Props {
|
||||
spaces: Space[];
|
||||
entry: RoleKibanaPrivilege;
|
||||
}
|
||||
|
||||
const SPACES_DISPLAY_COUNT = 4;
|
||||
|
||||
export const SpaceColumnHeader = (props: Props) => {
|
||||
const isGlobal = isGlobalPrivilegeDefinition(props.entry);
|
||||
const entrySpaces = props.entry.spaces.map(spaceId => {
|
||||
return (
|
||||
props.spaces.find(s => s.id === spaceId) ?? {
|
||||
id: spaceId,
|
||||
name: spaceId,
|
||||
disabledFeatures: [],
|
||||
}
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{entrySpaces.slice(0, SPACES_DISPLAY_COUNT).map(space => {
|
||||
return (
|
||||
<span key={space.id}>
|
||||
<SpaceAvatar size="s" space={space} />{' '}
|
||||
{isGlobal && (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName"
|
||||
defaultMessage="Global"
|
||||
/>
|
||||
<br />
|
||||
<SpacesPopoverList
|
||||
spaces={props.spaces.filter(s => s.id !== '*')}
|
||||
buttonText={i18n.translate(
|
||||
'xpack.security.management.editRole.spacePrivilegeMatrix.showAllSpacesLink',
|
||||
{
|
||||
defaultMessage: '(all spaces)',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{entrySpaces.length > SPACES_DISPLAY_COUNT && (
|
||||
<Fragment>
|
||||
<br />
|
||||
<SpacesPopoverList
|
||||
spaces={entrySpaces}
|
||||
buttonText={i18n.translate(
|
||||
'xpack.security.management.editRole.spacePrivilegeMatrix.showNMoreSpacesLink',
|
||||
{
|
||||
defaultMessage: '+{count} more',
|
||||
values: {
|
||||
count: entrySpaces.length - SPACES_DISPLAY_COUNT,
|
||||
},
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -2,153 +2,159 @@
|
|||
|
||||
exports[`<SimplePrivilegeForm> renders without crashing 1`] = `
|
||||
<Fragment>
|
||||
<EuiDescribedFormGroup
|
||||
description={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Specifies the Kibana privilege for this role."
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
defaultMessage="Kibana privileges"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
||||
}
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={true}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Specifies the Kibana privilege for this role."
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Kibana privileges"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
compressed={false}
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="None"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdown"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="No access to Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="None"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdown"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="No access to Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="None"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>,
|
||||
"value": "none",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
</EuiText>,
|
||||
"value": "none",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Custom"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdown"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Customize access to Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="Custom"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdown"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Customize access to Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="Custom"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>,
|
||||
"value": "custom",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
</EuiText>,
|
||||
"value": "custom",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Read"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdown"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grants read-only access to all of Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="Read"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdown"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grants read-only access to all of Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="Read"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>,
|
||||
"value": "read",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
</EuiText>,
|
||||
"value": "read",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="All"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdown"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grants full access to all of Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="All"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdown"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grants full access to all of Kibana"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdownDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="All"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeInput"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>,
|
||||
"value": "all",
|
||||
},
|
||||
]
|
||||
}
|
||||
valueOfSelected="none"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</EuiText>,
|
||||
"value": "all",
|
||||
},
|
||||
]
|
||||
}
|
||||
valueOfSelected="none"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
||||
|
|
|
@ -7,24 +7,53 @@
|
|||
import { EuiButtonGroup, EuiButtonGroupProps, EuiComboBox, EuiSuperSelect } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Feature } from '../../../../../../../../features/public';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { SimplePrivilegeSection } from './simple_privilege_section';
|
||||
import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning';
|
||||
import { KibanaPrivileges, SecuredFeature } from '../../../../model';
|
||||
|
||||
const buildProps = (customProps: any = {}) => {
|
||||
const kibanaPrivileges = new KibanaPrivileges({
|
||||
features: {
|
||||
feature1: {
|
||||
all: ['*'],
|
||||
read: ['read'],
|
||||
const features = [
|
||||
new SecuredFeature({
|
||||
id: 'feature1',
|
||||
name: 'Feature 1',
|
||||
app: ['app'],
|
||||
icon: 'spacesApp',
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['app'],
|
||||
savedObject: {
|
||||
all: ['foo'],
|
||||
read: [],
|
||||
},
|
||||
ui: ['app-ui'],
|
||||
},
|
||||
read: {
|
||||
app: ['app'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['app-ui'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
] as SecuredFeature[];
|
||||
|
||||
const kibanaPrivileges = new KibanaPrivileges(
|
||||
{
|
||||
features: {
|
||||
feature1: {
|
||||
all: ['*'],
|
||||
read: ['read'],
|
||||
},
|
||||
},
|
||||
global: {},
|
||||
space: {},
|
||||
reserved: {},
|
||||
},
|
||||
global: {},
|
||||
space: {},
|
||||
reserved: {},
|
||||
});
|
||||
features
|
||||
);
|
||||
|
||||
const role = {
|
||||
name: '',
|
||||
|
@ -40,34 +69,9 @@ const buildProps = (customProps: any = {}) => {
|
|||
return {
|
||||
editable: true,
|
||||
kibanaPrivileges,
|
||||
privilegeCalculatorFactory: new KibanaPrivilegeCalculatorFactory(kibanaPrivileges),
|
||||
features: [
|
||||
{
|
||||
id: 'feature1',
|
||||
name: 'Feature 1',
|
||||
app: ['app'],
|
||||
icon: 'spacesApp',
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['app'],
|
||||
savedObject: {
|
||||
all: ['foo'],
|
||||
read: [],
|
||||
},
|
||||
ui: ['app-ui'],
|
||||
},
|
||||
read: {
|
||||
app: ['app'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['app-ui'],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as Feature[],
|
||||
features,
|
||||
onChange: jest.fn(),
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
...customProps,
|
||||
role,
|
||||
};
|
||||
|
|
|
@ -6,34 +6,28 @@
|
|||
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { Feature } from '../../../../../../../../features/public';
|
||||
import {
|
||||
KibanaPrivileges,
|
||||
Role,
|
||||
RoleKibanaPrivilege,
|
||||
copyRole,
|
||||
} from '../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator';
|
||||
import { Role, RoleKibanaPrivilege, copyRole } from '../../../../../../../common/model';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { FeatureTable } from '../feature_table';
|
||||
import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning';
|
||||
import { KibanaPrivileges } from '../../../../model';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
|
||||
interface Props {
|
||||
role: Role;
|
||||
kibanaPrivileges: KibanaPrivileges;
|
||||
privilegeCalculatorFactory: KibanaPrivilegeCalculatorFactory;
|
||||
features: Feature[];
|
||||
onChange: (role: Role) => void;
|
||||
editable: boolean;
|
||||
canCustomizeSubFeaturePrivileges: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -58,20 +52,14 @@ export class SimplePrivilegeSection extends Component<Props, State> {
|
|||
public render() {
|
||||
const kibanaPrivilege = this.getDisplayedBasePrivilege();
|
||||
|
||||
const privilegeCalculator = this.props.privilegeCalculatorFactory.getInstance(this.props.role);
|
||||
const reservedPrivileges = this.props.role.kibana[this.state.globalPrivsIndex]?._reserved ?? [];
|
||||
|
||||
const calculatedPrivileges = privilegeCalculator.calculateEffectivePrivileges()[
|
||||
this.state.globalPrivsIndex
|
||||
];
|
||||
|
||||
const allowedPrivileges = privilegeCalculator.calculateAllowedPrivileges()[
|
||||
this.state.globalPrivsIndex
|
||||
];
|
||||
|
||||
const hasReservedPrivileges =
|
||||
calculatedPrivileges &&
|
||||
calculatedPrivileges.reserved != null &&
|
||||
calculatedPrivileges.reserved.length > 0;
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle"
|
||||
defaultMessage="Kibana privileges"
|
||||
/>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<p>
|
||||
|
@ -84,162 +72,159 @@ export class SimplePrivilegeSection extends Component<Props, State> {
|
|||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle"
|
||||
defaultMessage="Kibana privileges"
|
||||
/>
|
||||
</h3>
|
||||
}
|
||||
description={description}
|
||||
>
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
{hasReservedPrivileges ? (
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={calculatedPrivileges.reserved!.map(privilege => ({
|
||||
label: privilege,
|
||||
}))}
|
||||
isDisabled
|
||||
/>
|
||||
) : (
|
||||
<EuiSuperSelect
|
||||
disabled={!this.props.editable}
|
||||
onChange={this.onKibanaPrivilegeChange}
|
||||
options={[
|
||||
{
|
||||
value: NO_PRIVILEGE_VALUE,
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeInput"
|
||||
defaultMessage="None"
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
{description}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label={title}>
|
||||
{reservedPrivileges.length > 0 ? (
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={reservedPrivileges.map(rp => ({ label: rp }))}
|
||||
isDisabled
|
||||
/>
|
||||
) : (
|
||||
<EuiSuperSelect
|
||||
disabled={!this.props.editable}
|
||||
onChange={this.onKibanaPrivilegeChange}
|
||||
options={[
|
||||
{
|
||||
value: NO_PRIVILEGE_VALUE,
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdown"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeInput"
|
||||
defaultMessage="None"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdown"
|
||||
defaultMessage="None"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdownDescription"
|
||||
defaultMessage="No access to Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: CUSTOM_PRIVILEGE_VALUE,
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.noPrivilegeDropdownDescription"
|
||||
defaultMessage="No access to Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: CUSTOM_PRIVILEGE_VALUE,
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeInput"
|
||||
defaultMessage="Custom"
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdown"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeInput"
|
||||
defaultMessage="Custom"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdown"
|
||||
defaultMessage="Custom"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdownDescription"
|
||||
defaultMessage="Customize access to Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'read',
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.customPrivilegeDropdownDescription"
|
||||
defaultMessage="Customize access to Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'read',
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeInput"
|
||||
defaultMessage="Read"
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdown"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeInput"
|
||||
defaultMessage="Read"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdown"
|
||||
defaultMessage="Read"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdownDescription"
|
||||
defaultMessage="Grants read-only access to all of Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'all',
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.readPrivilegeDropdownDescription"
|
||||
defaultMessage="Grants read-only access to all of Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'all',
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeInput"
|
||||
defaultMessage="All"
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdown"
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeInput"
|
||||
defaultMessage="All"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdownDescription"
|
||||
defaultMessage="Grants full access to all of Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
]}
|
||||
hasDividers
|
||||
valueOfSelected={kibanaPrivilege}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
{this.state.isCustomizingGlobalPrivilege && (
|
||||
<EuiFormRow>
|
||||
<FeatureTable
|
||||
role={this.props.role}
|
||||
kibanaPrivileges={this.props.kibanaPrivileges}
|
||||
calculatedPrivileges={calculatedPrivileges}
|
||||
allowedPrivileges={allowedPrivileges}
|
||||
rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges}
|
||||
features={this.props.features}
|
||||
onChange={this.onFeaturePrivilegeChange}
|
||||
onChangeAll={this.onChangeAllFeaturePrivileges}
|
||||
spacesIndex={this.props.role.kibana.findIndex(k => isGlobalPrivilegeDefinition(k))}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdown"
|
||||
defaultMessage="All"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.simplePrivilegeForm.allPrivilegeDropdownDescription"
|
||||
defaultMessage="Grants full access to all of Kibana"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
]}
|
||||
hasDividers
|
||||
valueOfSelected={kibanaPrivilege}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{this.maybeRenderSpacePrivilegeWarning()}
|
||||
</EuiDescribedFormGroup>
|
||||
{this.state.isCustomizingGlobalPrivilege && (
|
||||
<EuiFormRow fullWidth>
|
||||
<FeatureTable
|
||||
role={this.props.role}
|
||||
kibanaPrivileges={this.props.kibanaPrivileges}
|
||||
privilegeCalculator={
|
||||
new PrivilegeFormCalculator(this.props.kibanaPrivileges, this.props.role)
|
||||
}
|
||||
onChange={this.onFeaturePrivilegeChange}
|
||||
onChangeAll={this.onChangeAllFeaturePrivileges}
|
||||
privilegeIndex={this.props.role.kibana.findIndex(k =>
|
||||
isGlobalPrivilegeDefinition(k)
|
||||
)}
|
||||
canCustomizeSubFeaturePrivileges={this.props.canCustomizeSubFeaturePrivileges}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{this.maybeRenderSpacePrivilegeWarning()}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -295,7 +280,7 @@ export class SimplePrivilegeSection extends Component<Props, State> {
|
|||
|
||||
const form = this.locateGlobalPrivilege(role) || this.createGlobalPrivilegeEntry(role);
|
||||
if (privileges.length > 0) {
|
||||
this.props.features.forEach(feature => {
|
||||
this.props.kibanaPrivileges.getSecuredFeatures().forEach(feature => {
|
||||
form.feature[feature.id] = [...privileges];
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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 { RawKibanaPrivileges } from '../../../../../../../../common/model';
|
||||
|
||||
export const rawKibanaPrivileges: RawKibanaPrivileges = {
|
||||
global: {
|
||||
all: [
|
||||
'normal-feature-all',
|
||||
'normal-feature-read',
|
||||
'just-global-all',
|
||||
'all-privilege-excluded-from-base-read',
|
||||
],
|
||||
read: ['normal-feature-read', 'all-privilege-excluded-from-base-read'],
|
||||
},
|
||||
space: {
|
||||
all: ['normal-feature-all', 'normal-feature-read', 'all-privilege-excluded-from-base-read'],
|
||||
read: ['normal-feature-read', 'all-privilege-excluded-from-base-read'],
|
||||
},
|
||||
reserved: {},
|
||||
features: {
|
||||
normal: {
|
||||
all: ['normal-feature-all', 'normal-feature-read'],
|
||||
read: ['normal-feature-read'],
|
||||
},
|
||||
bothPrivilegesExcludedFromBase: {
|
||||
all: ['both-privileges-excluded-from-base-all', 'both-privileges-excluded-from-base-read'],
|
||||
read: ['both-privileges-excluded-from-base-read'],
|
||||
},
|
||||
allPrivilegeExcludedFromBase: {
|
||||
all: ['all-privilege-excluded-from-base-all', 'all-privilege-excluded-from-base-read'],
|
||||
read: ['all-privilege-excluded-from-base-read'],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,118 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PrivilegeDisplay renders a superceded privilege 1`] = `
|
||||
<SupersededPrivilegeDisplay
|
||||
explanation={
|
||||
Object {
|
||||
"actualPrivilege": "all",
|
||||
"actualPrivilegeSource": 40,
|
||||
"isDirectlyAssigned": false,
|
||||
"supersededPrivilege": "read",
|
||||
"supersededPrivilegeSource": 20,
|
||||
}
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
privilege="all"
|
||||
/>
|
||||
`;
|
|
@ -1,497 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PrivilegeSpaceForm> renders without crashing 1`] = `
|
||||
<EuiOverlayMask>
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="m"
|
||||
>
|
||||
<EuiFlyoutHeader
|
||||
hasBorder={true}
|
||||
>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
defaultMessage="Space privileges"
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.modalTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Spaces"
|
||||
labelType="label"
|
||||
>
|
||||
<SpaceSelector
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedSpaceIds={Array []}
|
||||
spaces={
|
||||
Array [
|
||||
Object {
|
||||
"_reserved": true,
|
||||
"description": "",
|
||||
"disabledFeatures": Array [],
|
||||
"id": "default",
|
||||
"name": "Default Space",
|
||||
},
|
||||
Object {
|
||||
"description": "",
|
||||
"disabledFeatures": Array [],
|
||||
"id": "marketing",
|
||||
"name": "Marketing",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Privilege"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
compressed={false}
|
||||
data-test-subj="basePrivilegeComboBox"
|
||||
disabled={true}
|
||||
fullWidth={true}
|
||||
hasDividers={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": false,
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Custom"
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDropdownDisplay"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Customize access by feature in selected spaces."
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDetails"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="Custom"
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDisplay"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>,
|
||||
"value": "basePrivilege_custom",
|
||||
},
|
||||
Object {
|
||||
"disabled": false,
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Read"
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDropdownDisplay"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grant read-only access to all features in selected spaces."
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDetails"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="Read"
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDisplay"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>,
|
||||
"value": "basePrivilege_read",
|
||||
},
|
||||
Object {
|
||||
"dropdownDisplay": <EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="All"
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay"
|
||||
values={Object {}}
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grant full access to all features in selected spaces."
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>,
|
||||
"inputDisplay": <EuiText>
|
||||
<FormattedMessage
|
||||
defaultMessage="All"
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiText>,
|
||||
"value": "basePrivilege_all",
|
||||
},
|
||||
]
|
||||
}
|
||||
valueOfSelected="basePrivilege_custom"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiTitle
|
||||
size="xxs"
|
||||
>
|
||||
<h3>
|
||||
Customize by feature
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="xs"
|
||||
/>
|
||||
<EuiText
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
Increase privilege levels on a per feature basis. Some features might be hidden by the space or affected by a global space privilege.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
/>
|
||||
<FeatureTable
|
||||
allowedPrivileges={
|
||||
Object {
|
||||
"base": Object {
|
||||
"canUnassign": true,
|
||||
"privileges": Array [
|
||||
"all",
|
||||
"read",
|
||||
],
|
||||
},
|
||||
"feature": Object {
|
||||
"allPrivilegeExcludedFromBase": Object {
|
||||
"canUnassign": true,
|
||||
"privileges": Array [
|
||||
"all",
|
||||
"read",
|
||||
],
|
||||
},
|
||||
"bothPrivilegesExcludedFromBase": Object {
|
||||
"canUnassign": true,
|
||||
"privileges": Array [
|
||||
"all",
|
||||
"read",
|
||||
],
|
||||
},
|
||||
"normal": Object {
|
||||
"canUnassign": true,
|
||||
"privileges": Array [
|
||||
"all",
|
||||
"read",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
calculatedPrivileges={
|
||||
Object {
|
||||
"base": Object {
|
||||
"actualPrivilege": "none",
|
||||
"actualPrivilegeSource": 40,
|
||||
"isDirectlyAssigned": true,
|
||||
},
|
||||
"feature": Object {
|
||||
"allPrivilegeExcludedFromBase": Object {
|
||||
"actualPrivilege": "none",
|
||||
"actualPrivilegeSource": 30,
|
||||
"isDirectlyAssigned": true,
|
||||
},
|
||||
"bothPrivilegesExcludedFromBase": Object {
|
||||
"actualPrivilege": "none",
|
||||
"actualPrivilegeSource": 30,
|
||||
"isDirectlyAssigned": true,
|
||||
},
|
||||
"normal": Object {
|
||||
"actualPrivilege": "none",
|
||||
"actualPrivilegeSource": 30,
|
||||
"isDirectlyAssigned": true,
|
||||
},
|
||||
},
|
||||
"reserved": undefined,
|
||||
}
|
||||
}
|
||||
disabled={true}
|
||||
features={Array []}
|
||||
kibanaPrivileges={
|
||||
KibanaPrivileges {
|
||||
"rawKibanaPrivileges": Object {
|
||||
"features": Object {
|
||||
"allPrivilegeExcludedFromBase": Object {
|
||||
"all": Array [
|
||||
"all-privilege-excluded-from-base-all",
|
||||
"all-privilege-excluded-from-base-read",
|
||||
],
|
||||
"read": Array [
|
||||
"all-privilege-excluded-from-base-read",
|
||||
],
|
||||
},
|
||||
"bothPrivilegesExcludedFromBase": Object {
|
||||
"all": Array [
|
||||
"both-privileges-excluded-from-base-all",
|
||||
"both-privileges-excluded-from-base-read",
|
||||
],
|
||||
"read": Array [
|
||||
"both-privileges-excluded-from-base-read",
|
||||
],
|
||||
},
|
||||
"normal": Object {
|
||||
"all": Array [
|
||||
"normal-feature-all",
|
||||
"normal-feature-read",
|
||||
],
|
||||
"read": Array [
|
||||
"normal-feature-read",
|
||||
],
|
||||
},
|
||||
},
|
||||
"global": Object {
|
||||
"all": Array [
|
||||
"normal-feature-all",
|
||||
"normal-feature-read",
|
||||
"just-global-all",
|
||||
"all-privilege-excluded-from-base-read",
|
||||
],
|
||||
"read": Array [
|
||||
"normal-feature-read",
|
||||
"all-privilege-excluded-from-base-read",
|
||||
],
|
||||
},
|
||||
"reserved": Object {},
|
||||
"space": Object {
|
||||
"all": Array [
|
||||
"normal-feature-all",
|
||||
"normal-feature-read",
|
||||
"all-privilege-excluded-from-base-read",
|
||||
],
|
||||
"read": Array [
|
||||
"normal-feature-read",
|
||||
"all-privilege-excluded-from-base-read",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
onChangeAll={[Function]}
|
||||
rankedFeaturePrivileges={
|
||||
Object {
|
||||
"allPrivilegeExcludedFromBase": Array [
|
||||
"all",
|
||||
"read",
|
||||
],
|
||||
"bothPrivilegesExcludedFromBase": Array [
|
||||
"all",
|
||||
"read",
|
||||
],
|
||||
"normal": Array [
|
||||
"all",
|
||||
"read",
|
||||
],
|
||||
}
|
||||
}
|
||||
role={
|
||||
Object {
|
||||
"elasticsearch": Object {
|
||||
"cluster": Array [
|
||||
"all",
|
||||
],
|
||||
"indices": Array [],
|
||||
"run_as": Array [],
|
||||
},
|
||||
"kibana": Array [
|
||||
Object {
|
||||
"base": Array [],
|
||||
"feature": Object {},
|
||||
"spaces": Array [],
|
||||
},
|
||||
],
|
||||
"name": "test role",
|
||||
}
|
||||
}
|
||||
showLocks={true}
|
||||
spacesIndex={0}
|
||||
/>
|
||||
</EuiForm>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="cancelSpacePrivilegeButton"
|
||||
flush="left"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="xpack.security.management.editRolespacePrivilegeForm.cancelButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="createSpacePrivilegeButton"
|
||||
disabled={true}
|
||||
fill={true}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Update space privilege"
|
||||
id="xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiOverlayMask>
|
||||
`;
|
|
@ -4,10 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiIconTip, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { PRIVILEGE_SOURCE } from '../kibana_privilege_calculator';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { PrivilegeDisplay } from './privilege_display';
|
||||
|
||||
describe('PrivilegeDisplay', () => {
|
||||
|
@ -23,41 +22,4 @@ describe('PrivilegeDisplay', () => {
|
|||
color: 'danger',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a privilege with tooltip, if provided', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeDisplay privilege={'all'} tooltipContent={<b>ahh</b>} />
|
||||
);
|
||||
expect(wrapper.text().trim()).toEqual('All');
|
||||
expect(wrapper.find(EuiToolTip).props()).toMatchObject({
|
||||
content: <b>ahh</b>,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a privilege with icon tooltip, if provided', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeDisplay privilege={'all'} iconTooltipContent={<b>ahh</b>} iconType={'asterisk'} />
|
||||
);
|
||||
expect(wrapper.text().trim()).toEqual('All');
|
||||
expect(wrapper.find(EuiIconTip).props()).toMatchObject({
|
||||
type: 'asterisk',
|
||||
content: <b>ahh</b>,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a superceded privilege', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PrivilegeDisplay
|
||||
privilege={'all'}
|
||||
explanation={{
|
||||
supersededPrivilege: 'read',
|
||||
supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE,
|
||||
actualPrivilege: 'all',
|
||||
actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE,
|
||||
isDirectlyAssigned: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,95 +3,28 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiIcon, EuiIconTip, EuiText, IconType, PropsOf, EuiToolTip } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiIcon, EuiText, PropsOf } from '@elastic/eui';
|
||||
import _ from 'lodash';
|
||||
import React, { ReactNode, FC } from 'react';
|
||||
import { PRIVILEGE_SOURCE, PrivilegeExplanation } from '../kibana_privilege_calculator';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
|
||||
interface Props extends PropsOf<typeof EuiText> {
|
||||
privilege: string | string[] | undefined;
|
||||
explanation?: PrivilegeExplanation;
|
||||
iconType?: IconType;
|
||||
iconTooltipContent?: ReactNode;
|
||||
tooltipContent?: ReactNode;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
export const PrivilegeDisplay: FC<Props> = (props: Props) => {
|
||||
const { explanation } = props;
|
||||
|
||||
if (!explanation) {
|
||||
return <SimplePrivilegeDisplay {...props} />;
|
||||
}
|
||||
|
||||
if (explanation.supersededPrivilege) {
|
||||
return <SupersededPrivilegeDisplay {...props} />;
|
||||
}
|
||||
|
||||
if (!explanation.isDirectlyAssigned) {
|
||||
return <EffectivePrivilegeDisplay {...props} />;
|
||||
}
|
||||
|
||||
return <SimplePrivilegeDisplay {...props} />;
|
||||
};
|
||||
|
||||
const SimplePrivilegeDisplay: FC<Props> = (props: Props) => {
|
||||
const { privilege, iconType, iconTooltipContent, explanation, tooltipContent, ...rest } = props;
|
||||
const { privilege, ...rest } = props;
|
||||
|
||||
const text = (
|
||||
<EuiText {...rest}>
|
||||
{getDisplayValue(privilege)} {getIconTip(iconType, iconTooltipContent)}
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
if (tooltipContent) {
|
||||
return <EuiToolTip content={tooltipContent}>{text}</EuiToolTip>;
|
||||
}
|
||||
const text = <EuiText {...rest}>{getDisplayValue(privilege)}</EuiText>;
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
export const SupersededPrivilegeDisplay: FC<Props> = (props: Props) => {
|
||||
const { supersededPrivilege, actualPrivilegeSource } =
|
||||
props.explanation || ({} as PrivilegeExplanation);
|
||||
|
||||
return (
|
||||
<SimplePrivilegeDisplay
|
||||
{...props}
|
||||
iconType={'lock'}
|
||||
iconTooltipContent={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeDisplay.privilegeSupercededMessage"
|
||||
defaultMessage="Original privilege of {supersededPrivilege} has been overriden by {actualPrivilegeSource}"
|
||||
values={{
|
||||
supersededPrivilege: `'${supersededPrivilege}'`,
|
||||
actualPrivilegeSource: getReadablePrivilegeSource(actualPrivilegeSource),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EffectivePrivilegeDisplay: FC<Props> = (props: Props) => {
|
||||
const { explanation, ...rest } = props;
|
||||
|
||||
const source = getReadablePrivilegeSource(explanation!.actualPrivilegeSource);
|
||||
|
||||
const iconTooltipContent = (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeDisplay.effectivePrivilegeMessage"
|
||||
defaultMessage="Granted via {source}."
|
||||
values={{ source }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SimplePrivilegeDisplay {...rest} iconType={'lock'} iconTooltipContent={iconTooltipContent} />
|
||||
);
|
||||
};
|
||||
|
||||
PrivilegeDisplay.defaultProps = {
|
||||
privilege: [],
|
||||
};
|
||||
|
@ -113,24 +46,6 @@ function getDisplayValue(privilege: string | string[] | undefined) {
|
|||
return displayValue;
|
||||
}
|
||||
|
||||
function getIconTip(iconType?: IconType, tooltipContent?: ReactNode) {
|
||||
if (!iconType || !tooltipContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiIconTip
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
color="subdued"
|
||||
type={iconType}
|
||||
content={tooltipContent}
|
||||
size={'s'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function coerceToArray(privilege: string | string[] | undefined): string[] {
|
||||
if (privilege === undefined) {
|
||||
return [];
|
||||
|
@ -140,43 +55,3 @@ function coerceToArray(privilege: string | string[] | undefined): string[] {
|
|||
}
|
||||
return [privilege];
|
||||
}
|
||||
|
||||
function getReadablePrivilegeSource(privilegeSource: PRIVILEGE_SOURCE) {
|
||||
switch (privilegeSource) {
|
||||
case PRIVILEGE_SOURCE.GLOBAL_BASE:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeDisplay.globalBasePrivilegeSource"
|
||||
defaultMessage="global base privilege"
|
||||
/>
|
||||
);
|
||||
case PRIVILEGE_SOURCE.GLOBAL_FEATURE:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeDisplay.globalFeaturePrivilegeSource"
|
||||
defaultMessage="global feature privilege"
|
||||
/>
|
||||
);
|
||||
case PRIVILEGE_SOURCE.SPACE_BASE:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceBasePrivilegeSource"
|
||||
defaultMessage="space base privilege"
|
||||
/>
|
||||
);
|
||||
case PRIVILEGE_SOURCE.SPACE_FEATURE:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceFeaturePrivilegeSource"
|
||||
defaultMessage="space feature privilege"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spaceAwarePrivilegeDisplay.unknownPrivilegeSource"
|
||||
defaultMessage="**UNKNOWN**"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiInMemoryTable } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Space } from '../../../../../../../../spaces/public';
|
||||
import { Feature } from '../../../../../../../../features/public';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator';
|
||||
import { PrivilegeMatrix } from './privilege_matrix';
|
||||
|
||||
describe('PrivilegeMatrix', () => {
|
||||
it('can render a complex matrix', () => {
|
||||
const spaces: Space[] = ['*', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'].map(a => ({
|
||||
id: a,
|
||||
name: `${a} space`,
|
||||
disabledFeatures: [],
|
||||
}));
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
id: 'feature1',
|
||||
name: 'feature 1',
|
||||
icon: 'apmApp',
|
||||
app: [],
|
||||
privileges: {},
|
||||
},
|
||||
{
|
||||
id: 'feature2',
|
||||
name: 'feature 2',
|
||||
icon: 'apmApp',
|
||||
app: [],
|
||||
privileges: {},
|
||||
},
|
||||
{
|
||||
id: 'feature3',
|
||||
name: 'feature 3',
|
||||
icon: 'apmApp',
|
||||
app: [],
|
||||
privileges: {},
|
||||
},
|
||||
];
|
||||
|
||||
const role: Role = {
|
||||
name: 'role',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {
|
||||
feature1: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature2: ['read'],
|
||||
feature3: ['all'],
|
||||
},
|
||||
},
|
||||
{
|
||||
spaces: ['k'],
|
||||
base: ['all'],
|
||||
feature: {
|
||||
feature2: ['read'],
|
||||
feature3: ['read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const calculator = new KibanaPrivilegeCalculatorFactory(
|
||||
new KibanaPrivileges({
|
||||
global: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
features: {
|
||||
feature1: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
feature2: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
feature3: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
},
|
||||
space: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
reserved: {},
|
||||
})
|
||||
).getInstance(role);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PrivilegeMatrix
|
||||
role={role}
|
||||
spaces={spaces}
|
||||
features={features}
|
||||
calculatedPrivileges={calculator.calculateEffectivePrivileges()}
|
||||
intl={null as any}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find(EuiButtonEmpty).simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
const { columns, items } = wrapper.find(EuiInMemoryTable).props() as any;
|
||||
|
||||
expect(columns).toHaveLength(4); // all spaces groups plus the "feature" column
|
||||
expect(items).toHaveLength(features.length + 1); // all features plus the "base" row
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue