mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Add support for licensed sub feature privileges (#80905)
This commit is contained in:
parent
bc3bb2afa8
commit
fe33579272
22 changed files with 1641 additions and 55 deletions
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue