Add support for licensed sub feature privileges (#80905)

This commit is contained in:
Larry Gregory 2020-11-16 14:50:20 -05:00 committed by GitHub
parent bc3bb2afa8
commit fe33579272
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1641 additions and 55 deletions

View file

@ -198,7 +198,10 @@ server.route({
=== Example 3: Discover
Discover takes advantage of subfeature privileges to allow fine-grained access control. In this example,
a single "Create Short URLs" subfeature privilege is defined, which allows users to grant access to this feature without having to grant the `all` privilege to Discover. In other words, you can grant `read` access to Discover, and also grant the ability to create short URLs.
two subfeature privileges are defined: "Create Short URLs", and "Generate PDF Reports". These allow users to grant access to this feature without having to grant the `all` privilege to Discover. In other words, you can grant `read` access to Discover, and also grant the ability to create short URLs or generate PDF reports.
Notice the "Generate PDF Reports" subfeature privilege has an additional `minimumPrivilege` option. Kibana will only offer this subfeature privilege if the
license requirement is satisfied.
["source","javascript"]
-----------
@ -259,6 +262,28 @@ public setup(core, { features }) {
},
],
},
{
groupType: 'independent',
privileges: [
{
id: 'pdf_generate',
name: i18n.translate(
'xpack.features.ossFeatures.discoverGeneratePDFReportsPrivilegeName',
{
defaultMessage: 'Generate PDF Reports',
}
),
minimumLicense: 'platinum',
includeIn: 'all',
savedObject: {
all: [],
read: [],
},
api: ['generatePDFReports'],
ui: ['generatePDFReports'],
},
],
},
],
},
],

View file

@ -5,6 +5,7 @@
*/
import { RecursiveReadonly } from '@kbn/utility-types';
import { LicenseType } from '../../licensing/common/types';
import { FeatureKibanaPrivileges } from './feature_kibana_privileges';
/**
@ -68,6 +69,13 @@ export interface SubFeaturePrivilegeConfig
* `read` is also included in `all` automatically.
*/
includeIn: 'all' | 'read' | 'none';
/**
* The minimum supported license level for this sub-feature privilege.
* If no license level is supplied, then this privilege will be available for all licences
* that are valid for the overall feature.
*/
minimumLicense?: LicenseType;
}
export class SubFeature {

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`buildOSSFeatures returns the advancedSettings feature augmented with appropriate sub feature privileges 1`] = `
exports[`buildOSSFeatures with a basic license returns the advancedSettings feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
@ -51,7 +51,7 @@ Array [
]
`;
exports[`buildOSSFeatures returns the dashboard feature augmented with appropriate sub feature privileges 1`] = `
exports[`buildOSSFeatures with a basic license returns the dashboard feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
@ -128,7 +128,7 @@ Array [
]
`;
exports[`buildOSSFeatures returns the dev_tools feature augmented with appropriate sub feature privileges 1`] = `
exports[`buildOSSFeatures with a basic license returns the dev_tools feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
@ -182,7 +182,7 @@ Array [
]
`;
exports[`buildOSSFeatures returns the discover feature augmented with appropriate sub feature privileges 1`] = `
exports[`buildOSSFeatures with a basic license returns the discover feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
@ -243,7 +243,7 @@ Array [
]
`;
exports[`buildOSSFeatures returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = `
exports[`buildOSSFeatures with a basic license returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
@ -296,7 +296,7 @@ Array [
]
`;
exports[`buildOSSFeatures returns the savedObjectsManagement feature augmented with appropriate sub feature privileges 1`] = `
exports[`buildOSSFeatures with a basic license returns the savedObjectsManagement feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
@ -363,7 +363,7 @@ Array [
]
`;
exports[`buildOSSFeatures returns the timelion feature augmented with appropriate sub feature privileges 1`] = `
exports[`buildOSSFeatures with a basic license returns the timelion feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
@ -411,7 +411,489 @@ Array [
]
`;
exports[`buildOSSFeatures returns the visualize feature augmented with appropriate sub feature privileges 1`] = `
exports[`buildOSSFeatures with a basic license returns the visualize feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
"alerting": Object {
"all": Array [],
"read": Array [],
},
"api": Array [],
"app": Array [
"visualize",
"lens",
"kibana",
],
"catalogue": Array [
"visualize",
],
"management": Object {},
"savedObject": Object {
"all": Array [
"visualization",
"query",
"lens",
"url",
],
"read": Array [
"index-pattern",
"search",
"tag",
],
},
"ui": Array [
"show",
"delete",
"save",
"saveQuery",
"createShortUrl",
],
},
"privilegeId": "all",
},
Object {
"privilege": Object {
"app": Array [
"visualize",
"lens",
"kibana",
],
"catalogue": Array [
"visualize",
],
"savedObject": Object {
"all": Array [],
"read": Array [
"index-pattern",
"search",
"visualization",
"query",
"lens",
"tag",
],
},
"ui": Array [
"show",
],
},
"privilegeId": "read",
},
]
`;
exports[`buildOSSFeatures with a enterprise license 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 with a enterprise license returns the dashboard feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
"alerting": Object {
"all": Array [],
"read": Array [],
},
"api": Array [],
"app": Array [
"dashboards",
"kibana",
],
"catalogue": Array [
"dashboard",
],
"management": Object {},
"savedObject": Object {
"all": Array [
"dashboard",
"query",
"url",
],
"read": Array [
"index-pattern",
"search",
"visualization",
"timelion-sheet",
"canvas-workpad",
"lens",
"map",
"tag",
],
},
"ui": Array [
"createNew",
"show",
"showWriteControls",
"saveQuery",
"createShortUrl",
],
},
"privilegeId": "all",
},
Object {
"privilege": Object {
"app": Array [
"dashboards",
"kibana",
],
"catalogue": Array [
"dashboard",
],
"savedObject": Object {
"all": Array [],
"read": Array [
"index-pattern",
"search",
"visualization",
"timelion-sheet",
"canvas-workpad",
"lens",
"map",
"dashboard",
"query",
"tag",
],
},
"ui": Array [
"show",
],
},
"privilegeId": "read",
},
]
`;
exports[`buildOSSFeatures with a enterprise license returns the dev_tools feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
"api": Array [
"console",
],
"app": Array [
"dev_tools",
"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 [
"dev_tools",
"kibana",
],
"catalogue": Array [
"console",
"searchprofiler",
"grokdebugger",
],
"savedObject": Object {
"all": Array [],
"read": Array [],
},
"ui": Array [
"show",
],
},
"privilegeId": "read",
},
]
`;
exports[`buildOSSFeatures with a enterprise license returns the discover feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
"alerting": Object {
"all": Array [],
"read": Array [],
},
"api": Array [],
"app": Array [
"discover",
"kibana",
],
"catalogue": Array [
"discover",
],
"management": Object {},
"savedObject": Object {
"all": Array [
"search",
"query",
"index-pattern",
"url",
],
"read": Array [],
},
"ui": Array [
"show",
"save",
"saveQuery",
"createShortUrl",
],
},
"privilegeId": "all",
},
Object {
"privilege": Object {
"app": Array [
"discover",
"kibana",
],
"catalogue": Array [
"discover",
],
"savedObject": Object {
"all": Array [],
"read": Array [
"index-pattern",
"search",
"query",
],
},
"ui": Array [
"show",
],
},
"privilegeId": "read",
},
]
`;
exports[`buildOSSFeatures with a enterprise license returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {
"app": Array [
"kibana",
],
"catalogue": Array [
"indexPatterns",
],
"management": Object {
"kibana": Array [
"indexPatterns",
],
},
"savedObject": Object {
"all": Array [
"index-pattern",
],
"read": Array [],
},
"ui": Array [
"save",
],
},
"privilegeId": "all",
},
Object {
"privilege": Object {
"app": Array [
"kibana",
],
"catalogue": Array [
"indexPatterns",
],
"management": Object {
"kibana": Array [
"indexPatterns",
],
},
"savedObject": Object {
"all": Array [],
"read": Array [
"index-pattern",
],
},
"ui": Array [],
},
"privilegeId": "read",
},
]
`;
exports[`buildOSSFeatures with a enterprise license 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",
"shareIntoSpace",
],
},
"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 with a enterprise license 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 with a enterprise license returns the visualize feature augmented with appropriate sub feature privileges 1`] = `
Array [
Object {
"privilege": Object {

View file

@ -6,6 +6,7 @@
import { FeatureRegistry } from './feature_registry';
import { ElasticsearchFeatureConfig, KibanaFeatureConfig } from '../common';
import { licensingMock } from '../../licensing/server/mocks';
describe('FeatureRegistry', () => {
describe('Kibana Features', () => {
@ -1280,6 +1281,123 @@ describe('FeatureRegistry', () => {
);
});
it('allows independent sub-feature privileges to register a minimumLicense', () => {
const feature1: KibanaFeatureConfig = {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
all: [],
read: [],
},
ui: [],
},
read: {
savedObject: {
all: [],
read: [],
},
ui: [],
},
},
subFeatures: [
{
name: 'foo',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
id: 'foo',
name: 'foo',
minimumLicense: 'platinum',
includeIn: 'all',
savedObject: {
all: [],
read: [],
},
ui: [],
},
],
},
],
},
],
};
const featureRegistry = new FeatureRegistry();
featureRegistry.registerKibanaFeature(feature1);
});
it('prevents mutually exclusive sub-feature privileges from registering a minimumLicense', () => {
const feature1: KibanaFeatureConfig = {
id: 'test-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
all: [],
read: [],
},
ui: [],
},
read: {
savedObject: {
all: [],
read: [],
},
ui: [],
},
},
subFeatures: [
{
name: 'foo',
privilegeGroups: [
{
groupType: 'mutually_exclusive',
privileges: [
{
id: 'foo',
name: 'foo',
minimumLicense: 'platinum',
includeIn: 'all',
savedObject: {
all: [],
read: [],
},
ui: [],
},
{
id: 'bar',
name: 'Bar',
minimumLicense: 'platinum',
includeIn: 'all',
savedObject: {
all: [],
read: [],
},
ui: [],
},
],
},
],
},
],
};
const featureRegistry = new FeatureRegistry();
expect(() => {
featureRegistry.registerKibanaFeature(feature1);
}).toThrowErrorMatchingInlineSnapshot(
`"child \\"subFeatures\\" fails because [\\"subFeatures\\" at position 0 fails because [child \\"privilegeGroups\\" fails because [\\"privilegeGroups\\" at position 0 fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"minimumLicense\\" fails because [\\"minimumLicense\\" is not allowed]]]]]]]"`
);
});
it('cannot register feature after getAll has been called', () => {
const feature1: KibanaFeatureConfig = {
id: 'test-feature',
@ -1305,6 +1423,89 @@ describe('FeatureRegistry', () => {
`"Features are locked, can't register new features. Attempt to register test-feature-2 failed."`
);
});
describe('#getAllKibanaFeatures', () => {
const features: KibanaFeatureConfig[] = [
{
id: 'gold-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
minimumLicense: 'gold',
privileges: null,
},
{
id: 'unlicensed-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
},
{
id: 'with-sub-feature',
name: 'Test Feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: { savedObject: { all: [], read: [] }, ui: [] },
read: { savedObject: { all: [], read: [] }, ui: [] },
},
minimumLicense: 'platinum',
subFeatures: [
{
name: 'licensed-sub-feature',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
id: 'sub-feature',
includeIn: 'all',
minimumLicense: 'enterprise',
name: 'sub feature',
savedObject: { all: [], read: [] },
ui: [],
},
],
},
],
},
],
},
];
const registry = new FeatureRegistry();
features.forEach((f) => registry.registerKibanaFeature(f));
it('returns all features and sub-feature privileges by default', () => {
const result = registry.getAllKibanaFeatures();
expect(result).toHaveLength(3);
const [, , withSubFeature] = result;
expect(withSubFeature.subFeatures).toHaveLength(1);
expect(withSubFeature.subFeatures[0].privilegeGroups).toHaveLength(1);
expect(withSubFeature.subFeatures[0].privilegeGroups[0].privileges).toHaveLength(1);
});
it('returns features which are satisfied by the current license', () => {
const license = licensingMock.createLicense({ license: { type: 'gold' } });
const result = registry.getAllKibanaFeatures(license);
expect(result).toHaveLength(2);
const ids = result.map((f) => f.id);
expect(ids).toEqual(['gold-feature', 'unlicensed-feature']);
});
it('filters out sub-feature privileges which do not match the current license', () => {
const license = licensingMock.createLicense({ license: { type: 'platinum' } });
const result = registry.getAllKibanaFeatures(license);
expect(result).toHaveLength(3);
const ids = result.map((f) => f.id);
expect(ids).toEqual(['gold-feature', 'unlicensed-feature', 'with-sub-feature']);
const [, , withSubFeature] = result;
expect(withSubFeature.subFeatures).toHaveLength(1);
expect(withSubFeature.subFeatures[0].privilegeGroups).toHaveLength(1);
expect(withSubFeature.subFeatures[0].privilegeGroups[0].privileges).toHaveLength(0);
});
});
});
describe('Elasticsearch Features', () => {

View file

@ -5,6 +5,7 @@
*/
import { cloneDeep, uniq } from 'lodash';
import { ILicense } from '../../licensing/server';
import {
KibanaFeatureConfig,
KibanaFeature,
@ -55,11 +56,30 @@ export class FeatureRegistry {
this.esFeatures[feature.id] = featureCopy;
}
public getAllKibanaFeatures(): KibanaFeature[] {
public getAllKibanaFeatures(license?: ILicense, ignoreLicense = false): KibanaFeature[] {
this.locked = true;
return Object.values(this.kibanaFeatures).map(
(featureConfig) => new KibanaFeature(featureConfig)
);
let features = Object.values(this.kibanaFeatures);
const performLicenseCheck = license && !ignoreLicense;
if (performLicenseCheck) {
features = features.filter((feature) => {
const filter = !feature.minimumLicense || license!.hasAtLeast(feature.minimumLicense);
if (!filter) return false;
feature.subFeatures?.forEach((subFeature) => {
subFeature.privilegeGroups.forEach((group) => {
group.privileges = group.privileges.filter(
(privilege) =>
!privilege.minimumLicense || license!.hasAtLeast(privilege.minimumLicense)
);
});
});
return true;
});
}
return features.map((featureConfig) => new KibanaFeature(featureConfig));
}
public getAllElasticsearchFeatures(): ElasticsearchFeature[] {

View file

@ -21,6 +21,11 @@ const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/;
const reservedFeaturePrrivilegePartRegex = /^(?!reserved_)[a-zA-Z0-9_-]+$/;
export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/;
const validLicenses = ['basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial'];
// sub-feature privileges are only available with a `gold` license or better, so restricting sub-feature privileges
// for `gold` or below doesn't make a whole lot of sense.
const validSubFeaturePrivilegeLicenses = ['platinum', 'enterprise', 'trial'];
const managementSchema = Joi.object().pattern(
managementSectionIdRegex,
Joi.array().items(Joi.string().regex(uiCapabilitiesRegex))
@ -53,10 +58,11 @@ const kibanaPrivilegeSchema = Joi.object({
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
});
const kibanaSubFeaturePrivilegeSchema = Joi.object({
const kibanaIndependentSubFeaturePrivilegeSchema = Joi.object({
id: Joi.string().regex(subFeaturePrivilegePartRegex).required(),
name: Joi.string().required(),
includeIn: Joi.string().allow('all', 'read', 'none').required(),
minimumLicense: Joi.string().valid(...validSubFeaturePrivilegeLicenses),
management: managementSchema,
catalogue: catalogueSchema,
alerting: Joi.object({
@ -72,12 +78,22 @@ const kibanaSubFeaturePrivilegeSchema = Joi.object({
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
});
const kibanaMutuallyExclusiveSubFeaturePrivilegeSchema = kibanaIndependentSubFeaturePrivilegeSchema.keys(
{
minimumLicense: Joi.forbidden(),
}
);
const kibanaSubFeatureSchema = Joi.object({
name: Joi.string().required(),
privilegeGroups: Joi.array().items(
Joi.object({
groupType: Joi.string().valid('mutually_exclusive', 'independent').required(),
privileges: Joi.array().items(kibanaSubFeaturePrivilegeSchema).min(1),
privileges: Joi.when('groupType', {
is: 'mutually_exclusive',
then: Joi.array().items(kibanaMutuallyExclusiveSubFeaturePrivilegeSchema).min(1),
otherwise: Joi.array().items(kibanaIndependentSubFeaturePrivilegeSchema).min(1),
}),
})
),
});
@ -91,14 +107,7 @@ const kibanaFeatureSchema = Joi.object({
category: appCategorySchema,
order: Joi.number(),
excludeFromBasePrivileges: Joi.boolean(),
minimumLicense: Joi.string().valid(
'basic',
'standard',
'gold',
'platinum',
'enterprise',
'trial'
),
minimumLicense: Joi.string().valid(...validLicenses),
app: Joi.array().items(Joi.string()).required(),
management: managementSchema,
catalogue: catalogueSchema,

View file

@ -7,6 +7,7 @@
import { buildOSSFeatures } from './oss_features';
import { featurePrivilegeIterator } from '../../security/server/authorization';
import { KibanaFeature } from '.';
import { LicenseType } from '../../licensing/server';
describe('buildOSSFeatures', () => {
it('returns features including timelion', () => {
@ -46,14 +47,22 @@ 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 KibanaFeature(featureConfig), {
augmentWithSubFeaturePrivileges: true,
})) {
privileges.push(featurePrivilege);
}
expect(privileges).toMatchSnapshot();
(['enterprise', 'basic'] as LicenseType[]).forEach((licenseType) => {
describe(`with a ${licenseType} license`, () => {
it(`returns the ${featureConfig.id} feature augmented with appropriate sub feature privileges`, () => {
const privileges = [];
for (const featurePrivilege of featurePrivilegeIterator(
new KibanaFeature(featureConfig),
{
augmentWithSubFeaturePrivileges: true,
licenseType,
}
)) {
privileges.push(featurePrivilege);
}
expect(privileges).toMatchSnapshot();
});
});
});
});
});

View file

@ -11,15 +11,78 @@ import { httpServerMock, httpServiceMock, coreMock } from '../../../../../src/co
import { LicenseType } from '../../../licensing/server/';
import { licensingMock } from '../../../licensing/server/mocks';
import { RequestHandler } from '../../../../../src/core/server';
import { KibanaFeatureConfig } from '../../common';
import { FeatureKibanaPrivileges, KibanaFeatureConfig, SubFeatureConfig } from '../../common';
function createContextMock(licenseType: LicenseType = 'gold') {
function createContextMock(licenseType: LicenseType = 'platinum') {
return {
core: coreMock.createRequestHandlerContext(),
licensing: licensingMock.createRequestHandlerContext({ license: { type: licenseType } }),
};
}
function createPrivilege(partial: Partial<FeatureKibanaPrivileges> = {}): FeatureKibanaPrivileges {
return {
savedObject: {
all: [],
read: [],
},
ui: [],
...partial,
};
}
function getExpectedSubFeatures(licenseType: LicenseType = 'platinum'): SubFeatureConfig[] {
return [
{
name: 'basicFeature',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
id: 'basicSub1',
name: 'basic sub 1',
includeIn: 'all',
...createPrivilege(),
},
],
},
],
},
{
name: 'platinumFeature',
privilegeGroups: [
{
groupType: 'independent',
privileges:
licenseType !== 'basic'
? [
{
id: 'platinumFeatureSub1',
name: 'platinum sub 1',
includeIn: 'all',
minimumLicense: 'platinum',
...createPrivilege(),
},
]
: [],
},
{
groupType: 'mutually_exclusive',
privileges: [
{
id: 'platinumFeatureMutExSub1',
name: 'platinum sub 1',
includeIn: 'all',
...createPrivilege(),
},
],
},
],
},
];
}
describe('GET /api/features', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeEach(() => {
@ -29,7 +92,11 @@ describe('GET /api/features', () => {
name: 'Feature 1',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: null,
privileges: {
all: createPrivilege(),
read: createPrivilege(),
},
subFeatures: getExpectedSubFeatures(),
});
featureRegistry.registerKibanaFeature({
@ -76,7 +143,12 @@ describe('GET /api/features', () => {
const [call] = mockResponse.ok.mock.calls;
const body = call[0]!.body as KibanaFeatureConfig[];
const features = body.map((feature) => ({ id: feature.id, order: feature.order }));
const features = body.map((feature) => ({
id: feature.id,
order: feature.order,
subFeatures: feature.subFeatures,
}));
expect(features).toEqual([
{
id: 'feature_3',
@ -89,6 +161,7 @@ describe('GET /api/features', () => {
{
id: 'feature_1',
order: undefined,
subFeatures: getExpectedSubFeatures(),
},
{
id: 'licensed_feature',
@ -105,7 +178,11 @@ describe('GET /api/features', () => {
const [call] = mockResponse.ok.mock.calls;
const body = call[0]!.body as KibanaFeatureConfig[];
const features = body.map((feature) => ({ id: feature.id, order: feature.order }));
const features = body.map((feature) => ({
id: feature.id,
order: feature.order,
subFeatures: feature.subFeatures,
}));
expect(features).toEqual([
{
@ -119,6 +196,7 @@ describe('GET /api/features', () => {
{
id: 'feature_1',
order: undefined,
subFeatures: getExpectedSubFeatures('basic'),
},
]);
});
@ -135,7 +213,11 @@ describe('GET /api/features', () => {
const [call] = mockResponse.ok.mock.calls;
const body = call[0]!.body as KibanaFeatureConfig[];
const features = body.map((feature) => ({ id: feature.id, order: feature.order }));
const features = body.map((feature) => ({
id: feature.id,
order: feature.order,
subFeatures: feature.subFeatures,
}));
expect(features).toEqual([
{
@ -149,6 +231,7 @@ describe('GET /api/features', () => {
{
id: 'feature_1',
order: undefined,
subFeatures: getExpectedSubFeatures('basic'),
},
]);
});
@ -165,7 +248,11 @@ describe('GET /api/features', () => {
const [call] = mockResponse.ok.mock.calls;
const body = call[0]!.body as KibanaFeatureConfig[];
const features = body.map((feature) => ({ id: feature.id, order: feature.order }));
const features = body.map((feature) => ({
id: feature.id,
order: feature.order,
subFeatures: feature.subFeatures,
}));
expect(features).toEqual([
{
@ -179,6 +266,7 @@ describe('GET /api/features', () => {
{
id: 'feature_1',
order: undefined,
subFeatures: getExpectedSubFeatures(),
},
{
id: 'licensed_feature',

View file

@ -26,17 +26,15 @@ export function defineRoutes({ router, featureRegistry }: RouteDefinitionParams)
},
},
(context, request, response) => {
const allFeatures = featureRegistry.getAllKibanaFeatures();
const currentLicense = context.licensing!.license;
const allFeatures = featureRegistry.getAllKibanaFeatures(
currentLicense,
request.query.ignoreValidLicenses
);
return response.ok({
body: allFeatures
.filter(
(feature) =>
request.query.ignoreValidLicenses ||
!feature.minimumLicense ||
(context.licensing!.license &&
context.licensing!.license.hasAtLeast(feature.minimumLicense))
)
.sort(
(f1, f2) =>
(f1.order ?? Number.MAX_SAFE_INTEGER) - (f2.order ?? Number.MAX_SAFE_INTEGER)

View file

@ -11,6 +11,7 @@ export const licenseMock = {
create: (): jest.Mocked<SecurityLicense> => ({
isLicenseAvailable: jest.fn(),
isEnabled: jest.fn().mockReturnValue(true),
getType: jest.fn().mockReturnValue('basic'),
getFeatures: jest.fn(),
features$: of(),
}),

View file

@ -6,12 +6,13 @@
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { ILicense } from '../../../licensing/common/types';
import { ILicense, LicenseType } from '../../../licensing/common/types';
import { SecurityLicenseFeatures } from './license_features';
export interface SecurityLicense {
isLicenseAvailable(): boolean;
isEnabled(): boolean;
getType(): LicenseType | undefined;
getFeatures(): SecurityLicenseFeatures;
features$: Observable<SecurityLicenseFeatures>;
}
@ -36,6 +37,8 @@ export class SecurityLicenseService {
isEnabled: () => this.isSecurityEnabledFromRawLicense(rawLicense),
getType: () => rawLicense?.type,
getFeatures: () => this.calculateFeaturesFromRawLicense(rawLicense),
features$: license$.pipe(

View file

@ -23,6 +23,7 @@ export const createRawKibanaPrivileges = (
const licensingService = {
getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures),
getType: () => 'basic' as const,
};
return privilegesFactory(

View file

@ -183,6 +183,7 @@ exports[`it renders without crashing 1`] = `
"_subscribe": [Function],
},
"getFeatures": [MockFunction],
"getType": [MockFunction],
"isEnabled": [MockFunction],
"isLicenseAvailable": [MockFunction],
}

View file

@ -14,6 +14,7 @@ import { mountWithIntl } from '@kbn/test/jest';
import { SubFeatureForm } from './sub_feature_form';
import { EuiCheckbox, EuiButtonGroup } from '@elastic/eui';
import { act } from '@testing-library/react';
import { KibanaFeature } from '../../../../../../../../features/public';
// 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.
@ -234,4 +235,65 @@ describe('SubFeatureForm', () => {
expect(onChange).toHaveBeenCalledWith([]);
});
it('does not render empty privilege groups', () => {
// privilege groups are filtered server-side to only include the
// sub-feature privileges that are allowed by the current license.
const role = createRole([
{
base: [],
feature: {
with_sub_features: ['cool_all'],
},
spaces: [],
},
]);
const feature = new KibanaFeature({
id: 'test_feature',
name: 'test feature',
category: { id: 'test', label: 'test' },
app: [],
privileges: {
all: {
savedObject: { all: [], read: [] },
ui: [],
},
read: {
savedObject: { all: [], read: [] },
ui: [],
},
},
subFeatures: [
{
name: 'subFeature1',
privilegeGroups: [
{
groupType: 'independent',
privileges: [],
},
],
},
],
});
const subFeature1 = new SecuredSubFeature(feature.toRaw().subFeatures![0]);
const kibanaPrivileges = createKibanaPrivileges([feature]);
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
const onChange = jest.fn();
const wrapper = mountWithIntl(
<SubFeatureForm
featureId={feature.id}
subFeature={subFeature1}
selectedFeaturePrivileges={['cool_all']}
privilegeCalculator={calculator}
privilegeIndex={0}
onChange={onChange}
disabled={false}
/>
);
expect(wrapper.children()).toMatchInlineSnapshot(`null`);
});
});

View file

@ -27,12 +27,20 @@ interface Props {
}
export const SubFeatureForm = (props: Props) => {
const groupsWithPrivileges = props.subFeature
.getPrivilegeGroups()
.filter((group) => group.privileges.length > 0);
if (groupsWithPrivileges.length === 0) {
return null;
}
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiText size="s">{props.subFeature.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem>{props.subFeature.getPrivilegeGroups().map(renderPrivilegeGroup)}</EuiFlexItem>
<EuiFlexItem>{groupsWithPrivileges.map(renderPrivilegeGroup)}</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -45,6 +45,7 @@ describe('Security Plugin', () => {
license: {
isLicenseAvailable: expect.any(Function),
isEnabled: expect.any(Function),
getType: expect.any(Function),
getFeatures: expect.any(Function),
features$: expect.any(Observable),
},
@ -73,6 +74,7 @@ describe('Security Plugin', () => {
license: {
isLicenseAvailable: expect.any(Function),
isEnabled: expect.any(Function),
getType: expect.any(Function),
getFeatures: expect.any(Function),
features$: expect.any(Observable),
},

View file

@ -20,6 +20,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);
@ -72,6 +73,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);
@ -164,6 +166,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
predicate: (privilegeId) => privilegeId === 'all',
})
);
@ -270,6 +273,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: false,
licenseType: 'basic',
})
);
@ -394,6 +398,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);
@ -519,6 +524,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);
@ -645,6 +651,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);
@ -771,6 +778,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);
@ -818,6 +826,133 @@ describe('featurePrivilegeIterator', () => {
]);
});
it('excludes sub feature privileges when the minimum license is not met', () => {
const feature = new KibanaFeature({
id: 'foo',
name: 'foo',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
app: ['foo'],
catalogue: ['foo-catalogue'],
management: {
section: ['foo-management'],
},
savedObject: {
all: ['all-type'],
read: ['read-type'],
},
alerting: {
all: ['alerting-all-type'],
read: ['alerting-read-type'],
},
ui: ['ui-action'],
},
read: {
api: ['read-api'],
app: ['foo'],
catalogue: ['foo-catalogue'],
management: {
section: ['foo-management'],
},
savedObject: {
all: [],
read: ['read-type'],
},
alerting: {
read: ['alerting-read-type'],
},
ui: ['ui-action'],
},
},
subFeatures: [
{
name: 'sub feature 1',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
id: 'sub-feature-priv-1',
name: 'first sub feature privilege',
includeIn: 'all',
minimumLicense: 'gold',
api: ['sub-feature-api'],
app: ['sub-app'],
catalogue: ['sub-catalogue'],
management: {
section: ['other-sub-management'],
kibana: ['sub-management'],
},
savedObject: {
all: ['all-sub-type'],
read: ['read-sub-type'],
},
alerting: {
all: ['alerting-all-sub-type'],
read: ['alerting-read-sub-type'],
},
ui: ['ui-sub-type'],
},
],
},
],
},
],
});
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);
expect(actualPrivileges).toEqual([
{
privilegeId: 'all',
privilege: {
api: ['all-api', 'read-api'],
app: ['foo'],
catalogue: ['foo-catalogue'],
management: {
section: ['foo-management'],
},
savedObject: {
all: ['all-type'],
read: ['read-type'],
},
alerting: {
all: ['alerting-all-type'],
read: ['alerting-read-type'],
},
ui: ['ui-action'],
},
},
{
privilegeId: 'read',
privilege: {
api: ['read-api'],
app: ['foo'],
catalogue: ['foo-catalogue'],
management: {
section: ['foo-management'],
},
savedObject: {
all: [],
read: ['read-type'],
},
alerting: {
read: ['alerting-read-type'],
},
ui: ['ui-action'],
},
},
]);
});
it(`can augment primary feature privileges even if they don't specify their own`, () => {
const feature = new KibanaFeature({
id: 'foo',
@ -878,6 +1013,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);
@ -995,6 +1131,7 @@ describe('featurePrivilegeIterator', () => {
const actualPrivileges = Array.from(
featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
);

View file

@ -5,11 +5,13 @@
*/
import _ from 'lodash';
import { LicenseType } from '../../../../../licensing/server';
import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server';
import { subFeaturePrivilegeIterator } from './sub_feature_privilege_iterator';
interface IteratorOptions {
augmentWithSubFeaturePrivileges: boolean;
licenseType: LicenseType;
predicate?: (privilegeId: string, privilege: FeatureKibanaPrivileges) => boolean;
}
@ -25,7 +27,10 @@ export function* featurePrivilegeIterator(
}
if (options.augmentWithSubFeaturePrivileges) {
yield { privilegeId, privilege: mergeWithSubFeatures(privilegeId, privilege, feature) };
yield {
privilegeId,
privilege: mergeWithSubFeatures(privilegeId, privilege, feature, options.licenseType),
};
} else {
yield { privilegeId, privilege };
}
@ -35,10 +40,11 @@ export function* featurePrivilegeIterator(
function mergeWithSubFeatures(
privilegeId: string,
privilege: FeatureKibanaPrivileges,
feature: KibanaFeature
feature: KibanaFeature,
licenseType: LicenseType
) {
const mergedConfig = _.cloneDeep(privilege);
for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature)) {
for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature, licenseType)) {
if (subFeaturePrivilege.includeIn !== 'read' && subFeaturePrivilege.includeIn !== privilegeId) {
continue;
}

View file

@ -4,14 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { LicenseType } from '../../../../../licensing/server';
import { KibanaFeature, SubFeaturePrivilegeConfig } from '../../../../../features/common';
export function* subFeaturePrivilegeIterator(
feature: KibanaFeature
feature: KibanaFeature,
licenseType: LicenseType
): IterableIterator<SubFeaturePrivilegeConfig> {
for (const subFeature of feature.subFeatures) {
for (const group of subFeature.privilegeGroups) {
yield* group.privileges;
yield* group.privileges.filter(
(privilege) => !privilege.minimumLicense || privilege.minimumLicense <= licenseType
);
}
}
}

View file

@ -48,6 +48,7 @@ describe('features', () => {
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesService, mockLicenseService);
@ -89,6 +90,7 @@ describe('features', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -177,6 +179,7 @@ describe('features', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -245,6 +248,7 @@ describe('features', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -368,6 +372,7 @@ describe('features', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -443,6 +448,7 @@ describe('features', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -508,6 +514,7 @@ describe('features', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -574,6 +581,7 @@ describe('features', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -633,6 +641,7 @@ describe('reserved', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -671,6 +680,7 @@ describe('reserved', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -737,6 +747,7 @@ describe('reserved', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -800,6 +811,7 @@ describe('subFeatures', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -928,6 +940,7 @@ describe('subFeatures', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -1135,6 +1148,7 @@ describe('subFeatures', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -1278,6 +1292,7 @@ describe('subFeatures', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -1446,6 +1461,7 @@ describe('subFeatures', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -1576,6 +1592,7 @@ describe('subFeatures', () => {
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: false }),
getType: jest.fn().mockReturnValue('basic'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
@ -1705,4 +1722,502 @@ describe('subFeatures', () => {
]);
});
});
describe(`when license allows subfeatures, but not a specific sub feature`, () => {
test(`should create minimal privileges, but not augment the primary feature privileges or create the disallowed sub-feature privileges`, () => {
const features: KibanaFeature[] = [
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
all: [],
read: [],
},
ui: ['foo'],
},
read: {
savedObject: {
all: [],
read: [],
},
ui: ['foo'],
},
},
subFeatures: [
{
name: 'subFeature1',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
id: 'subFeaturePriv1',
name: 'sub feature priv 1',
includeIn: 'read',
savedObject: {
all: ['all-sub-feature-type'],
read: ['read-sub-feature-type'],
},
ui: ['sub-feature-ui'],
},
],
},
{
groupType: 'independent',
privileges: [
{
id: 'licensedSubFeaturePriv',
name: 'licensed sub feature priv',
includeIn: 'read',
minimumLicense: 'platinum',
savedObject: {
all: ['all-licensed-sub-feature-type'],
read: ['read-licensed-sub-feature-type'],
},
ui: ['licensed-sub-feature-ui'],
},
],
},
],
},
],
}),
];
const mockFeaturesPlugin = {
getKibanaFeatures: jest.fn().mockReturnValue(features),
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('gold'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`);
expect(actual.features).not.toHaveProperty(`foo.licensedSubFeaturePriv`);
expect(actual.features).toHaveProperty(`foo.all`, [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
expect(actual.features).toHaveProperty(`foo.minimal_all`, [
actions.login,
actions.version,
actions.ui.get('foo', 'foo'),
]);
expect(actual.features).toHaveProperty(`foo.read`, [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
expect(actual.features).toHaveProperty(`foo.minimal_read`, [
actions.login,
actions.version,
actions.ui.get('foo', 'foo'),
]);
expect(actual).toHaveProperty('global.all', [
actions.login,
actions.version,
actions.api.get('features'),
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
actions.ui.get('catalogue', 'spaces'),
actions.ui.get('enterpriseSearch', 'all'),
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
expect(actual).toHaveProperty('global.read', [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
expect(actual).toHaveProperty('space.all', [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
expect(actual).toHaveProperty('space.read', [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
]);
});
});
describe(`when license allows subfeatures, but and a licensed sub feature`, () => {
test(`should create minimal privileges, augment the primary feature privileges, and create the licensed sub-feature privileges`, () => {
const features: KibanaFeature[] = [
new KibanaFeature({
id: 'foo',
name: 'Foo KibanaFeature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
all: [],
read: [],
},
ui: ['foo'],
},
read: {
savedObject: {
all: [],
read: [],
},
ui: ['foo'],
},
},
subFeatures: [
{
name: 'subFeature1',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
id: 'subFeaturePriv1',
name: 'sub feature priv 1',
includeIn: 'read',
savedObject: {
all: ['all-sub-feature-type'],
read: ['read-sub-feature-type'],
},
ui: ['sub-feature-ui'],
},
],
},
{
groupType: 'independent',
privileges: [
{
id: 'licensedSubFeaturePriv',
name: 'licensed sub feature priv',
includeIn: 'read',
minimumLicense: 'platinum',
savedObject: {
all: ['all-licensed-sub-feature-type'],
read: ['read-licensed-sub-feature-type'],
},
ui: ['licensed-sub-feature-ui'],
},
],
},
],
},
],
}),
];
const mockFeaturesPlugin = {
getKibanaFeatures: jest.fn().mockReturnValue(features),
};
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
getType: jest.fn().mockReturnValue('platinum'),
};
const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`);
expect(actual.features).toHaveProperty(`foo.licensedSubFeaturePriv`);
expect(actual.features).toHaveProperty(`foo.all`, [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
]);
expect(actual.features).toHaveProperty(`foo.minimal_all`, [
actions.login,
actions.version,
actions.ui.get('foo', 'foo'),
]);
expect(actual.features).toHaveProperty(`foo.read`, [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
]);
expect(actual.features).toHaveProperty(`foo.minimal_read`, [
actions.login,
actions.version,
actions.ui.get('foo', 'foo'),
]);
expect(actual).toHaveProperty('global.all', [
actions.login,
actions.version,
actions.api.get('features'),
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
actions.ui.get('catalogue', 'spaces'),
actions.ui.get('enterpriseSearch', 'all'),
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
]);
expect(actual).toHaveProperty('global.read', [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
]);
expect(actual).toHaveProperty('space.all', [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
]);
expect(actual).toHaveProperty('space.read', [
actions.login,
actions.version,
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-sub-feature-type', 'get'),
actions.savedObject.get('all-sub-feature-type', 'find'),
actions.savedObject.get('all-sub-feature-type', 'create'),
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-sub-feature-type', 'update'),
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-sub-feature-type', 'delete'),
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
actions.savedObject.get('all-licensed-sub-feature-type', 'find'),
actions.savedObject.get('all-licensed-sub-feature-type', 'create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'),
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-sub-feature-type', 'get'),
actions.savedObject.get('read-sub-feature-type', 'find'),
actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'get'),
actions.savedObject.get('read-licensed-sub-feature-type', 'find'),
actions.ui.get('foo', 'foo'),
actions.ui.get('foo', 'sub-feature-ui'),
actions.ui.get('foo', 'licensed-sub-feature-ui'),
]);
});
});
});

View file

@ -25,7 +25,7 @@ export interface PrivilegesService {
export function privilegesFactory(
actions: Actions,
featuresService: FeaturesPluginSetup,
licenseService: Pick<SecurityLicense, 'getFeatures'>
licenseService: Pick<SecurityLicense, 'getFeatures' | 'getType'>
) {
const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions);
@ -33,6 +33,7 @@ export function privilegesFactory(
get() {
const features = featuresService.getKibanaFeatures();
const { allowSubFeaturePrivileges } = licenseService.getFeatures();
const licenseType = licenseService.getType()!;
const basePrivilegeFeatures = features.filter(
(feature) => !feature.excludeFromBasePrivileges
);
@ -43,6 +44,7 @@ export function privilegesFactory(
basePrivilegeFeatures.forEach((feature) => {
for (const { privilegeId, privilege } of featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType,
predicate: (pId, featurePrivilege) => !featurePrivilege.excludeFromBasePrivileges,
})) {
const privilegeActions = featurePrivilegeBuilder.getActions(privilege, feature);
@ -61,6 +63,7 @@ export function privilegesFactory(
featurePrivileges[feature.id] = {};
for (const featurePrivilege of featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType,
})) {
featurePrivileges[feature.id][featurePrivilege.privilegeId] = [
actions.login,
@ -72,6 +75,7 @@ export function privilegesFactory(
if (allowSubFeaturePrivileges && feature.subFeatures?.length > 0) {
for (const featurePrivilege of featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: false,
licenseType,
})) {
featurePrivileges[feature.id][`minimal_${featurePrivilege.privilegeId}`] = [
actions.login,
@ -80,7 +84,7 @@ export function privilegesFactory(
];
}
for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature)) {
for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature, licenseType)) {
featurePrivileges[feature.id][subFeaturePrivilege.id] = [
actions.login,
actions.version,

View file

@ -110,6 +110,7 @@ describe('Security Plugin', () => {
},
},
"getFeatures": [Function],
"getType": [Function],
"isEnabled": [Function],
"isLicenseAvailable": [Function],
},