mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -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 type {
|
||||
ParamTypes,
|
||||
InstructionSetSchema,
|
||||
ParamsSchema,
|
||||
InstructionsSchema,
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* 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 {
|
||||
NOT_INSTALLED = 'not_installed',
|
||||
|
@ -26,57 +27,4 @@ export enum EmbeddableTypes {
|
|||
SEARCH_EMBEDDABLE_TYPE = 'search',
|
||||
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;
|
||||
|
|
|
@ -5,22 +5,27 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import type { Writable } from '@kbn/utility-types';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import Joi from 'joi';
|
||||
|
||||
const dataIndexSchema = Joi.object({
|
||||
id: Joi.string()
|
||||
.regex(/^[a-zA-Z0-9-]+$/)
|
||||
.required(),
|
||||
const idRegExp = /^[a-zA-Z0-9-]+$/;
|
||||
const dataIndexSchema = schema.object({
|
||||
id: schema.string({
|
||||
validate(value: string) {
|
||||
if (!idRegExp.test(value)) {
|
||||
return `Does not satisfy regexp: ${idRegExp.toString()}`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// 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)
|
||||
fields: Joi.object().required(),
|
||||
fields: schema.recordOf(schema.string(), schema.any()),
|
||||
|
||||
// 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.
|
||||
// When data is installed, timestamps are converted to the present time.
|
||||
|
@ -28,37 +33,66 @@ const dataIndexSchema = Joi.object({
|
|||
// 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: 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
|
||||
// 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({
|
||||
path: Joi.string().required(),
|
||||
label: Joi.string().required(),
|
||||
icon: Joi.string().required(),
|
||||
});
|
||||
export type DataIndexSchema = TypeOf<typeof dataIndexSchema>;
|
||||
|
||||
export const sampleDataSchema = {
|
||||
id: Joi.string()
|
||||
.regex(/^[a-zA-Z0-9-]+$/)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string().required(),
|
||||
previewImagePath: Joi.string().required(),
|
||||
darkPreviewImagePath: Joi.string(),
|
||||
const appLinkSchema = schema.object({
|
||||
path: schema.string(),
|
||||
label: schema.string(),
|
||||
icon: schema.string(),
|
||||
});
|
||||
export type AppLinkSchema = TypeOf<typeof appLinkSchema>;
|
||||
|
||||
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
|
||||
overviewDashboard: Joi.string().required(),
|
||||
appLinks: Joi.array().items(appLinkSchema).default([]),
|
||||
overviewDashboard: schema.string(),
|
||||
appLinks: schema.arrayOf(appLinkSchema, { defaultValue: [] }),
|
||||
|
||||
// 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, ...)
|
||||
// Should provide a nice demo of Kibana's functionality with the sample data set
|
||||
savedObjects: Joi.array().items(Joi.object()).required(),
|
||||
dataIndices: Joi.array().items(dataIndexSchema).required(),
|
||||
};
|
||||
savedObjects: schema.arrayOf(
|
||||
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 { 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 { createIndexName } from '../lib/create_index_name';
|
||||
import {
|
||||
|
@ -148,8 +153,9 @@ export function createInstallRoute(
|
|||
|
||||
const client = getClient({ includedHiddenTypes });
|
||||
|
||||
const savedObjects = sampleDataset.savedObjects as SavedObjectsBulkCreateObject[];
|
||||
createResults = await client.bulkCreate(
|
||||
sampleDataset.savedObjects.map(({ version, ...savedObject }) => savedObject),
|
||||
savedObjects.map(({ version, ...savedObject }) => savedObject),
|
||||
{ overwrite: true }
|
||||
);
|
||||
} catch (err) {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
|
||||
import { SavedObject } from 'src/core/public';
|
||||
import {
|
||||
|
@ -55,11 +54,13 @@ export class SampleDataRegistry {
|
|||
|
||||
return {
|
||||
registerSampleDataset: (specProvider: SampleDatasetProvider) => {
|
||||
const { error, value } = Joi.validate(specProvider(), sampleDataSchema);
|
||||
|
||||
if (error) {
|
||||
let value: SampleDatasetSchema;
|
||||
try {
|
||||
value = sampleDataSchema.validate(specProvider());
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to register sample dataset spec because it's invalid. ${error}`);
|
||||
}
|
||||
|
||||
const defaultIndexSavedObjectJson = value.savedObjects.find((savedObjectJson: any) => {
|
||||
return (
|
||||
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 type {
|
||||
ParamTypes,
|
||||
InstructionSetSchema,
|
||||
ParamsSchema,
|
||||
InstructionsSchema,
|
||||
|
|
|
@ -5,121 +5,153 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import Joi from 'joi';
|
||||
|
||||
const PARAM_TYPES = {
|
||||
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(),
|
||||
}),
|
||||
const dashboardSchema = schema.object({
|
||||
// Dashboard saved object id
|
||||
id: schema.string(),
|
||||
// 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.
|
||||
exportedFields: Joi.object({
|
||||
documentationUrl: Joi.string().required(),
|
||||
}),
|
||||
exportedFields: schema.maybe(
|
||||
schema.object({
|
||||
documentationUrl: schema.string(),
|
||||
})
|
||||
),
|
||||
// Kibana dashboards created by this product.
|
||||
dashboards: Joi.array().items(dashboardSchema).required(),
|
||||
application: Joi.object({
|
||||
path: Joi.string().required(),
|
||||
label: Joi.string().required(),
|
||||
dashboards: schema.arrayOf(dashboardSchema),
|
||||
application: schema.maybe(
|
||||
schema.object({
|
||||
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({
|
||||
title: Joi.string(),
|
||||
text: Joi.string(),
|
||||
btnLabel: Joi.string(),
|
||||
success: Joi.string(),
|
||||
error: Joi.string(),
|
||||
esHitsCheck: Joi.object({
|
||||
index: Joi.alternatives().try(Joi.string(), Joi.array().items(Joi.string())).required(),
|
||||
query: Joi.object().required(),
|
||||
}).required(),
|
||||
const instructionSchema = schema.object({
|
||||
title: schema.maybe(schema.string()),
|
||||
textPre: schema.maybe(schema.string()),
|
||||
commands: schema.maybe(schema.arrayOf(schema.string())),
|
||||
textPost: schema.maybe(schema.string()),
|
||||
});
|
||||
export type Instruction = TypeOf<typeof instructionSchema>;
|
||||
|
||||
const instructionVariantSchema = schema.object({
|
||||
id: schema.string(),
|
||||
instructions: schema.arrayOf(instructionSchema),
|
||||
});
|
||||
|
||||
const instructionSchema = Joi.object({
|
||||
title: Joi.string(),
|
||||
textPre: Joi.string(),
|
||||
commands: Joi.array().items(Joi.string().allow('')),
|
||||
textPost: Joi.string(),
|
||||
});
|
||||
export type InstructionVariant = TypeOf<typeof instructionVariantSchema>;
|
||||
|
||||
const instructionVariantSchema = Joi.object({
|
||||
id: Joi.string().required(),
|
||||
instructions: Joi.array().items(instructionSchema).required(),
|
||||
});
|
||||
|
||||
const instructionSetSchema = Joi.object({
|
||||
title: Joi.string(),
|
||||
callOut: Joi.object({
|
||||
title: Joi.string().required(),
|
||||
message: Joi.string(),
|
||||
iconType: Joi.string(),
|
||||
}),
|
||||
const instructionSetSchema = schema.object({
|
||||
title: schema.maybe(schema.string()),
|
||||
callOut: schema.maybe(
|
||||
schema.object({
|
||||
title: schema.string(),
|
||||
message: schema.maybe(schema.string()),
|
||||
iconType: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
// Variants (OSes, languages, etc.) for which tutorial instructions are specified.
|
||||
instructionVariants: Joi.array().items(instructionVariantSchema).required(),
|
||||
statusCheck: statusCheckSchema,
|
||||
instructionVariants: schema.arrayOf(instructionVariantSchema),
|
||||
statusCheck: schema.maybe(statusCheckSchema),
|
||||
});
|
||||
export type InstructionSetSchema = TypeOf<typeof instructionSetSchema>;
|
||||
|
||||
const paramSchema = Joi.object({
|
||||
defaultValue: Joi.required(),
|
||||
id: Joi.string()
|
||||
.regex(/^[a-zA-Z_]+$/)
|
||||
.required(),
|
||||
label: Joi.string().required(),
|
||||
type: Joi.string().valid(Object.values(PARAM_TYPES)).required(),
|
||||
const idRegExp = /^[a-zA-Z_]+$/;
|
||||
const paramSchema = schema.object({
|
||||
defaultValue: schema.any(),
|
||||
id: schema.string({
|
||||
validate(value: string) {
|
||||
if (!idRegExp.test(value)) {
|
||||
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({
|
||||
instructionSets: Joi.array().items(instructionSetSchema).required(),
|
||||
params: Joi.array().items(paramSchema),
|
||||
const instructionsSchema = schema.object({
|
||||
instructionSets: schema.arrayOf(instructionSetSchema),
|
||||
params: schema.maybe(schema.arrayOf(paramSchema)),
|
||||
});
|
||||
export type InstructionsSchema = TypeOf<typeof instructionSchema>;
|
||||
|
||||
export const tutorialSchema = {
|
||||
id: Joi.string()
|
||||
.regex(/^[a-zA-Z0-9-]+$/)
|
||||
.required(),
|
||||
category: Joi.string().valid(Object.values(TUTORIAL_CATEGORY)).required(),
|
||||
name: Joi.string().required(),
|
||||
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
|
||||
longDescription: Joi.string().required(),
|
||||
completionTimeMinutes: Joi.number().integer(),
|
||||
previewImagePath: Joi.string(),
|
||||
|
||||
const tutorialIdRegExp = /^[a-zA-Z0-9-]+$/;
|
||||
export const tutorialSchema = schema.object({
|
||||
id: schema.string({
|
||||
validate(value: string) {
|
||||
if (!tutorialIdRegExp.test(value)) {
|
||||
return `Does not satisfy regexp ${tutorialIdRegExp.toString()}`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
category: schema.oneOf([
|
||||
schema.literal('logging'),
|
||||
schema.literal('security'),
|
||||
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
|
||||
onPrem: instructionsSchema.required(),
|
||||
|
||||
onPrem: instructionsSchema,
|
||||
// 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
|
||||
onPremElasticCloud: instructionsSchema,
|
||||
|
||||
onPremElasticCloud: schema.maybe(instructionsSchema),
|
||||
// Elastic stack artifacts produced by product when it is setup and run.
|
||||
artifacts: artifactsSchema,
|
||||
artifacts: schema.maybe(artifactsSchema),
|
||||
|
||||
// saved objects used by data module.
|
||||
savedObjects: Joi.array().items(),
|
||||
savedObjectsInstallMsg: Joi.string(),
|
||||
};
|
||||
savedObjects: schema.maybe(schema.arrayOf(schema.any())),
|
||||
savedObjectsInstallMsg: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export type TutorialSchema = TypeOf<typeof tutorialSchema>;
|
||||
|
|
|
@ -6,8 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import type { 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 */
|
||||
export enum TutorialsCategory {
|
||||
|
@ -18,82 +28,6 @@ export enum TutorialsCategory {
|
|||
}
|
||||
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 {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ describe('TutorialsRegistry', () => {
|
|||
const setup = new TutorialsRegistry().setup(mockCoreSetup);
|
||||
testProvider = ({}) => invalidTutorialProvider;
|
||||
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.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { CoreSetup } from 'src/core/server';
|
||||
import {
|
||||
TutorialProvider,
|
||||
|
@ -42,10 +41,10 @@ export class TutorialsRegistry {
|
|||
);
|
||||
return {
|
||||
registerTutorial: (specProvider: TutorialProvider) => {
|
||||
const emptyContext = {};
|
||||
const { error } = Joi.validate(specProvider(emptyContext), tutorialSchema);
|
||||
|
||||
if (error) {
|
||||
try {
|
||||
const emptyContext = {};
|
||||
tutorialSchema.validate(specProvider(emptyContext));
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to register tutorial spec because its invalid. ${error}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { parse } from 'hjson';
|
||||
import { ElasticsearchClient, SavedObject } from 'src/core/server';
|
||||
import type { ElasticsearchClient } from 'src/core/server';
|
||||
|
||||
import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types';
|
||||
|
||||
|
@ -27,7 +27,7 @@ const getDefaultVegaVisualizations = (home: UsageCollectorDependencies['home'])
|
|||
const sampleDataSets = home?.sampleData.getSampleDatasets() ?? [];
|
||||
|
||||
sampleDataSets.forEach((sampleDataSet) =>
|
||||
sampleDataSet.savedObjects.forEach((savedObject: SavedObject<any>) => {
|
||||
sampleDataSet.savedObjects.forEach((savedObject) => {
|
||||
try {
|
||||
if (savedObject.type === 'visualization') {
|
||||
const visState = JSON.parse(savedObject.attributes?.visState);
|
||||
|
|
|
@ -108,6 +108,7 @@ export class APMPlugin
|
|||
plugins.home?.tutorials.registerTutorial(() => {
|
||||
const ossPart = ossTutorialProvider({});
|
||||
if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) {
|
||||
// @ts-expect-error ossPart.artifacts.application is readonly
|
||||
ossPart.artifacts.application = {
|
||||
path: '/app/apm',
|
||||
label: i18n.translate(
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
// 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
|
||||
// https://github.com/elastic/kibana/issues/87388
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
export { featurePrivilegeIterator } from '../../security/server/authorization';
|
||||
|
|
|
@ -158,7 +158,7 @@ describe('FeatureRegistry', () => {
|
|||
expect(() =>
|
||||
featureRegistry.registerKibanaFeature(feature)
|
||||
).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(() =>
|
||||
featureRegistry.registerKibanaFeature(feature)
|
||||
).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(() =>
|
||||
featureRegistry.registerKibanaFeature(feature)
|
||||
).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(() =>
|
||||
featureRegistry.registerKibanaFeature(feature)
|
||||
).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(() =>
|
||||
featureRegistry.registerKibanaFeature(feature)
|
||||
).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();
|
||||
expect(() =>
|
||||
featureRegistry.registerKibanaFeature(feature)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"`
|
||||
);
|
||||
expect(() => featureRegistry.registerKibanaFeature(feature))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"[privileges]: types that failed validation:
|
||||
- [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`, () => {
|
||||
|
@ -1278,7 +1279,7 @@ describe('FeatureRegistry', () => {
|
|||
expect(() =>
|
||||
featureRegistry.registerKibanaFeature(feature)
|
||||
).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();
|
||||
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]]]]]]]"`
|
||||
);
|
||||
}).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[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', () => {
|
||||
|
@ -1575,7 +1578,7 @@ describe('FeatureRegistry', () => {
|
|||
expect(() =>
|
||||
featureRegistry.registerElasticsearchFeature(feature)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"child \\"privileges\\" fails because [\\"privileges\\" is required]"`
|
||||
`"[privileges]: expected value of type [array] but got [undefined]"`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { difference } from 'lodash';
|
||||
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,
|
||||
// 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 subFeaturePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
|
@ -22,144 +26,211 @@ 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'];
|
||||
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
|
||||
// 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(
|
||||
managementSectionIdRegex,
|
||||
Joi.array().items(Joi.string().regex(uiCapabilitiesRegex))
|
||||
const listOfCapabilitiesSchema = schema.arrayOf(
|
||||
schema.string({
|
||||
validate(key: string) {
|
||||
if (!uiCapabilitiesRegex.test(key)) {
|
||||
return `Does not satisfy regexp ${uiCapabilitiesRegex.toString()}`;
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
|
||||
const alertingSchema = Joi.array().items(Joi.string());
|
||||
|
||||
const appCategorySchema = Joi.object({
|
||||
id: Joi.string().required(),
|
||||
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,
|
||||
const managementSchema = schema.recordOf(
|
||||
schema.string({
|
||||
validate(key: string) {
|
||||
if (!managementSectionIdRegex.test(key)) {
|
||||
return `Does not satisfy regexp ${managementSectionIdRegex.toString()}`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
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(),
|
||||
listOfCapabilitiesSchema
|
||||
);
|
||||
const catalogueSchema = listOfCapabilitiesSchema;
|
||||
const alertingSchema = schema.arrayOf(schema.string());
|
||||
|
||||
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({
|
||||
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({
|
||||
all: alertingSchema,
|
||||
read: alertingSchema,
|
||||
const kibanaPrivilegeSchema = schema.object({
|
||||
excludeFromBasePrivileges: schema.maybe(schema.boolean()),
|
||||
management: schema.maybe(managementSchema),
|
||||
catalogue: schema.maybe(catalogueSchema),
|
||||
api: schema.maybe(schema.arrayOf(schema.string())),
|
||||
app: schema.maybe(schema.arrayOf(schema.string())),
|
||||
alerting: schema.maybe(
|
||||
schema.object({
|
||||
all: schema.maybe(alertingSchema),
|
||||
read: schema.maybe(alertingSchema),
|
||||
})
|
||||
),
|
||||
savedObject: schema.object({
|
||||
all: schema.arrayOf(schema.string()),
|
||||
read: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
api: Joi.array().items(Joi.string()),
|
||||
app: Joi.array().items(Joi.string()),
|
||||
savedObject: Joi.object({
|
||||
all: Joi.array().items(Joi.string()).required(),
|
||||
read: Joi.array().items(Joi.string()).required(),
|
||||
}).required(),
|
||||
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
|
||||
ui: listOfCapabilitiesSchema,
|
||||
});
|
||||
|
||||
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({
|
||||
name: Joi.string().required(),
|
||||
privilegeGroups: Joi.array().items(
|
||||
Joi.object({
|
||||
groupType: Joi.string().valid('mutually_exclusive', 'independent').required(),
|
||||
privileges: Joi.when('groupType', {
|
||||
is: 'mutually_exclusive',
|
||||
then: Joi.array().items(kibanaMutuallyExclusiveSubFeaturePrivilegeSchema).min(1),
|
||||
otherwise: Joi.array().items(kibanaIndependentSubFeaturePrivilegeSchema).min(1),
|
||||
}),
|
||||
const kibanaSubFeatureSchema = schema.object({
|
||||
name: schema.string(),
|
||||
privilegeGroups: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.object({
|
||||
groupType: schema.literal('mutually_exclusive'),
|
||||
privileges: schema.maybe(
|
||||
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({
|
||||
id: Joi.string()
|
||||
.regex(featurePrivilegePartRegex)
|
||||
.invalid(...prohibitedFeatureIds)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
category: appCategorySchema,
|
||||
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 = schema.object({
|
||||
ui: schema.arrayOf(schema.string()),
|
||||
requiredClusterPrivileges: schema.maybe(schema.arrayOf(schema.string())),
|
||||
requiredIndexPrivileges: schema.maybe(
|
||||
schema.recordOf(schema.string(), schema.arrayOf(schema.string()))
|
||||
),
|
||||
requiredRoles: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
||||
const elasticsearchPrivilegeSchema = Joi.object({
|
||||
ui: Joi.array().items(Joi.string()).required(),
|
||||
requiredClusterPrivileges: Joi.array().items(Joi.string()),
|
||||
requiredIndexPrivileges: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())),
|
||||
requiredRoles: Joi.array().items(Joi.string()),
|
||||
});
|
||||
|
||||
const elasticsearchFeatureSchema = Joi.object({
|
||||
id: Joi.string()
|
||||
.regex(featurePrivilegePartRegex)
|
||||
.invalid(...prohibitedFeatureIds)
|
||||
.required(),
|
||||
management: managementSchema,
|
||||
catalogue: catalogueSchema,
|
||||
privileges: Joi.array().items(elasticsearchPrivilegeSchema).required(),
|
||||
const elasticsearchFeatureSchema = 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`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
management: schema.maybe(managementSchema),
|
||||
catalogue: schema.maybe(catalogueSchema),
|
||||
privileges: schema.arrayOf(elasticsearchPrivilegeSchema),
|
||||
});
|
||||
|
||||
export function validateKibanaFeature(feature: KibanaFeatureConfig) {
|
||||
const validateResult = Joi.validate(feature, kibanaFeatureSchema);
|
||||
if (validateResult.error) {
|
||||
throw validateResult.error;
|
||||
}
|
||||
kibanaFeatureSchema.validate(feature);
|
||||
|
||||
// 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;
|
||||
|
||||
|
@ -343,10 +414,7 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) {
|
|||
}
|
||||
|
||||
export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) {
|
||||
const validateResult = Joi.validate(feature, elasticsearchFeatureSchema);
|
||||
if (validateResult.error) {
|
||||
throw validateResult.error;
|
||||
}
|
||||
elasticsearchFeatureSchema.validate(feature);
|
||||
// the following validation can't be enforced by the Joi schema without a very convoluted and verbose definition
|
||||
const { privileges } = feature;
|
||||
privileges.forEach((privilege, index) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue