mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Migrate from Joi to @kbn/config-schema in "home" and "features" plugins (#100201)
* add a link for issue to remove circular deps * features: migrate from joi to config-schema * update tests * migrate home tutorials to config-schema * migrate home dataset validation to config schema * remove unnecessary type. we cannot guarantee this is a valid SO * address Pierres comments
This commit is contained in:
parent
15abf24339
commit
574f6595ad
16 changed files with 442 additions and 417 deletions
|
@ -15,7 +15,6 @@ export type { TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials
|
||||||
export { TutorialsCategory } from './tutorials';
|
export { TutorialsCategory } from './tutorials';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ParamTypes,
|
|
||||||
InstructionSetSchema,
|
InstructionSetSchema,
|
||||||
ParamsSchema,
|
ParamsSchema,
|
||||||
InstructionsSchema,
|
InstructionsSchema,
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SavedObject } from 'src/core/server';
|
import type { SampleDatasetSchema } from './sample_dataset_schema';
|
||||||
|
export type { SampleDatasetSchema, AppLinkSchema, DataIndexSchema } from './sample_dataset_schema';
|
||||||
|
|
||||||
export enum DatasetStatusTypes {
|
export enum DatasetStatusTypes {
|
||||||
NOT_INSTALLED = 'not_installed',
|
NOT_INSTALLED = 'not_installed',
|
||||||
|
@ -26,57 +27,4 @@ export enum EmbeddableTypes {
|
||||||
SEARCH_EMBEDDABLE_TYPE = 'search',
|
SEARCH_EMBEDDABLE_TYPE = 'search',
|
||||||
VISUALIZE_EMBEDDABLE_TYPE = 'visualization',
|
VISUALIZE_EMBEDDABLE_TYPE = 'visualization',
|
||||||
}
|
}
|
||||||
export interface DataIndexSchema {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
// path to newline delimented JSON file containing data relative to KIBANA_HOME
|
|
||||||
dataPath: string;
|
|
||||||
|
|
||||||
// Object defining Elasticsearch field mappings (contents of index.mappings.type.properties)
|
|
||||||
fields: object;
|
|
||||||
|
|
||||||
// times fields that will be updated relative to now when data is installed
|
|
||||||
timeFields: string[];
|
|
||||||
|
|
||||||
// Reference to now in your test data set.
|
|
||||||
// When data is installed, timestamps are converted to the present time.
|
|
||||||
// The distance between a timestamp and currentTimeMarker is preserved but the date and time will change.
|
|
||||||
// For example:
|
|
||||||
// sample data set: timestamp: 2018-01-01T00:00:00Z, currentTimeMarker: 2018-01-01T12:00:00Z
|
|
||||||
// installed data set: timestamp: 2018-04-18T20:33:14Z, currentTimeMarker: 2018-04-19T08:33:14Z
|
|
||||||
currentTimeMarker: string;
|
|
||||||
|
|
||||||
// Set to true to move timestamp to current week, preserving day of week and time of day
|
|
||||||
// Relative distance from timestamp to currentTimeMarker will not remain the same
|
|
||||||
preserveDayOfWeekTimeOfDay: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppLinkSchema {
|
|
||||||
path: string;
|
|
||||||
icon: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SampleDatasetSchema<T = unknown> {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
previewImagePath: string;
|
|
||||||
darkPreviewImagePath: string;
|
|
||||||
|
|
||||||
// saved object id of main dashboard for sample data set
|
|
||||||
overviewDashboard: string;
|
|
||||||
appLinks: AppLinkSchema[];
|
|
||||||
|
|
||||||
// saved object id of default index-pattern for sample data set
|
|
||||||
defaultIndex: string;
|
|
||||||
|
|
||||||
// Kibana saved objects (index patter, visualizations, dashboard, ...)
|
|
||||||
// Should provide a nice demo of Kibana's functionality with the sample data set
|
|
||||||
savedObjects: Array<SavedObject<T>>;
|
|
||||||
dataIndices: DataIndexSchema[];
|
|
||||||
status?: string | undefined;
|
|
||||||
statusMsg?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SampleDatasetProvider = () => SampleDatasetSchema;
|
export type SampleDatasetProvider = () => SampleDatasetSchema;
|
||||||
|
|
|
@ -5,22 +5,27 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
import type { Writable } from '@kbn/utility-types';
|
||||||
|
import { schema, TypeOf } from '@kbn/config-schema';
|
||||||
|
|
||||||
import Joi from 'joi';
|
const idRegExp = /^[a-zA-Z0-9-]+$/;
|
||||||
|
const dataIndexSchema = schema.object({
|
||||||
const dataIndexSchema = Joi.object({
|
id: schema.string({
|
||||||
id: Joi.string()
|
validate(value: string) {
|
||||||
.regex(/^[a-zA-Z0-9-]+$/)
|
if (!idRegExp.test(value)) {
|
||||||
.required(),
|
return `Does not satisfy regexp: ${idRegExp.toString()}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
// path to newline delimented JSON file containing data relative to KIBANA_HOME
|
// path to newline delimented JSON file containing data relative to KIBANA_HOME
|
||||||
dataPath: Joi.string().required(),
|
dataPath: schema.string(),
|
||||||
|
|
||||||
// Object defining Elasticsearch field mappings (contents of index.mappings.type.properties)
|
// Object defining Elasticsearch field mappings (contents of index.mappings.type.properties)
|
||||||
fields: Joi.object().required(),
|
fields: schema.recordOf(schema.string(), schema.any()),
|
||||||
|
|
||||||
// times fields that will be updated relative to now when data is installed
|
// times fields that will be updated relative to now when data is installed
|
||||||
timeFields: Joi.array().items(Joi.string()).required(),
|
timeFields: schema.arrayOf(schema.string()),
|
||||||
|
|
||||||
// Reference to now in your test data set.
|
// Reference to now in your test data set.
|
||||||
// When data is installed, timestamps are converted to the present time.
|
// When data is installed, timestamps are converted to the present time.
|
||||||
|
@ -28,37 +33,66 @@ const dataIndexSchema = Joi.object({
|
||||||
// For example:
|
// For example:
|
||||||
// sample data set: timestamp: 2018-01-01T00:00:00Z, currentTimeMarker: 2018-01-01T12:00:00Z
|
// sample data set: timestamp: 2018-01-01T00:00:00Z, currentTimeMarker: 2018-01-01T12:00:00Z
|
||||||
// installed data set: timestamp: 2018-04-18T20:33:14Z, currentTimeMarker: 2018-04-19T08:33:14Z
|
// installed data set: timestamp: 2018-04-18T20:33:14Z, currentTimeMarker: 2018-04-19T08:33:14Z
|
||||||
currentTimeMarker: Joi.string().isoDate().required(),
|
currentTimeMarker: schema.string({
|
||||||
|
validate(value: string) {
|
||||||
|
if (isNaN(Date.parse(value))) {
|
||||||
|
return 'Expected a valid string in iso format';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
// Set to true to move timestamp to current week, preserving day of week and time of day
|
// Set to true to move timestamp to current week, preserving day of week and time of day
|
||||||
// Relative distance from timestamp to currentTimeMarker will not remain the same
|
// Relative distance from timestamp to currentTimeMarker will not remain the same
|
||||||
preserveDayOfWeekTimeOfDay: Joi.boolean().default(false),
|
preserveDayOfWeekTimeOfDay: schema.boolean({ defaultValue: false }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const appLinkSchema = Joi.object({
|
export type DataIndexSchema = TypeOf<typeof dataIndexSchema>;
|
||||||
path: Joi.string().required(),
|
|
||||||
label: Joi.string().required(),
|
|
||||||
icon: Joi.string().required(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sampleDataSchema = {
|
const appLinkSchema = schema.object({
|
||||||
id: Joi.string()
|
path: schema.string(),
|
||||||
.regex(/^[a-zA-Z0-9-]+$/)
|
label: schema.string(),
|
||||||
.required(),
|
icon: schema.string(),
|
||||||
name: Joi.string().required(),
|
});
|
||||||
description: Joi.string().required(),
|
export type AppLinkSchema = TypeOf<typeof appLinkSchema>;
|
||||||
previewImagePath: Joi.string().required(),
|
|
||||||
darkPreviewImagePath: Joi.string(),
|
export const sampleDataSchema = schema.object({
|
||||||
|
id: schema.string({
|
||||||
|
validate(value: string) {
|
||||||
|
if (!idRegExp.test(value)) {
|
||||||
|
return `Does not satisfy regexp: ${idRegExp.toString()}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
name: schema.string(),
|
||||||
|
description: schema.string(),
|
||||||
|
previewImagePath: schema.string(),
|
||||||
|
darkPreviewImagePath: schema.maybe(schema.string()),
|
||||||
|
|
||||||
// saved object id of main dashboard for sample data set
|
// saved object id of main dashboard for sample data set
|
||||||
overviewDashboard: Joi.string().required(),
|
overviewDashboard: schema.string(),
|
||||||
appLinks: Joi.array().items(appLinkSchema).default([]),
|
appLinks: schema.arrayOf(appLinkSchema, { defaultValue: [] }),
|
||||||
|
|
||||||
// saved object id of default index-pattern for sample data set
|
// saved object id of default index-pattern for sample data set
|
||||||
defaultIndex: Joi.string().required(),
|
defaultIndex: schema.string(),
|
||||||
|
|
||||||
// Kibana saved objects (index patter, visualizations, dashboard, ...)
|
// Kibana saved objects (index patter, visualizations, dashboard, ...)
|
||||||
// Should provide a nice demo of Kibana's functionality with the sample data set
|
// Should provide a nice demo of Kibana's functionality with the sample data set
|
||||||
savedObjects: Joi.array().items(Joi.object()).required(),
|
savedObjects: schema.arrayOf(
|
||||||
dataIndices: Joi.array().items(dataIndexSchema).required(),
|
schema.object(
|
||||||
};
|
{
|
||||||
|
id: schema.string(),
|
||||||
|
type: schema.string(),
|
||||||
|
attributes: schema.any(),
|
||||||
|
references: schema.arrayOf(schema.any()),
|
||||||
|
version: schema.maybe(schema.any()),
|
||||||
|
},
|
||||||
|
{ unknowns: 'allow' }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
dataIndices: schema.arrayOf(dataIndexSchema),
|
||||||
|
|
||||||
|
status: schema.maybe(schema.string()),
|
||||||
|
statusMsg: schema.maybe(schema.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SampleDatasetSchema = Writable<TypeOf<typeof sampleDataSchema>>;
|
||||||
|
|
|
@ -7,7 +7,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { schema } from '@kbn/config-schema';
|
import { schema } from '@kbn/config-schema';
|
||||||
import { IRouter, Logger, IScopedClusterClient } from 'src/core/server';
|
import type {
|
||||||
|
IRouter,
|
||||||
|
Logger,
|
||||||
|
IScopedClusterClient,
|
||||||
|
SavedObjectsBulkCreateObject,
|
||||||
|
} from 'src/core/server';
|
||||||
import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types';
|
import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types';
|
||||||
import { createIndexName } from '../lib/create_index_name';
|
import { createIndexName } from '../lib/create_index_name';
|
||||||
import {
|
import {
|
||||||
|
@ -148,8 +153,9 @@ export function createInstallRoute(
|
||||||
|
|
||||||
const client = getClient({ includedHiddenTypes });
|
const client = getClient({ includedHiddenTypes });
|
||||||
|
|
||||||
|
const savedObjects = sampleDataset.savedObjects as SavedObjectsBulkCreateObject[];
|
||||||
createResults = await client.bulkCreate(
|
createResults = await client.bulkCreate(
|
||||||
sampleDataset.savedObjects.map(({ version, ...savedObject }) => savedObject),
|
savedObjects.map(({ version, ...savedObject }) => savedObject),
|
||||||
{ overwrite: true }
|
{ overwrite: true }
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Joi from 'joi';
|
|
||||||
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
|
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
|
||||||
import { SavedObject } from 'src/core/public';
|
import { SavedObject } from 'src/core/public';
|
||||||
import {
|
import {
|
||||||
|
@ -55,11 +54,13 @@ export class SampleDataRegistry {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
registerSampleDataset: (specProvider: SampleDatasetProvider) => {
|
registerSampleDataset: (specProvider: SampleDatasetProvider) => {
|
||||||
const { error, value } = Joi.validate(specProvider(), sampleDataSchema);
|
let value: SampleDatasetSchema;
|
||||||
|
try {
|
||||||
if (error) {
|
value = sampleDataSchema.validate(specProvider());
|
||||||
|
} catch (error) {
|
||||||
throw new Error(`Unable to register sample dataset spec because it's invalid. ${error}`);
|
throw new Error(`Unable to register sample dataset spec because it's invalid. ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultIndexSavedObjectJson = value.savedObjects.find((savedObjectJson: any) => {
|
const defaultIndexSavedObjectJson = value.savedObjects.find((savedObjectJson: any) => {
|
||||||
return (
|
return (
|
||||||
savedObjectJson.type === 'index-pattern' && savedObjectJson.id === value.defaultIndex
|
savedObjectJson.type === 'index-pattern' && savedObjectJson.id === value.defaultIndex
|
||||||
|
|
|
@ -12,7 +12,6 @@ export type { TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials
|
||||||
export { TutorialsCategory } from './lib/tutorials_registry_types';
|
export { TutorialsCategory } from './lib/tutorials_registry_types';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ParamTypes,
|
|
||||||
InstructionSetSchema,
|
InstructionSetSchema,
|
||||||
ParamsSchema,
|
ParamsSchema,
|
||||||
InstructionsSchema,
|
InstructionsSchema,
|
||||||
|
|
|
@ -5,121 +5,153 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
import { schema, TypeOf } from '@kbn/config-schema';
|
||||||
|
|
||||||
import Joi from 'joi';
|
const dashboardSchema = schema.object({
|
||||||
|
// Dashboard saved object id
|
||||||
const PARAM_TYPES = {
|
id: schema.string(),
|
||||||
NUMBER: 'number',
|
|
||||||
STRING: 'string',
|
|
||||||
};
|
|
||||||
|
|
||||||
const TUTORIAL_CATEGORY = {
|
|
||||||
LOGGING: 'logging',
|
|
||||||
SECURITY_SOLUTION: 'security solution',
|
|
||||||
METRICS: 'metrics',
|
|
||||||
OTHER: 'other',
|
|
||||||
};
|
|
||||||
|
|
||||||
const dashboardSchema = Joi.object({
|
|
||||||
id: Joi.string().required(), // Dashboard saved object id
|
|
||||||
linkLabel: Joi.string().when('isOverview', {
|
|
||||||
is: true,
|
|
||||||
then: Joi.required(),
|
|
||||||
}),
|
|
||||||
// Is this an Overview / Entry Point dashboard?
|
// Is this an Overview / Entry Point dashboard?
|
||||||
isOverview: Joi.boolean().required(),
|
isOverview: schema.boolean(),
|
||||||
|
linkLabel: schema.conditional(
|
||||||
|
schema.siblingRef('isOverview'),
|
||||||
|
true,
|
||||||
|
schema.string(),
|
||||||
|
schema.maybe(schema.string())
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
export type DashboardSchema = TypeOf<typeof dashboardSchema>;
|
||||||
|
|
||||||
const artifactsSchema = Joi.object({
|
const artifactsSchema = schema.object({
|
||||||
// Fields present in Elasticsearch documents created by this product.
|
// Fields present in Elasticsearch documents created by this product.
|
||||||
exportedFields: Joi.object({
|
exportedFields: schema.maybe(
|
||||||
documentationUrl: Joi.string().required(),
|
schema.object({
|
||||||
}),
|
documentationUrl: schema.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
// Kibana dashboards created by this product.
|
// Kibana dashboards created by this product.
|
||||||
dashboards: Joi.array().items(dashboardSchema).required(),
|
dashboards: schema.arrayOf(dashboardSchema),
|
||||||
application: Joi.object({
|
application: schema.maybe(
|
||||||
path: Joi.string().required(),
|
schema.object({
|
||||||
label: Joi.string().required(),
|
path: schema.string(),
|
||||||
|
label: schema.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
export type ArtifactsSchema = TypeOf<typeof artifactsSchema>;
|
||||||
|
|
||||||
|
const statusCheckSchema = schema.object({
|
||||||
|
title: schema.maybe(schema.string()),
|
||||||
|
text: schema.maybe(schema.string()),
|
||||||
|
btnLabel: schema.maybe(schema.string()),
|
||||||
|
success: schema.maybe(schema.string()),
|
||||||
|
error: schema.maybe(schema.string()),
|
||||||
|
esHitsCheck: schema.object({
|
||||||
|
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
|
||||||
|
query: schema.recordOf(schema.string(), schema.any()),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusCheckSchema = Joi.object({
|
const instructionSchema = schema.object({
|
||||||
title: Joi.string(),
|
title: schema.maybe(schema.string()),
|
||||||
text: Joi.string(),
|
textPre: schema.maybe(schema.string()),
|
||||||
btnLabel: Joi.string(),
|
commands: schema.maybe(schema.arrayOf(schema.string())),
|
||||||
success: Joi.string(),
|
textPost: schema.maybe(schema.string()),
|
||||||
error: Joi.string(),
|
});
|
||||||
esHitsCheck: Joi.object({
|
export type Instruction = TypeOf<typeof instructionSchema>;
|
||||||
index: Joi.alternatives().try(Joi.string(), Joi.array().items(Joi.string())).required(),
|
|
||||||
query: Joi.object().required(),
|
const instructionVariantSchema = schema.object({
|
||||||
}).required(),
|
id: schema.string(),
|
||||||
|
instructions: schema.arrayOf(instructionSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const instructionSchema = Joi.object({
|
export type InstructionVariant = TypeOf<typeof instructionVariantSchema>;
|
||||||
title: Joi.string(),
|
|
||||||
textPre: Joi.string(),
|
|
||||||
commands: Joi.array().items(Joi.string().allow('')),
|
|
||||||
textPost: Joi.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const instructionVariantSchema = Joi.object({
|
const instructionSetSchema = schema.object({
|
||||||
id: Joi.string().required(),
|
title: schema.maybe(schema.string()),
|
||||||
instructions: Joi.array().items(instructionSchema).required(),
|
callOut: schema.maybe(
|
||||||
});
|
schema.object({
|
||||||
|
title: schema.string(),
|
||||||
const instructionSetSchema = Joi.object({
|
message: schema.maybe(schema.string()),
|
||||||
title: Joi.string(),
|
iconType: schema.maybe(schema.string()),
|
||||||
callOut: Joi.object({
|
})
|
||||||
title: Joi.string().required(),
|
),
|
||||||
message: Joi.string(),
|
|
||||||
iconType: Joi.string(),
|
|
||||||
}),
|
|
||||||
// Variants (OSes, languages, etc.) for which tutorial instructions are specified.
|
// Variants (OSes, languages, etc.) for which tutorial instructions are specified.
|
||||||
instructionVariants: Joi.array().items(instructionVariantSchema).required(),
|
instructionVariants: schema.arrayOf(instructionVariantSchema),
|
||||||
statusCheck: statusCheckSchema,
|
statusCheck: schema.maybe(statusCheckSchema),
|
||||||
});
|
});
|
||||||
|
export type InstructionSetSchema = TypeOf<typeof instructionSetSchema>;
|
||||||
|
|
||||||
const paramSchema = Joi.object({
|
const idRegExp = /^[a-zA-Z_]+$/;
|
||||||
defaultValue: Joi.required(),
|
const paramSchema = schema.object({
|
||||||
id: Joi.string()
|
defaultValue: schema.any(),
|
||||||
.regex(/^[a-zA-Z_]+$/)
|
id: schema.string({
|
||||||
.required(),
|
validate(value: string) {
|
||||||
label: Joi.string().required(),
|
if (!idRegExp.test(value)) {
|
||||||
type: Joi.string().valid(Object.values(PARAM_TYPES)).required(),
|
return `Does not satisfy regexp ${idRegExp.toString()}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
label: schema.string(),
|
||||||
|
type: schema.oneOf([schema.literal('number'), schema.literal('string')]),
|
||||||
});
|
});
|
||||||
|
export type ParamsSchema = TypeOf<typeof paramSchema>;
|
||||||
|
|
||||||
const instructionsSchema = Joi.object({
|
const instructionsSchema = schema.object({
|
||||||
instructionSets: Joi.array().items(instructionSetSchema).required(),
|
instructionSets: schema.arrayOf(instructionSetSchema),
|
||||||
params: Joi.array().items(paramSchema),
|
params: schema.maybe(schema.arrayOf(paramSchema)),
|
||||||
});
|
});
|
||||||
|
export type InstructionsSchema = TypeOf<typeof instructionSchema>;
|
||||||
|
|
||||||
export const tutorialSchema = {
|
const tutorialIdRegExp = /^[a-zA-Z0-9-]+$/;
|
||||||
id: Joi.string()
|
export const tutorialSchema = schema.object({
|
||||||
.regex(/^[a-zA-Z0-9-]+$/)
|
id: schema.string({
|
||||||
.required(),
|
validate(value: string) {
|
||||||
category: Joi.string().valid(Object.values(TUTORIAL_CATEGORY)).required(),
|
if (!tutorialIdRegExp.test(value)) {
|
||||||
name: Joi.string().required(),
|
return `Does not satisfy regexp ${tutorialIdRegExp.toString()}`;
|
||||||
moduleName: Joi.string(),
|
}
|
||||||
isBeta: Joi.boolean().default(false),
|
},
|
||||||
shortDescription: Joi.string().required(),
|
}),
|
||||||
euiIconType: Joi.string(), // EUI icon type string, one of https://elastic.github.io/eui/#/icons
|
category: schema.oneOf([
|
||||||
longDescription: Joi.string().required(),
|
schema.literal('logging'),
|
||||||
completionTimeMinutes: Joi.number().integer(),
|
schema.literal('security'),
|
||||||
previewImagePath: Joi.string(),
|
schema.literal('metrics'),
|
||||||
|
schema.literal('other'),
|
||||||
|
]),
|
||||||
|
name: schema.string({
|
||||||
|
validate(value: string) {
|
||||||
|
if (value === '') {
|
||||||
|
return 'is not allowed to be empty';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
moduleName: schema.maybe(schema.string()),
|
||||||
|
isBeta: schema.maybe(schema.boolean()),
|
||||||
|
shortDescription: schema.string(),
|
||||||
|
// EUI icon type string, one of https://elastic.github.io/eui/#/icons
|
||||||
|
euiIconType: schema.maybe(schema.string()),
|
||||||
|
longDescription: schema.string(),
|
||||||
|
completionTimeMinutes: schema.maybe(
|
||||||
|
schema.number({
|
||||||
|
validate(value: number) {
|
||||||
|
if (!Number.isInteger(value)) {
|
||||||
|
return 'Expected to be a valid integer number';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
previewImagePath: schema.maybe(schema.string()),
|
||||||
// kibana and elastic cluster running on prem
|
// kibana and elastic cluster running on prem
|
||||||
onPrem: instructionsSchema.required(),
|
onPrem: instructionsSchema,
|
||||||
|
|
||||||
// kibana and elastic cluster running in elastic's cloud
|
// kibana and elastic cluster running in elastic's cloud
|
||||||
elasticCloud: instructionsSchema,
|
elasticCloud: schema.maybe(instructionsSchema),
|
||||||
|
|
||||||
// kibana running on prem and elastic cluster running in elastic's cloud
|
// kibana running on prem and elastic cluster running in elastic's cloud
|
||||||
onPremElasticCloud: instructionsSchema,
|
onPremElasticCloud: schema.maybe(instructionsSchema),
|
||||||
|
|
||||||
// Elastic stack artifacts produced by product when it is setup and run.
|
// Elastic stack artifacts produced by product when it is setup and run.
|
||||||
artifacts: artifactsSchema,
|
artifacts: schema.maybe(artifactsSchema),
|
||||||
|
|
||||||
// saved objects used by data module.
|
// saved objects used by data module.
|
||||||
savedObjects: Joi.array().items(),
|
savedObjects: schema.maybe(schema.arrayOf(schema.any())),
|
||||||
savedObjectsInstallMsg: Joi.string(),
|
savedObjectsInstallMsg: schema.maybe(schema.string()),
|
||||||
};
|
});
|
||||||
|
|
||||||
|
export type TutorialSchema = TypeOf<typeof tutorialSchema>;
|
||||||
|
|
|
@ -6,8 +6,18 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IconType } from '@elastic/eui';
|
import type { KibanaRequest } from 'src/core/server';
|
||||||
import { KibanaRequest } from 'src/core/server';
|
import type { TutorialSchema } from './tutorial_schema';
|
||||||
|
export type {
|
||||||
|
TutorialSchema,
|
||||||
|
ArtifactsSchema,
|
||||||
|
DashboardSchema,
|
||||||
|
InstructionsSchema,
|
||||||
|
ParamsSchema,
|
||||||
|
InstructionSetSchema,
|
||||||
|
InstructionVariant,
|
||||||
|
Instruction,
|
||||||
|
} from './tutorial_schema';
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export enum TutorialsCategory {
|
export enum TutorialsCategory {
|
||||||
|
@ -18,82 +28,6 @@ export enum TutorialsCategory {
|
||||||
}
|
}
|
||||||
export type Platform = 'WINDOWS' | 'OSX' | 'DEB' | 'RPM';
|
export type Platform = 'WINDOWS' | 'OSX' | 'DEB' | 'RPM';
|
||||||
|
|
||||||
export interface ParamTypes {
|
|
||||||
NUMBER: string;
|
|
||||||
STRING: string;
|
|
||||||
}
|
|
||||||
export interface Instruction {
|
|
||||||
title?: string;
|
|
||||||
textPre?: string;
|
|
||||||
commands?: string[];
|
|
||||||
textPost?: string;
|
|
||||||
}
|
|
||||||
export interface InstructionVariant {
|
|
||||||
id: string;
|
|
||||||
instructions: Instruction[];
|
|
||||||
}
|
|
||||||
export interface InstructionSetSchema {
|
|
||||||
readonly title?: string;
|
|
||||||
readonly callOut?: {
|
|
||||||
title: string;
|
|
||||||
message?: string;
|
|
||||||
iconType?: IconType;
|
|
||||||
};
|
|
||||||
instructionVariants: InstructionVariant[];
|
|
||||||
}
|
|
||||||
export interface ParamsSchema {
|
|
||||||
defaultValue: any;
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
type: ParamTypes;
|
|
||||||
}
|
|
||||||
export interface InstructionsSchema {
|
|
||||||
readonly instructionSets: InstructionSetSchema[];
|
|
||||||
readonly params?: ParamsSchema[];
|
|
||||||
}
|
|
||||||
export interface DashboardSchema {
|
|
||||||
id: string;
|
|
||||||
linkLabel?: string;
|
|
||||||
isOverview: boolean;
|
|
||||||
}
|
|
||||||
export interface ArtifactsSchema {
|
|
||||||
exportedFields?: {
|
|
||||||
documentationUrl: string;
|
|
||||||
};
|
|
||||||
dashboards: DashboardSchema[];
|
|
||||||
application?: {
|
|
||||||
path: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export interface TutorialSchema {
|
|
||||||
id: string;
|
|
||||||
category: TutorialsCategory;
|
|
||||||
name: string;
|
|
||||||
moduleName?: string;
|
|
||||||
isBeta?: boolean;
|
|
||||||
shortDescription: string;
|
|
||||||
euiIconType?: IconType; // EUI icon type string, one of https://elastic.github.io/eui/#/display/icons;
|
|
||||||
longDescription: string;
|
|
||||||
completionTimeMinutes?: number;
|
|
||||||
previewImagePath?: string;
|
|
||||||
|
|
||||||
// kibana and elastic cluster running on prem
|
|
||||||
onPrem: InstructionsSchema;
|
|
||||||
|
|
||||||
// kibana and elastic cluster running in elastic's cloud
|
|
||||||
elasticCloud?: InstructionsSchema;
|
|
||||||
|
|
||||||
// kibana running on prem and elastic cluster running in elastic's cloud
|
|
||||||
onPremElasticCloud?: InstructionsSchema;
|
|
||||||
|
|
||||||
// Elastic stack artifacts produced by product when it is setup and run.
|
|
||||||
artifacts?: ArtifactsSchema;
|
|
||||||
|
|
||||||
// saved objects used by data module.
|
|
||||||
savedObjects?: any[];
|
|
||||||
savedObjectsInstallMsg?: string;
|
|
||||||
}
|
|
||||||
export interface TutorialContext {
|
export interface TutorialContext {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ describe('TutorialsRegistry', () => {
|
||||||
const setup = new TutorialsRegistry().setup(mockCoreSetup);
|
const setup = new TutorialsRegistry().setup(mockCoreSetup);
|
||||||
testProvider = ({}) => invalidTutorialProvider;
|
testProvider = ({}) => invalidTutorialProvider;
|
||||||
expect(() => setup.registerTutorial(testProvider)).toThrowErrorMatchingInlineSnapshot(
|
expect(() => setup.registerTutorial(testProvider)).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"Unable to register tutorial spec because its invalid. ValidationError: child \\"name\\" fails because [\\"name\\" is not allowed to be empty]"`
|
`"Unable to register tutorial spec because its invalid. Error: [name]: is not allowed to be empty"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Joi from 'joi';
|
|
||||||
import { CoreSetup } from 'src/core/server';
|
import { CoreSetup } from 'src/core/server';
|
||||||
import {
|
import {
|
||||||
TutorialProvider,
|
TutorialProvider,
|
||||||
|
@ -42,10 +41,10 @@ export class TutorialsRegistry {
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
registerTutorial: (specProvider: TutorialProvider) => {
|
registerTutorial: (specProvider: TutorialProvider) => {
|
||||||
const emptyContext = {};
|
try {
|
||||||
const { error } = Joi.validate(specProvider(emptyContext), tutorialSchema);
|
const emptyContext = {};
|
||||||
|
tutorialSchema.validate(specProvider(emptyContext));
|
||||||
if (error) {
|
} catch (error) {
|
||||||
throw new Error(`Unable to register tutorial spec because its invalid. ${error}`);
|
throw new Error(`Unable to register tutorial spec because its invalid. ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { parse } from 'hjson';
|
import { parse } from 'hjson';
|
||||||
import { ElasticsearchClient, SavedObject } from 'src/core/server';
|
import type { ElasticsearchClient } from 'src/core/server';
|
||||||
|
|
||||||
import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types';
|
import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ const getDefaultVegaVisualizations = (home: UsageCollectorDependencies['home'])
|
||||||
const sampleDataSets = home?.sampleData.getSampleDatasets() ?? [];
|
const sampleDataSets = home?.sampleData.getSampleDatasets() ?? [];
|
||||||
|
|
||||||
sampleDataSets.forEach((sampleDataSet) =>
|
sampleDataSets.forEach((sampleDataSet) =>
|
||||||
sampleDataSet.savedObjects.forEach((savedObject: SavedObject<any>) => {
|
sampleDataSet.savedObjects.forEach((savedObject) => {
|
||||||
try {
|
try {
|
||||||
if (savedObject.type === 'visualization') {
|
if (savedObject.type === 'visualization') {
|
||||||
const visState = JSON.parse(savedObject.attributes?.visState);
|
const visState = JSON.parse(savedObject.attributes?.visState);
|
||||||
|
|
|
@ -108,6 +108,7 @@ export class APMPlugin
|
||||||
plugins.home?.tutorials.registerTutorial(() => {
|
plugins.home?.tutorials.registerTutorial(() => {
|
||||||
const ossPart = ossTutorialProvider({});
|
const ossPart = ossTutorialProvider({});
|
||||||
if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) {
|
if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) {
|
||||||
|
// @ts-expect-error ossPart.artifacts.application is readonly
|
||||||
ossPart.artifacts.application = {
|
ossPart.artifacts.application = {
|
||||||
path: '/app/apm',
|
path: '/app/apm',
|
||||||
label: i18n.translate(
|
label: i18n.translate(
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "" 1`] = `"[catalogue.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains space" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains space" 1`] = `"[catalogue.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains_invalid()_chars" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains_invalid()_chars" 1`] = `"[catalogue.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "" 1`] = `"[management.kibana.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains space" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains space" 1`] = `"[management.kibana.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"[management.kibana.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "catalogue" 1`] = `"[id]: [catalogue] is not allowed"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"[id]: Does not satisfy regexp /^[a-zA-Z0-9_-]+$/"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "management" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "management" 1`] = `"[id]: [management] is not allowed"`;
|
||||||
|
|
||||||
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "navLinks" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`;
|
exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "navLinks" 1`] = `"[id]: [navLinks] is not allowed"`;
|
||||||
|
|
|
@ -6,5 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// the file created to remove TS cicular dependency between features and security pluin
|
// the file created to remove TS cicular dependency between features and security pluin
|
||||||
|
// https://github.com/elastic/kibana/issues/87388
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||||
export { featurePrivilegeIterator } from '../../security/server/authorization';
|
export { featurePrivilegeIterator } from '../../security/server/authorization';
|
||||||
|
|
|
@ -158,7 +158,7 @@ describe('FeatureRegistry', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
featureRegistry.registerKibanaFeature(feature)
|
featureRegistry.registerKibanaFeature(feature)
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"child \\"category\\" fails because [\\"category\\" is required]"`
|
`"[category.id]: expected value of type [string] but got [undefined]"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ describe('FeatureRegistry', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
featureRegistry.registerKibanaFeature(feature)
|
featureRegistry.registerKibanaFeature(feature)
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"child \\"category\\" fails because [child \\"id\\" fails because [\\"id\\" is required]]"`
|
`"[category.id]: expected value of type [string] but got [undefined]"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -192,7 +192,7 @@ describe('FeatureRegistry', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
featureRegistry.registerKibanaFeature(feature)
|
featureRegistry.registerKibanaFeature(feature)
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"child \\"category\\" fails because [child \\"label\\" fails because [\\"label\\" is required]]"`
|
`"[category.label]: expected value of type [string] but got [undefined]"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -209,7 +209,7 @@ describe('FeatureRegistry', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
featureRegistry.registerKibanaFeature(feature)
|
featureRegistry.registerKibanaFeature(feature)
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"child \\"privileges\\" fails because [\\"privileges\\" is required]"`
|
`"[privileges]: expected at least one defined value but got [undefined]"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -248,7 +248,7 @@ describe('FeatureRegistry', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
featureRegistry.registerKibanaFeature(feature)
|
featureRegistry.registerKibanaFeature(feature)
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"child \\"subFeatures\\" fails because [\\"subFeatures\\" must contain less than or equal to 0 items]"`
|
`"[subFeatures]: array size is [1], but cannot be greater than [0]"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -488,11 +488,12 @@ describe('FeatureRegistry', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const featureRegistry = new FeatureRegistry();
|
const featureRegistry = new FeatureRegistry();
|
||||||
expect(() =>
|
expect(() => featureRegistry.registerKibanaFeature(feature))
|
||||||
featureRegistry.registerKibanaFeature(feature)
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
"[privileges]: types that failed validation:
|
||||||
`"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"`
|
- [privileges.0]: expected value to equal [null]
|
||||||
);
|
- [privileges.1.foo]: definition for this key is missing"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`prevents privileges from specifying app entries that don't exist at the root level`, () => {
|
it(`prevents privileges from specifying app entries that don't exist at the root level`, () => {
|
||||||
|
@ -1278,7 +1279,7 @@ describe('FeatureRegistry', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
featureRegistry.registerKibanaFeature(feature)
|
featureRegistry.registerKibanaFeature(feature)
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"child \\"reserved\\" fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"id\\" fails because [\\"id\\" with value \\"reserved_1\\" fails to match the required pattern: /^(?!reserved_)[a-zA-Z0-9_-]+$/]]]]"`
|
`"[reserved.privileges.0.id]: Does not satisfy regexp /^(?!reserved_)[a-zA-Z0-9_-]+$/"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1394,9 +1395,11 @@ describe('FeatureRegistry', () => {
|
||||||
const featureRegistry = new FeatureRegistry();
|
const featureRegistry = new FeatureRegistry();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
featureRegistry.registerKibanaFeature(feature1);
|
featureRegistry.registerKibanaFeature(feature1);
|
||||||
}).toThrowErrorMatchingInlineSnapshot(
|
}).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]]]]]]]"`
|
"[subFeatures.0.privilegeGroups.0]: types that failed validation:
|
||||||
);
|
- [subFeatures.0.privilegeGroups.0.0.privileges.0.minimumLicense]: a value wasn't expected to be present
|
||||||
|
- [subFeatures.0.privilegeGroups.0.1.groupType]: expected value to equal [independent]"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cannot register feature after getAll has been called', () => {
|
it('cannot register feature after getAll has been called', () => {
|
||||||
|
@ -1575,7 +1578,7 @@ describe('FeatureRegistry', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
featureRegistry.registerElasticsearchFeature(feature)
|
featureRegistry.registerElasticsearchFeature(feature)
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"child \\"privileges\\" fails because [\\"privileges\\" is required]"`
|
`"[privileges]: expected value of type [array] but got [undefined]"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Joi from 'joi';
|
import { schema } from '@kbn/config-schema';
|
||||||
|
|
||||||
import { difference } from 'lodash';
|
import { difference } from 'lodash';
|
||||||
import { Capabilities as UICapabilities } from '../../../../src/core/server';
|
import { Capabilities as UICapabilities } from '../../../../src/core/server';
|
||||||
|
@ -14,7 +14,11 @@ import { FeatureKibanaPrivileges, ElasticsearchFeatureConfig } from '.';
|
||||||
|
|
||||||
// Each feature gets its own property on the UICapabilities object,
|
// Each feature gets its own property on the UICapabilities object,
|
||||||
// but that object has a few built-in properties which should not be overwritten.
|
// but that object has a few built-in properties which should not be overwritten.
|
||||||
const prohibitedFeatureIds: Array<keyof UICapabilities> = ['catalogue', 'management', 'navLinks'];
|
const prohibitedFeatureIds: Set<keyof UICapabilities> = new Set([
|
||||||
|
'catalogue',
|
||||||
|
'management',
|
||||||
|
'navLinks',
|
||||||
|
]);
|
||||||
|
|
||||||
const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
||||||
const subFeaturePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
const subFeaturePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
||||||
|
@ -22,144 +26,211 @@ const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/;
|
||||||
const reservedFeaturePrrivilegePartRegex = /^(?!reserved_)[a-zA-Z0-9_-]+$/;
|
const reservedFeaturePrrivilegePartRegex = /^(?!reserved_)[a-zA-Z0-9_-]+$/;
|
||||||
export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/;
|
export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/;
|
||||||
|
|
||||||
const validLicenses = ['basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial'];
|
const validLicenseSchema = schema.oneOf([
|
||||||
|
schema.literal('basic'),
|
||||||
|
schema.literal('standard'),
|
||||||
|
schema.literal('gold'),
|
||||||
|
schema.literal('platinum'),
|
||||||
|
schema.literal('enterprise'),
|
||||||
|
schema.literal('trial'),
|
||||||
|
]);
|
||||||
// sub-feature privileges are only available with a `gold` license or better, so restricting sub-feature privileges
|
// 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.
|
// for `gold` or below doesn't make a whole lot of sense.
|
||||||
const validSubFeaturePrivilegeLicenses = ['platinum', 'enterprise', 'trial'];
|
const validSubFeaturePrivilegeLicensesSchema = schema.oneOf([
|
||||||
|
schema.literal('platinum'),
|
||||||
|
schema.literal('enterprise'),
|
||||||
|
schema.literal('trial'),
|
||||||
|
]);
|
||||||
|
|
||||||
const managementSchema = Joi.object().pattern(
|
const listOfCapabilitiesSchema = schema.arrayOf(
|
||||||
managementSectionIdRegex,
|
schema.string({
|
||||||
Joi.array().items(Joi.string().regex(uiCapabilitiesRegex))
|
validate(key: string) {
|
||||||
|
if (!uiCapabilitiesRegex.test(key)) {
|
||||||
|
return `Does not satisfy regexp ${uiCapabilitiesRegex.toString()}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
);
|
);
|
||||||
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
|
const managementSchema = schema.recordOf(
|
||||||
const alertingSchema = Joi.array().items(Joi.string());
|
schema.string({
|
||||||
|
validate(key: string) {
|
||||||
const appCategorySchema = Joi.object({
|
if (!managementSectionIdRegex.test(key)) {
|
||||||
id: Joi.string().required(),
|
return `Does not satisfy regexp ${managementSectionIdRegex.toString()}`;
|
||||||
label: Joi.string().required(),
|
}
|
||||||
ariaLabel: Joi.string(),
|
},
|
||||||
euiIconType: Joi.string(),
|
|
||||||
order: Joi.number(),
|
|
||||||
}).required();
|
|
||||||
|
|
||||||
const kibanaPrivilegeSchema = Joi.object({
|
|
||||||
excludeFromBasePrivileges: Joi.boolean(),
|
|
||||||
management: managementSchema,
|
|
||||||
catalogue: catalogueSchema,
|
|
||||||
api: Joi.array().items(Joi.string()),
|
|
||||||
app: Joi.array().items(Joi.string()),
|
|
||||||
alerting: Joi.object({
|
|
||||||
all: alertingSchema,
|
|
||||||
read: alertingSchema,
|
|
||||||
}),
|
}),
|
||||||
savedObject: Joi.object({
|
listOfCapabilitiesSchema
|
||||||
all: Joi.array().items(Joi.string()).required(),
|
);
|
||||||
read: Joi.array().items(Joi.string()).required(),
|
const catalogueSchema = listOfCapabilitiesSchema;
|
||||||
}).required(),
|
const alertingSchema = schema.arrayOf(schema.string());
|
||||||
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
|
|
||||||
|
const appCategorySchema = schema.object({
|
||||||
|
id: schema.string(),
|
||||||
|
label: schema.string(),
|
||||||
|
ariaLabel: schema.maybe(schema.string()),
|
||||||
|
euiIconType: schema.maybe(schema.string()),
|
||||||
|
order: schema.maybe(schema.number()),
|
||||||
});
|
});
|
||||||
|
|
||||||
const kibanaIndependentSubFeaturePrivilegeSchema = Joi.object({
|
const kibanaPrivilegeSchema = schema.object({
|
||||||
id: Joi.string().regex(subFeaturePrivilegePartRegex).required(),
|
excludeFromBasePrivileges: schema.maybe(schema.boolean()),
|
||||||
name: Joi.string().required(),
|
management: schema.maybe(managementSchema),
|
||||||
includeIn: Joi.string().allow('all', 'read', 'none').required(),
|
catalogue: schema.maybe(catalogueSchema),
|
||||||
minimumLicense: Joi.string().valid(...validSubFeaturePrivilegeLicenses),
|
api: schema.maybe(schema.arrayOf(schema.string())),
|
||||||
management: managementSchema,
|
app: schema.maybe(schema.arrayOf(schema.string())),
|
||||||
catalogue: catalogueSchema,
|
alerting: schema.maybe(
|
||||||
alerting: Joi.object({
|
schema.object({
|
||||||
all: alertingSchema,
|
all: schema.maybe(alertingSchema),
|
||||||
read: alertingSchema,
|
read: schema.maybe(alertingSchema),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
savedObject: schema.object({
|
||||||
|
all: schema.arrayOf(schema.string()),
|
||||||
|
read: schema.arrayOf(schema.string()),
|
||||||
}),
|
}),
|
||||||
api: Joi.array().items(Joi.string()),
|
ui: listOfCapabilitiesSchema,
|
||||||
app: Joi.array().items(Joi.string()),
|
|
||||||
savedObject: Joi.object({
|
|
||||||
all: Joi.array().items(Joi.string()).required(),
|
|
||||||
read: Joi.array().items(Joi.string()).required(),
|
|
||||||
}).required(),
|
|
||||||
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const kibanaMutuallyExclusiveSubFeaturePrivilegeSchema = kibanaIndependentSubFeaturePrivilegeSchema.keys(
|
const kibanaIndependentSubFeaturePrivilegeSchema = schema.object({
|
||||||
|
id: schema.string({
|
||||||
|
validate(key: string) {
|
||||||
|
if (!subFeaturePrivilegePartRegex.test(key)) {
|
||||||
|
return `Does not satisfy regexp ${subFeaturePrivilegePartRegex.toString()}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
name: schema.string(),
|
||||||
|
includeIn: schema.oneOf([schema.literal('all'), schema.literal('read'), schema.literal('none')]),
|
||||||
|
minimumLicense: schema.maybe(validSubFeaturePrivilegeLicensesSchema),
|
||||||
|
management: schema.maybe(managementSchema),
|
||||||
|
catalogue: schema.maybe(catalogueSchema),
|
||||||
|
alerting: schema.maybe(
|
||||||
|
schema.object({
|
||||||
|
all: schema.maybe(alertingSchema),
|
||||||
|
read: schema.maybe(alertingSchema),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
api: schema.maybe(schema.arrayOf(schema.string())),
|
||||||
|
app: schema.maybe(schema.arrayOf(schema.string())),
|
||||||
|
savedObject: schema.object({
|
||||||
|
all: schema.arrayOf(schema.string()),
|
||||||
|
read: schema.arrayOf(schema.string()),
|
||||||
|
}),
|
||||||
|
ui: listOfCapabilitiesSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const kibanaMutuallyExclusiveSubFeaturePrivilegeSchema = kibanaIndependentSubFeaturePrivilegeSchema.extends(
|
||||||
{
|
{
|
||||||
minimumLicense: Joi.forbidden(),
|
minimumLicense: schema.never(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const kibanaSubFeatureSchema = Joi.object({
|
const kibanaSubFeatureSchema = schema.object({
|
||||||
name: Joi.string().required(),
|
name: schema.string(),
|
||||||
privilegeGroups: Joi.array().items(
|
privilegeGroups: schema.maybe(
|
||||||
Joi.object({
|
schema.arrayOf(
|
||||||
groupType: Joi.string().valid('mutually_exclusive', 'independent').required(),
|
schema.oneOf([
|
||||||
privileges: Joi.when('groupType', {
|
schema.object({
|
||||||
is: 'mutually_exclusive',
|
groupType: schema.literal('mutually_exclusive'),
|
||||||
then: Joi.array().items(kibanaMutuallyExclusiveSubFeaturePrivilegeSchema).min(1),
|
privileges: schema.maybe(
|
||||||
otherwise: Joi.array().items(kibanaIndependentSubFeaturePrivilegeSchema).min(1),
|
schema.arrayOf(kibanaMutuallyExclusiveSubFeaturePrivilegeSchema, { minSize: 1 })
|
||||||
}),
|
),
|
||||||
|
}),
|
||||||
|
schema.object({
|
||||||
|
groupType: schema.literal('independent'),
|
||||||
|
privileges: schema.maybe(
|
||||||
|
schema.arrayOf(kibanaIndependentSubFeaturePrivilegeSchema, { minSize: 1 })
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const kibanaFeatureSchema = schema.object({
|
||||||
|
id: schema.string({
|
||||||
|
validate(value: string) {
|
||||||
|
if (!featurePrivilegePartRegex.test(value)) {
|
||||||
|
return `Does not satisfy regexp ${featurePrivilegePartRegex.toString()}`;
|
||||||
|
}
|
||||||
|
if (prohibitedFeatureIds.has(value)) {
|
||||||
|
return `[${value}] is not allowed`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
name: schema.string(),
|
||||||
|
category: appCategorySchema,
|
||||||
|
order: schema.maybe(schema.number()),
|
||||||
|
excludeFromBasePrivileges: schema.maybe(schema.boolean()),
|
||||||
|
minimumLicense: schema.maybe(validLicenseSchema),
|
||||||
|
app: schema.arrayOf(schema.string()),
|
||||||
|
management: schema.maybe(managementSchema),
|
||||||
|
catalogue: schema.maybe(catalogueSchema),
|
||||||
|
alerting: schema.maybe(alertingSchema),
|
||||||
|
privileges: schema.oneOf([
|
||||||
|
schema.literal(null),
|
||||||
|
schema.object({
|
||||||
|
all: schema.maybe(kibanaPrivilegeSchema),
|
||||||
|
read: schema.maybe(kibanaPrivilegeSchema),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
subFeatures: schema.maybe(
|
||||||
|
schema.conditional(
|
||||||
|
schema.siblingRef('privileges'),
|
||||||
|
null,
|
||||||
|
// allows an empty array only
|
||||||
|
schema.arrayOf(schema.never(), { maxSize: 0 }),
|
||||||
|
schema.arrayOf(kibanaSubFeatureSchema)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
privilegesTooltip: schema.maybe(schema.string()),
|
||||||
|
reserved: schema.maybe(
|
||||||
|
schema.object({
|
||||||
|
description: schema.string(),
|
||||||
|
privileges: schema.arrayOf(
|
||||||
|
schema.object({
|
||||||
|
id: schema.string({
|
||||||
|
validate(value: string) {
|
||||||
|
if (!reservedFeaturePrrivilegePartRegex.test(value)) {
|
||||||
|
return `Does not satisfy regexp ${reservedFeaturePrrivilegePartRegex.toString()}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
privilege: kibanaPrivilegeSchema,
|
||||||
|
})
|
||||||
|
),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const kibanaFeatureSchema = Joi.object({
|
const elasticsearchPrivilegeSchema = schema.object({
|
||||||
id: Joi.string()
|
ui: schema.arrayOf(schema.string()),
|
||||||
.regex(featurePrivilegePartRegex)
|
requiredClusterPrivileges: schema.maybe(schema.arrayOf(schema.string())),
|
||||||
.invalid(...prohibitedFeatureIds)
|
requiredIndexPrivileges: schema.maybe(
|
||||||
.required(),
|
schema.recordOf(schema.string(), schema.arrayOf(schema.string()))
|
||||||
name: Joi.string().required(),
|
),
|
||||||
category: appCategorySchema,
|
requiredRoles: schema.maybe(schema.arrayOf(schema.string())),
|
||||||
order: Joi.number(),
|
|
||||||
excludeFromBasePrivileges: Joi.boolean(),
|
|
||||||
minimumLicense: Joi.string().valid(...validLicenses),
|
|
||||||
app: Joi.array().items(Joi.string()).required(),
|
|
||||||
management: managementSchema,
|
|
||||||
catalogue: catalogueSchema,
|
|
||||||
alerting: alertingSchema,
|
|
||||||
privileges: Joi.object({
|
|
||||||
all: kibanaPrivilegeSchema,
|
|
||||||
read: kibanaPrivilegeSchema,
|
|
||||||
})
|
|
||||||
.allow(null)
|
|
||||||
.required(),
|
|
||||||
subFeatures: Joi.when('privileges', {
|
|
||||||
is: null,
|
|
||||||
then: Joi.array().items(kibanaSubFeatureSchema).max(0),
|
|
||||||
otherwise: Joi.array().items(kibanaSubFeatureSchema),
|
|
||||||
}),
|
|
||||||
privilegesTooltip: Joi.string(),
|
|
||||||
reserved: Joi.object({
|
|
||||||
description: Joi.string().required(),
|
|
||||||
privileges: Joi.array()
|
|
||||||
.items(
|
|
||||||
Joi.object({
|
|
||||||
id: Joi.string().regex(reservedFeaturePrrivilegePartRegex).required(),
|
|
||||||
privilege: kibanaPrivilegeSchema.required(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.required(),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const elasticsearchPrivilegeSchema = Joi.object({
|
const elasticsearchFeatureSchema = schema.object({
|
||||||
ui: Joi.array().items(Joi.string()).required(),
|
id: schema.string({
|
||||||
requiredClusterPrivileges: Joi.array().items(Joi.string()),
|
validate(value: string) {
|
||||||
requiredIndexPrivileges: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())),
|
if (!featurePrivilegePartRegex.test(value)) {
|
||||||
requiredRoles: Joi.array().items(Joi.string()),
|
return `Does not satisfy regexp ${featurePrivilegePartRegex.toString()}`;
|
||||||
});
|
}
|
||||||
|
if (prohibitedFeatureIds.has(value)) {
|
||||||
const elasticsearchFeatureSchema = Joi.object({
|
return `[${value}] is not allowed`;
|
||||||
id: Joi.string()
|
}
|
||||||
.regex(featurePrivilegePartRegex)
|
},
|
||||||
.invalid(...prohibitedFeatureIds)
|
}),
|
||||||
.required(),
|
management: schema.maybe(managementSchema),
|
||||||
management: managementSchema,
|
catalogue: schema.maybe(catalogueSchema),
|
||||||
catalogue: catalogueSchema,
|
privileges: schema.arrayOf(elasticsearchPrivilegeSchema),
|
||||||
privileges: Joi.array().items(elasticsearchPrivilegeSchema).required(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function validateKibanaFeature(feature: KibanaFeatureConfig) {
|
export function validateKibanaFeature(feature: KibanaFeatureConfig) {
|
||||||
const validateResult = Joi.validate(feature, kibanaFeatureSchema);
|
kibanaFeatureSchema.validate(feature);
|
||||||
if (validateResult.error) {
|
|
||||||
throw validateResult.error;
|
|
||||||
}
|
|
||||||
// the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid.
|
// the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid.
|
||||||
const { app = [], management = {}, catalogue = [], alerting = [] } = feature;
|
const { app = [], management = {}, catalogue = [], alerting = [] } = feature;
|
||||||
|
|
||||||
|
@ -343,10 +414,7 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) {
|
export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) {
|
||||||
const validateResult = Joi.validate(feature, elasticsearchFeatureSchema);
|
elasticsearchFeatureSchema.validate(feature);
|
||||||
if (validateResult.error) {
|
|
||||||
throw validateResult.error;
|
|
||||||
}
|
|
||||||
// the following validation can't be enforced by the Joi schema without a very convoluted and verbose definition
|
// the following validation can't be enforced by the Joi schema without a very convoluted and verbose definition
|
||||||
const { privileges } = feature;
|
const { privileges } = feature;
|
||||||
privileges.forEach((privilege, index) => {
|
privileges.forEach((privilege, index) => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue