mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
parent
e5f46b88f7
commit
30f69df238
122 changed files with 2759 additions and 2347 deletions
|
@ -35,7 +35,7 @@
|
|||
"xpack.security": "legacy/plugins/security",
|
||||
"xpack.server": "legacy/server",
|
||||
"xpack.snapshotRestore": "legacy/plugins/snapshot_restore",
|
||||
"xpack.spaces": "legacy/plugins/spaces",
|
||||
"xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"],
|
||||
"xpack.transform": "legacy/plugins/transform",
|
||||
"xpack.upgradeAssistant": "legacy/plugins/upgrade_assistant",
|
||||
"xpack.uptime": "legacy/plugins/uptime",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import Hapi from 'hapi';
|
||||
import { EncryptedSavedObjectsStartContract } from '../shim';
|
||||
import { SpacesPlugin as SpacesPluginStartContract } from '../../../spaces';
|
||||
import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../../spaces';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
|
||||
import {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ActionsConfigType } from './types';
|
|||
import { TaskManager } from '../../task_manager';
|
||||
import { XPackMainPlugin } from '../../xpack_main/xpack_main';
|
||||
import KbnServer from '../../../../../src/legacy/server/kbn_server';
|
||||
import { SpacesPlugin as SpacesPluginStartContract } from '../../spaces';
|
||||
import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
|
||||
import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server';
|
||||
import {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import Hapi from 'hapi';
|
||||
import { Legacy } from 'kibana';
|
||||
import { SpacesPlugin as SpacesPluginStartContract } from '../../spaces';
|
||||
import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces';
|
||||
import { TaskManager } from '../../task_manager';
|
||||
import { XPackMainPlugin } from '../../xpack_main/xpack_main';
|
||||
import KbnServer from '../../../../../src/legacy/server/kbn_server';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
import { SpacesPlugin } from '../../../spaces';
|
||||
import { LegacySpacesPlugin } from '../../../spaces';
|
||||
import { Space } from '../../../spaces/common/model/space';
|
||||
|
||||
interface GetActiveSpaceResponse {
|
||||
|
@ -13,7 +13,7 @@ interface GetActiveSpaceResponse {
|
|||
space?: Space;
|
||||
}
|
||||
|
||||
export function spacesUtilsProvider(spacesPlugin: SpacesPlugin, request: Request) {
|
||||
export function spacesUtilsProvider(spacesPlugin: LegacySpacesPlugin, request: Request) {
|
||||
async function activeSpace(): Promise<GetActiveSpaceResponse> {
|
||||
try {
|
||||
return {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesPlugin } from '../../../../spaces';
|
||||
import { LegacySpacesPlugin } from '../../../../spaces';
|
||||
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
|
||||
import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically';
|
||||
|
||||
|
@ -23,7 +23,7 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => {
|
|||
getBasePath: jest.fn(),
|
||||
getScopedSpacesClient: jest.fn(),
|
||||
getActiveSpace: jest.fn(),
|
||||
} as OptionalPlugin<SpacesPlugin>;
|
||||
} as OptionalPlugin<LegacySpacesPlugin>;
|
||||
const request = Symbol();
|
||||
const privilegeOrPrivileges = ['foo', 'bar'];
|
||||
const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory(
|
||||
|
@ -45,7 +45,7 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => {
|
|||
const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges);
|
||||
const mockSpaces = {
|
||||
isEnabled: false,
|
||||
} as OptionalPlugin<SpacesPlugin>;
|
||||
} as OptionalPlugin<LegacySpacesPlugin>;
|
||||
const request = Symbol();
|
||||
const privilegeOrPrivileges = ['foo', 'bar'];
|
||||
const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory(
|
||||
|
|
|
@ -13,7 +13,7 @@ import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from '.
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesPlugin } from '../../../../spaces';
|
||||
import { LegacySpacesPlugin } from '../../../../spaces';
|
||||
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
|
||||
|
||||
export type CheckPrivilegesDynamically = (
|
||||
|
@ -26,7 +26,7 @@ export type CheckPrivilegesDynamicallyWithRequest = (
|
|||
|
||||
export function checkPrivilegesDynamicallyWithRequestFactory(
|
||||
checkPrivilegesWithRequest: CheckPrivilegesWithRequest,
|
||||
spaces: OptionalPlugin<SpacesPlugin>
|
||||
spaces: OptionalPlugin<LegacySpacesPlugin>
|
||||
): CheckPrivilegesDynamicallyWithRequest {
|
||||
return function checkPrivilegesDynamicallyWithRequest(request: Legacy.Request) {
|
||||
const checkPrivileges = checkPrivilegesWithRequest(request);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesPlugin } from '../../../../spaces';
|
||||
import { LegacySpacesPlugin } from '../../../../spaces';
|
||||
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
|
||||
import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges';
|
||||
|
||||
|
@ -19,7 +19,7 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => {
|
|||
const mockSpaces = ({
|
||||
isEnabled: true,
|
||||
namespaceToSpaceId: jest.fn().mockReturnValue(spaceId),
|
||||
} as unknown) as OptionalPlugin<SpacesPlugin>;
|
||||
} as unknown) as OptionalPlugin<LegacySpacesPlugin>;
|
||||
const request = Symbol();
|
||||
|
||||
const privilegeOrPrivileges = ['foo', 'bar'];
|
||||
|
@ -50,7 +50,7 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => {
|
|||
namespaceToSpaceId: jest.fn().mockImplementation(() => {
|
||||
throw new Error('should not be called');
|
||||
}),
|
||||
} as unknown) as OptionalPlugin<SpacesPlugin>;
|
||||
} as unknown) as OptionalPlugin<LegacySpacesPlugin>;
|
||||
|
||||
const request = Symbol();
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { SpacesPlugin } from '../../../../spaces';
|
||||
import { LegacySpacesPlugin } from '../../../../spaces';
|
||||
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
|
||||
import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges';
|
||||
|
||||
|
@ -19,7 +19,7 @@ export type CheckSavedObjectsPrivileges = (
|
|||
|
||||
export const checkSavedObjectsPrivilegesWithRequestFactory = (
|
||||
checkPrivilegesWithRequest: CheckPrivilegesWithRequest,
|
||||
spaces: OptionalPlugin<SpacesPlugin>
|
||||
spaces: OptionalPlugin<LegacySpacesPlugin>
|
||||
): CheckSavedObjectsPrivilegesWithRequest => {
|
||||
return function checkSavedObjectsPrivilegesWithRequest(request: Legacy.Request) {
|
||||
return async function checkSavedObjectsPrivileges(
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Server } from 'hapi';
|
||||
|
||||
import { getClient } from '../../../../../server/lib/get_client_shield';
|
||||
import { SpacesPlugin } from '../../../../spaces';
|
||||
import { LegacySpacesPlugin } from '../../../../spaces';
|
||||
import { XPackFeature, XPackMainPlugin } from '../../../../xpack_main/xpack_main';
|
||||
import { APPLICATION_PREFIX } from '../../../common/constants';
|
||||
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
|
||||
|
@ -38,7 +38,7 @@ export function createAuthorizationService(
|
|||
server: Server,
|
||||
securityXPackFeature: XPackFeature,
|
||||
xpackMainPlugin: XPackMainPlugin,
|
||||
spaces: OptionalPlugin<SpacesPlugin>
|
||||
spaces: OptionalPlugin<LegacySpacesPlugin>
|
||||
): AuthorizationService {
|
||||
const shieldClient = getClient(server);
|
||||
const config = server.config();
|
||||
|
|
|
@ -6,5 +6,3 @@
|
|||
|
||||
export { isReservedSpace } from './is_reserved_space';
|
||||
export { MAX_SPACE_INITIALS } from './constants';
|
||||
|
||||
export { getSpaceIdFromPath, addSpaceIdToPath } from './lib/spaces_url_parser';
|
||||
|
|
|
@ -4,29 +4,29 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { resolve } from 'path';
|
||||
import KbnServer, { Server } from 'src/legacy/server/kbn_server';
|
||||
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
|
||||
import { Legacy } from 'kibana';
|
||||
import { KibanaRequest } from '../../../../src/core/server';
|
||||
import { SpacesServiceSetup } from '../../../plugins/spaces/server/spaces_service/spaces_service';
|
||||
import { SpacesPluginSetup } from '../../../plugins/spaces/server';
|
||||
import { createOptionalPlugin } from '../../server/lib/optional_plugin';
|
||||
// @ts-ignore
|
||||
import { AuditLogger } from '../../server/lib/audit_logger';
|
||||
import mappings from './mappings.json';
|
||||
import { wrapError } from './server/lib/errors';
|
||||
import { getActiveSpace } from './server/lib/get_active_space';
|
||||
import { migrateToKibana660 } from './server/lib/migrations';
|
||||
import { plugin } from './server/new_platform';
|
||||
import { SecurityPlugin } from '../security';
|
||||
import { SpacesServiceSetup } from './server/new_platform/spaces_service/spaces_service';
|
||||
// @ts-ignore
|
||||
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
|
||||
import { initSpaceSelectorView, initEnterSpaceView } from './server/routes/views';
|
||||
|
||||
export interface SpacesPlugin {
|
||||
getSpaceId: SpacesServiceSetup['getSpaceId'];
|
||||
getActiveSpace: SpacesServiceSetup['getActiveSpace'];
|
||||
export interface LegacySpacesPlugin {
|
||||
getSpaceId: (request: Legacy.Request) => ReturnType<SpacesServiceSetup['getSpaceId']>;
|
||||
getActiveSpace: (request: Legacy.Request) => ReturnType<SpacesServiceSetup['getActiveSpace']>;
|
||||
spaceIdToNamespace: SpacesServiceSetup['spaceIdToNamespace'];
|
||||
namespaceToSpaceId: SpacesServiceSetup['namespaceToSpaceId'];
|
||||
getBasePath: SpacesServiceSetup['getBasePath'];
|
||||
getScopedSpacesClient: SpacesServiceSetup['scopedClient'];
|
||||
}
|
||||
|
||||
export const spaces = (kibana: Record<string, any>) =>
|
||||
|
@ -36,13 +36,6 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
|
||||
config(Joi: any) {
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
maxSpaces: Joi.number().default(1000),
|
||||
}).default();
|
||||
},
|
||||
|
||||
uiCapabilities() {
|
||||
return {
|
||||
spaces: {
|
||||
|
@ -92,18 +85,20 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
},
|
||||
async replaceInjectedVars(
|
||||
vars: Record<string, any>,
|
||||
request: Record<string, any>,
|
||||
request: Legacy.Request,
|
||||
server: Record<string, any>
|
||||
) {
|
||||
const spacesClient = await server.plugins.spaces.getScopedSpacesClient(request);
|
||||
const spacesPlugin = server.newPlatform.setup.plugins.spaces as SpacesPluginSetup;
|
||||
if (!spacesPlugin) {
|
||||
throw new Error('New Platform XPack Spaces plugin is not available.');
|
||||
}
|
||||
const kibanaRequest = KibanaRequest.from(request);
|
||||
const spaceId = spacesPlugin.spacesService.getSpaceId(kibanaRequest);
|
||||
const spacesClient = await spacesPlugin.spacesService.scopedClient(kibanaRequest);
|
||||
try {
|
||||
vars.activeSpace = {
|
||||
valid: true,
|
||||
space: await getActiveSpace(
|
||||
spacesClient,
|
||||
request.getBasePath(),
|
||||
server.config().get('server.basePath')
|
||||
),
|
||||
space: await spacesClient.get(spaceId),
|
||||
};
|
||||
} catch (e) {
|
||||
vars.activeSpace = {
|
||||
|
@ -118,52 +113,18 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
|
||||
async init(server: Server) {
|
||||
const kbnServer = (server as unknown) as KbnServer;
|
||||
const initializerContext = {
|
||||
config: {
|
||||
create: () => {
|
||||
return Rx.of({
|
||||
maxSpaces: server.config().get('xpack.spaces.maxSpaces'),
|
||||
});
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
get(...contextParts: string[]) {
|
||||
return kbnServer.newPlatform.coreContext.logger.get(
|
||||
'plugins',
|
||||
'spaces',
|
||||
...contextParts
|
||||
);
|
||||
},
|
||||
},
|
||||
} as PluginInitializerContext;
|
||||
|
||||
const core = (kbnServer.newPlatform.setup.core as unknown) as CoreSetup;
|
||||
|
||||
const plugins = {
|
||||
xpackMain: server.plugins.xpack_main,
|
||||
// TODO: Spaces has a circular dependency with Security right now.
|
||||
// Security is not yet available when init runs, so this is wrapped in an optional function for the time being.
|
||||
security: createOptionalPlugin<SecurityPlugin>(
|
||||
server.config(),
|
||||
'xpack.security',
|
||||
server.plugins,
|
||||
'security'
|
||||
),
|
||||
spaces: this,
|
||||
};
|
||||
|
||||
const { spacesService, registerLegacyAPI } = await plugin(initializerContext).setup(
|
||||
core,
|
||||
plugins
|
||||
);
|
||||
const spacesPlugin = kbnServer.newPlatform.setup.plugins.spaces as SpacesPluginSetup;
|
||||
if (!spacesPlugin) {
|
||||
throw new Error('New Platform XPack Spaces plugin is not available.');
|
||||
}
|
||||
|
||||
const config = server.config();
|
||||
|
||||
const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat;
|
||||
|
||||
registerLegacyAPI({
|
||||
router: server.route.bind(server),
|
||||
legacyConfig: {
|
||||
serverBasePath: config.get('server.basePath'),
|
||||
serverDefaultRoute: config.get('server.defaultRoute'),
|
||||
kibanaIndex: config.get('kibana.index'),
|
||||
},
|
||||
savedObjects: server.savedObjects,
|
||||
|
@ -178,16 +139,30 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
create: (pluginId: string) =>
|
||||
new AuditLogger(server, pluginId, server.config(), server.plugins.xpack_main.info),
|
||||
},
|
||||
security: createOptionalPlugin<SecurityPlugin>(
|
||||
server.config(),
|
||||
'xpack.security',
|
||||
server.plugins,
|
||||
'security'
|
||||
),
|
||||
xpackMain: server.plugins.xpack_main,
|
||||
});
|
||||
|
||||
initEnterSpaceView(server);
|
||||
initSpaceSelectorView(server);
|
||||
|
||||
server.expose('getSpaceId', (request: any) => spacesService.getSpaceId(request));
|
||||
server.expose('getActiveSpace', spacesService.getActiveSpace);
|
||||
server.expose('spaceIdToNamespace', spacesService.spaceIdToNamespace);
|
||||
server.expose('namespaceToSpaceId', spacesService.namespaceToSpaceId);
|
||||
server.expose('getBasePath', spacesService.getBasePath);
|
||||
server.expose('getScopedSpacesClient', spacesService.scopedClient);
|
||||
watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => {
|
||||
await createDefaultSpace();
|
||||
});
|
||||
|
||||
server.expose('getSpaceId', (request: Legacy.Request) =>
|
||||
spacesPlugin.spacesService.getSpaceId(request)
|
||||
);
|
||||
server.expose('getActiveSpace', (request: Legacy.Request) =>
|
||||
spacesPlugin.spacesService.getActiveSpace(request)
|
||||
);
|
||||
server.expose('spaceIdToNamespace', spacesPlugin.spacesService.spaceIdToNamespace);
|
||||
server.expose('namespaceToSpaceId', spacesPlugin.spacesService.namespaceToSpaceId);
|
||||
server.expose('getBasePath', spacesPlugin.spacesService.getBasePath);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Space } from '../../common/model/space';
|
|||
import { GetSpacePurpose } from '../../common/model/types';
|
||||
import { CopySavedObjectsToSpaceResponse } from './copy_saved_objects_to_space/types';
|
||||
import { ENTER_SPACE_PATH } from '../../common/constants';
|
||||
import { addSpaceIdToPath } from '../../common';
|
||||
import { addSpaceIdToPath } from '../../../../../plugins/spaces/common';
|
||||
|
||||
export class SpacesManager extends EventEmitter {
|
||||
constructor(private readonly serverBasePath: string) {
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface LicenseCheckResult {
|
||||
showSpaces: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns object that defines behavior of the spaces related features based
|
||||
* on the license information extracted from the xPackInfo.
|
||||
* @param {XPackInfo} xPackInfo XPackInfo instance to extract license information from.
|
||||
* @returns {LicenseCheckResult}
|
||||
*/
|
||||
export function checkLicense(xPackInfo: any): LicenseCheckResult {
|
||||
if (!xPackInfo.isAvailable()) {
|
||||
return {
|
||||
showSpaces: false,
|
||||
};
|
||||
}
|
||||
|
||||
const isAnyXpackLicense = xPackInfo.license.isOneOf([
|
||||
'basic',
|
||||
'standard',
|
||||
'gold',
|
||||
'platinum',
|
||||
'trial',
|
||||
]);
|
||||
|
||||
if (!isAnyXpackLicense) {
|
||||
return {
|
||||
showSpaces: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
showSpaces: true,
|
||||
};
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Space } from '../../common/model/space';
|
||||
import { wrapError } from './errors';
|
||||
import { SpacesClient } from './spaces_client';
|
||||
import { getSpaceIdFromPath } from '../../common';
|
||||
|
||||
export async function getActiveSpace(
|
||||
spacesClient: SpacesClient,
|
||||
requestBasePath: string,
|
||||
serverBasePath: string
|
||||
): Promise<Space> {
|
||||
const spaceId = getSpaceIdFromPath(requestBasePath, serverBasePath);
|
||||
|
||||
try {
|
||||
return spacesClient.get(spaceId);
|
||||
} catch (e) {
|
||||
throw wrapError(e);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { XPackMainPlugin } from '../../../xpack_main/xpack_main';
|
||||
|
||||
interface LicenseCheckDeps {
|
||||
xpackMain: XPackMainPlugin;
|
||||
}
|
||||
|
||||
export function routePreCheckLicense({ xpackMain }: LicenseCheckDeps) {
|
||||
const pluginId = 'spaces';
|
||||
return function forbidApiAccess(request: any) {
|
||||
const licenseCheckResults = xpackMain.info.feature(pluginId).getLicenseCheckResults();
|
||||
if (!licenseCheckResults.showSpaces) {
|
||||
return Boom.forbidden(licenseCheckResults.linksMessage);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { spaceSchema } from './space_schema';
|
||||
|
||||
const defaultProperties = {
|
||||
id: 'foo',
|
||||
name: 'foo',
|
||||
};
|
||||
|
||||
describe('#id', () => {
|
||||
test('is optional', () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: undefined,
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test('allows lowercase a-z, 0-9, "_" and "-"', () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: 'abcdefghijklmnopqrstuvwxyz0123456789_-',
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test(`doesn't allow uppercase`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: 'Foo',
|
||||
});
|
||||
expect(result.error).toMatchInlineSnapshot(
|
||||
`[ValidationError: child "id" fails because ["id" with value "Foo" fails to match the lower case, a-z, 0-9, "_", and "-" are allowed pattern]]`
|
||||
);
|
||||
});
|
||||
|
||||
test(`doesn't allow an empty string`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: '',
|
||||
});
|
||||
expect(result.error).toMatchInlineSnapshot(
|
||||
`[ValidationError: child "id" fails because ["id" is not allowed to be empty]]`
|
||||
);
|
||||
});
|
||||
|
||||
['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', ',', '.', '/', '?'].forEach(
|
||||
invalidCharacter => {
|
||||
test(`doesn't allow ${invalidCharacter}`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: `foo-${invalidCharacter}`,
|
||||
});
|
||||
expect(result.error).toMatchObject({
|
||||
message: `child "id" fails because ["id" with value "foo-${invalidCharacter}" fails to match the lower case, a-z, 0-9, "_", and "-" are allowed pattern]`,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('#color', () => {
|
||||
test('is optional', () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: undefined,
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test(`doesn't allow an empty string`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '',
|
||||
});
|
||||
expect(result.error).toMatchInlineSnapshot(
|
||||
`[ValidationError: child "color" fails because ["color" is not allowed to be empty]]`
|
||||
);
|
||||
});
|
||||
|
||||
test(`allows lower case hex color code`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '#aabbcc',
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test(`allows upper case hex color code`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '#AABBCC',
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test(`allows numeric hex color code`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '#123456',
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test(`must start with a hash`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '123456',
|
||||
});
|
||||
expect(result.error).toMatchInlineSnapshot(
|
||||
`[ValidationError: child "color" fails because ["color" with value "123456" fails to match the 6 digit hex color, starting with a # pattern]]`
|
||||
);
|
||||
});
|
||||
|
||||
test(`cannot exceed 6 digits following the hash`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '1234567',
|
||||
});
|
||||
expect(result.error).toMatchInlineSnapshot(
|
||||
`[ValidationError: child "color" fails because ["color" with value "1234567" fails to match the 6 digit hex color, starting with a # pattern]]`
|
||||
);
|
||||
});
|
||||
|
||||
test(`cannot be fewer than 6 digits following the hash`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '12345',
|
||||
});
|
||||
expect(result.error).toMatchInlineSnapshot(
|
||||
`[ValidationError: child "color" fails because ["color" with value "12345" fails to match the 6 digit hex color, starting with a # pattern]]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#imageUrl', () => {
|
||||
test('is optional', () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
imageUrl: undefined,
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
test(`must start with data:image`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
imageUrl: 'notValid',
|
||||
});
|
||||
expect(result.error).toMatchInlineSnapshot(
|
||||
`[ValidationError: child "imageUrl" fails because ["imageUrl" with value "notValid" fails to match the Image URL should start with 'data:image' pattern]]`
|
||||
);
|
||||
});
|
||||
|
||||
test(`checking that a valid image is accepted as imageUrl`, () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
imageUrl:
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTnU1rJkAAAB3klEQVRYR+2WzUrDQBCARzwqehE8ir1WPfgqRRA1bePBXgpe/MGCB9/Aiw+j+ASCB6kotklaEwW1F0WwNSaps9lV69awGzBpDzt8pJP9mXxsmk3ABH2oUEIilJAIJSRCCYlQQiKUkIh4QgY5agZodVjBowFrBktWQzDBU2ykiYaDuQpCYgnl3QunGzM6Z6YF+b5SkcgK1UH/aLbYReQiYL9d9/o+XFop5IU0Vl4uapAzoXC3eEBPw9vH1/wT6Vs2otPSkoH/IZzlzO/TU2vgQm8nl69Hp0H7nZ4OXogLJSSKBIUC3w88n+Ueyfv56fVZnqCQNVnCHbLrkV0Gd2d+GNkglsk438dhaTxloZDutV4wb06Vf40JcWZ2sMttPpE8NaHGeBnzIAhwPXqHseVB11EyLD0hxLUeaYud2a3B0g3k7GyFtrhX7F2RqhC+yV3jgTb2Rqdqf7/kUxYiWBOlTtXxfPJEtc8b5thGb+8AhL4ohnCNqQjZ2T2+K5rnw2M6KwEhKNDSGM3pTdxjhDgLbHkw/v/zw4AiPuSsfMzAiTidKxiF/ArpFqyzK8SMOlkwvloUMYRCtNvZLWeuIomd2Za/WZS4QomjhEQoIRFKSIQSEqGERAyfEH4YDBFQ/ARU6BiBxCAIQQAAAABJRU5ErkJggg==',
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
});
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { MAX_SPACE_INITIALS } from '../../common/constants';
|
||||
|
||||
export const SPACE_ID_REGEX = /^[a-z0-9_\-]+$/;
|
||||
|
||||
export const spaceSchema = Joi.object({
|
||||
id: Joi.string().regex(SPACE_ID_REGEX, `lower case, a-z, 0-9, "_", and "-" are allowed`),
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string().allow(''),
|
||||
initials: Joi.string().max(MAX_SPACE_INITIALS),
|
||||
color: Joi.string().regex(/^#[a-zA-Z0-9]{6}$/, `6 digit hex color, starting with a #`),
|
||||
disabledFeatures: Joi.array()
|
||||
.items(Joi.string())
|
||||
.default([]),
|
||||
_reserved: Joi.boolean(),
|
||||
imageUrl: Joi.string()
|
||||
.allow('')
|
||||
.regex(/^data:image.*$/, `Image URL should start with 'data:image'`),
|
||||
}).default();
|
|
@ -1,309 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { Server } from 'hapi';
|
||||
import { Legacy } from 'kibana';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchServiceMock, coreMock } from 'src/core/server/mocks';
|
||||
import { SavedObjectsSchema, SavedObjectsLegacyService } from 'src/core/server';
|
||||
import { Readable } from 'stream';
|
||||
import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams';
|
||||
import { createOptionalPlugin } from '../../../../../../server/lib/optional_plugin';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { createSpaces } from './create_spaces';
|
||||
import { ExternalRouteDeps } from '../external';
|
||||
import { SpacesService } from '../../../new_platform/spaces_service';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { LegacyAPI } from '../../../new_platform/plugin';
|
||||
|
||||
interface KibanaServer extends Legacy.Server {
|
||||
savedObjects: any;
|
||||
}
|
||||
|
||||
export interface TestConfig {
|
||||
[configKey: string]: any;
|
||||
}
|
||||
|
||||
export interface TestOptions {
|
||||
setupFn?: (server: any) => void;
|
||||
testConfig?: TestConfig;
|
||||
payload?: any;
|
||||
preCheckLicenseImpl?: (req: any, h: any) => any;
|
||||
expectSpacesClientCall?: boolean;
|
||||
expectPreCheckLicenseCall?: boolean;
|
||||
}
|
||||
|
||||
export type TeardownFn = () => void;
|
||||
|
||||
export interface RequestRunnerResult {
|
||||
server: any;
|
||||
mockSavedObjectsRepository: any;
|
||||
mockSavedObjectsService: {
|
||||
getScopedSavedObjectsClient: jest.Mock<
|
||||
SavedObjectsLegacyService['getScopedSavedObjectsClient']
|
||||
>;
|
||||
importExport: {
|
||||
getSortedObjectsForExport: jest.Mock<
|
||||
SavedObjectsLegacyService['importExport']['getSortedObjectsForExport']
|
||||
>;
|
||||
importSavedObjects: jest.Mock<
|
||||
SavedObjectsLegacyService['importExport']['importSavedObjects']
|
||||
>;
|
||||
resolveImportErrors: jest.Mock<
|
||||
SavedObjectsLegacyService['importExport']['resolveImportErrors']
|
||||
>;
|
||||
};
|
||||
};
|
||||
headers: Record<string, unknown>;
|
||||
response: any;
|
||||
}
|
||||
|
||||
export type RequestRunner = (
|
||||
method: string,
|
||||
path: string,
|
||||
options?: TestOptions
|
||||
) => Promise<RequestRunnerResult>;
|
||||
|
||||
export const defaultPreCheckLicenseImpl = (request: any) => '';
|
||||
|
||||
const baseConfig: TestConfig = {
|
||||
'server.basePath': '',
|
||||
};
|
||||
|
||||
async function readStreamToCompletion(stream: Readable) {
|
||||
return (createPromiseFromStreams([stream, createConcatStream([])]) as unknown) as any[];
|
||||
}
|
||||
|
||||
export function createTestHandler(initApiFn: (deps: ExternalRouteDeps) => void) {
|
||||
const teardowns: TeardownFn[] = [];
|
||||
|
||||
const spaces = createSpaces();
|
||||
|
||||
const request: RequestRunner = async (
|
||||
method: string,
|
||||
path: string,
|
||||
options: TestOptions = {}
|
||||
) => {
|
||||
const {
|
||||
setupFn = () => {
|
||||
return;
|
||||
},
|
||||
testConfig = {},
|
||||
payload,
|
||||
preCheckLicenseImpl = defaultPreCheckLicenseImpl,
|
||||
expectPreCheckLicenseCall = true,
|
||||
expectSpacesClientCall = true,
|
||||
} = options;
|
||||
|
||||
let pre = jest.fn();
|
||||
if (preCheckLicenseImpl) {
|
||||
pre = pre.mockImplementation(preCheckLicenseImpl);
|
||||
}
|
||||
|
||||
const server = new Server() as KibanaServer;
|
||||
|
||||
const config = {
|
||||
...baseConfig,
|
||||
...testConfig,
|
||||
};
|
||||
|
||||
await setupFn(server);
|
||||
|
||||
const mockConfig = {
|
||||
get: (key: string) => config[key],
|
||||
};
|
||||
|
||||
server.decorate('server', 'config', jest.fn<any, any>(() => mockConfig));
|
||||
|
||||
const mockSavedObjectsClientContract = {
|
||||
get: jest.fn((type, id) => {
|
||||
const result = spaces.filter(s => s.id === id);
|
||||
if (!result.length) {
|
||||
throw new Error(`not found: [${type}:${id}]`);
|
||||
}
|
||||
return result[0];
|
||||
}),
|
||||
find: jest.fn(() => {
|
||||
return {
|
||||
total: spaces.length,
|
||||
saved_objects: spaces,
|
||||
};
|
||||
}),
|
||||
create: jest.fn((type, attributes, { id }) => {
|
||||
if (spaces.find(s => s.id === id)) {
|
||||
throw new Error('conflict');
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
update: jest.fn((type, id) => {
|
||||
if (!spaces.find(s => s.id === id)) {
|
||||
throw new Error('not found: during update');
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
delete: jest.fn((type: string, id: string) => {
|
||||
return {};
|
||||
}),
|
||||
deleteByNamespace: jest.fn(),
|
||||
};
|
||||
|
||||
server.savedObjects = {
|
||||
types: ['visualization', 'dashboard', 'index-pattern', 'globalType'],
|
||||
schema: new SavedObjectsSchema({
|
||||
space: {
|
||||
isNamespaceAgnostic: true,
|
||||
hidden: true,
|
||||
},
|
||||
globalType: {
|
||||
isNamespaceAgnostic: true,
|
||||
},
|
||||
}),
|
||||
getScopedSavedObjectsClient: jest.fn().mockResolvedValue(mockSavedObjectsClientContract),
|
||||
importExport: {
|
||||
getSortedObjectsForExport: jest.fn().mockResolvedValue(
|
||||
new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
if (Array.isArray(payload.objects)) {
|
||||
payload.objects.forEach((o: any) => this.push(o));
|
||||
}
|
||||
this.push(null);
|
||||
},
|
||||
})
|
||||
),
|
||||
importSavedObjects: jest.fn().mockImplementation(async (opts: Record<string, any>) => {
|
||||
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
|
||||
return {
|
||||
success: true,
|
||||
successCount: objectsToImport.length,
|
||||
};
|
||||
}),
|
||||
resolveImportErrors: jest.fn().mockImplementation(async (opts: Record<string, any>) => {
|
||||
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
|
||||
return {
|
||||
success: true,
|
||||
successCount: objectsToImport.length,
|
||||
};
|
||||
}),
|
||||
},
|
||||
SavedObjectsClient: {
|
||||
errors: {
|
||||
isNotFoundError: jest.fn((e: any) => e.message.startsWith('not found:')),
|
||||
isConflictError: jest.fn((e: any) => e.message.startsWith('conflict')),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
server.plugins.elasticsearch = {
|
||||
createCluster: jest.fn(),
|
||||
waitUntilReady: jest.fn(),
|
||||
getCluster: jest.fn().mockReturnValue({
|
||||
callWithRequest: jest.fn(),
|
||||
callWithInternalUser: jest.fn(),
|
||||
}),
|
||||
};
|
||||
|
||||
const log = {
|
||||
log: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn(),
|
||||
};
|
||||
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
|
||||
const legacyAPI = {
|
||||
legacyConfig: {
|
||||
serverBasePath: mockConfig.get('server.basePath'),
|
||||
serverDefaultRoute: mockConfig.get('server.defaultRoute'),
|
||||
},
|
||||
savedObjects: server.savedObjects,
|
||||
} as LegacyAPI;
|
||||
|
||||
const service = new SpacesService(log, () => legacyAPI);
|
||||
const spacesService = await service.setup({
|
||||
http: coreSetupMock.http,
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
security: createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
|
||||
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
|
||||
config$: Rx.of({ maxSpaces: 1000 }),
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
mockSavedObjectsClientContract,
|
||||
{ maxSpaces: 1000 },
|
||||
mockSavedObjectsClientContract,
|
||||
req
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
initApiFn({
|
||||
routePreCheckLicenseFn: pre,
|
||||
savedObjects: server.savedObjects,
|
||||
spacesService,
|
||||
log,
|
||||
legacyRouter: server.route.bind(server),
|
||||
});
|
||||
|
||||
teardowns.push(() => server.stop());
|
||||
|
||||
const headers = {
|
||||
authorization: 'foo',
|
||||
};
|
||||
|
||||
const testRun = async () => {
|
||||
const response = await server.inject({
|
||||
method,
|
||||
url: path,
|
||||
headers,
|
||||
payload,
|
||||
});
|
||||
|
||||
if (preCheckLicenseImpl && expectPreCheckLicenseCall) {
|
||||
expect(pre).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(pre).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
if (expectSpacesClientCall) {
|
||||
expect(spacesService.scopedClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: headers.authorization,
|
||||
}),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
expect(spacesService.scopedClient).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return {
|
||||
server,
|
||||
headers,
|
||||
mockSavedObjectsRepository: mockSavedObjectsClientContract,
|
||||
mockSavedObjectsService: server.savedObjects,
|
||||
response: await testRun(),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
request,
|
||||
teardowns,
|
||||
};
|
||||
}
|
|
@ -1,443 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('../../../lib/route_pre_check_license', () => {
|
||||
return {
|
||||
routePreCheckLicense: () => (request: any, h: any) => h.continue,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../server/lib/get_client_shield', () => {
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
callWithInternalUser: jest.fn(() => {
|
||||
return;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import Boom from 'boom';
|
||||
import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
|
||||
describe('POST /api/spaces/_copy_saved_objects', () => {
|
||||
let request: RequestRunner;
|
||||
let teardowns: TeardownFn[];
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = createTestHandler(initCopyToSpacesApi);
|
||||
|
||||
request = setup.request;
|
||||
teardowns = setup.teardowns;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(403);
|
||||
expect(JSON.parse(responsePayload)).toMatchObject({
|
||||
message: 'test forbidden message',
|
||||
});
|
||||
});
|
||||
|
||||
test(`uses a Saved Objects Client instance without the spaces wrapper`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { mockSavedObjectsService } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
expect(mockSavedObjectsService.getScopedSavedObjectsClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{
|
||||
excludedWrappers: ['spaces'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test(`requires space IDs to be unique`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'a-space'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`requires well-formed space IDS`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'a-space-invalid-!@#$%^&*()'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`requires objects to be unique`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [{ type: 'foo', id: 'bar' }, { type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [{ type: 'globalType', id: 'bar' }, { type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response, mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_copy_saved_objects',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsService.importExport.importSavedObjects).toHaveBeenCalledTimes(1);
|
||||
const [
|
||||
importCallOptions,
|
||||
] = mockSavedObjectsService.importExport.importSavedObjects.mock.calls[0];
|
||||
|
||||
expect(importCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
|
||||
test('copies to multiple spaces', async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'b-space'],
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response, mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_copy_saved_objects',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsService.importExport.importSavedObjects).toHaveBeenCalledTimes(2);
|
||||
const [
|
||||
firstImportCallOptions,
|
||||
] = mockSavedObjectsService.importExport.importSavedObjects.mock.calls[0];
|
||||
|
||||
expect(firstImportCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
});
|
||||
|
||||
const [
|
||||
secondImportCallOptions,
|
||||
] = mockSavedObjectsService.importExport.importSavedObjects.mock.calls[1];
|
||||
|
||||
expect(secondImportCallOptions).toMatchObject({
|
||||
namespace: 'b-space',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/spaces/_resolve_copy_saved_objects_errors', () => {
|
||||
let request: RequestRunner;
|
||||
let teardowns: TeardownFn[];
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = createTestHandler(initCopyToSpacesApi);
|
||||
|
||||
request = setup.request;
|
||||
teardowns = setup.teardowns;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const payload = {
|
||||
retries: {},
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_resolve_copy_saved_objects_errors', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(403);
|
||||
expect(JSON.parse(responsePayload)).toMatchObject({
|
||||
message: 'test forbidden message',
|
||||
});
|
||||
});
|
||||
|
||||
test(`uses a Saved Objects Client instance without the spaces wrapper`, async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
expect(mockSavedObjectsService.getScopedSavedObjectsClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{
|
||||
excludedWrappers: ['spaces'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test(`requires objects to be unique`, async () => {
|
||||
const payload = {
|
||||
retries: {},
|
||||
objects: [{ type: 'foo', id: 'bar' }, { type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_resolve_copy_saved_objects_errors', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`requires well-formed space ids`, async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['invalid-space-id!@#$%^&*()']: [
|
||||
{
|
||||
type: 'foo',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [{ type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_resolve_copy_saved_objects_errors', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
},
|
||||
{ type: 'visualization', id: 'bar' },
|
||||
],
|
||||
};
|
||||
|
||||
const { response, mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsService.importExport.resolveImportErrors).toHaveBeenCalledTimes(1);
|
||||
const [
|
||||
resolveImportErrorsCallOptions,
|
||||
] = mockSavedObjectsService.importExport.resolveImportErrors.mock.calls[0];
|
||||
|
||||
expect(resolveImportErrorsCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves conflicts for multiple spaces', async () => {
|
||||
const payload = {
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
['b-space']: [
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const { response, mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsService.importExport.resolveImportErrors).toHaveBeenCalledTimes(2);
|
||||
const [
|
||||
resolveImportErrorsFirstCallOptions,
|
||||
] = mockSavedObjectsService.importExport.resolveImportErrors.mock.calls[0];
|
||||
|
||||
expect(resolveImportErrorsFirstCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
|
||||
const [
|
||||
resolveImportErrorsSecondCallOptions,
|
||||
] = mockSavedObjectsService.importExport.resolveImportErrors.mock.calls[1];
|
||||
|
||||
expect(resolveImportErrorsSecondCallOptions).toMatchObject({
|
||||
namespace: 'b-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { Legacy } from 'kibana';
|
||||
import {
|
||||
copySavedObjectsToSpacesFactory,
|
||||
resolveCopySavedObjectsToSpacesConflictsFactory,
|
||||
} from '../../../lib/copy_to_spaces';
|
||||
import { ExternalRouteDeps } from '.';
|
||||
import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from '../../../lib/copy_to_spaces/copy_to_spaces';
|
||||
import { SPACE_ID_REGEX } from '../../../lib/space_schema';
|
||||
|
||||
interface CopyPayload {
|
||||
spaces: string[];
|
||||
objects: Array<{ type: string; id: string }>;
|
||||
includeReferences: boolean;
|
||||
overwrite: boolean;
|
||||
}
|
||||
|
||||
interface ResolveConflictsPayload {
|
||||
objects: Array<{ type: string; id: string }>;
|
||||
includeReferences: boolean;
|
||||
retries: {
|
||||
[spaceId: string]: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
overwrite: boolean;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { legacyRouter, spacesService, savedObjects, routePreCheckLicenseFn } = deps;
|
||||
|
||||
legacyRouter({
|
||||
method: 'POST',
|
||||
path: '/api/spaces/_copy_saved_objects',
|
||||
async handler(request: Legacy.Request, h: Legacy.ResponseToolkit) {
|
||||
const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(
|
||||
request,
|
||||
COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS
|
||||
);
|
||||
|
||||
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
|
||||
savedObjectsClient,
|
||||
savedObjects
|
||||
);
|
||||
|
||||
const {
|
||||
spaces: destinationSpaceIds,
|
||||
objects,
|
||||
includeReferences,
|
||||
overwrite,
|
||||
} = request.payload as CopyPayload;
|
||||
|
||||
const sourceSpaceId = spacesService.getSpaceId(request);
|
||||
|
||||
const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, {
|
||||
objects,
|
||||
includeReferences,
|
||||
overwrite,
|
||||
});
|
||||
|
||||
return h.response(copyResponse);
|
||||
},
|
||||
options: {
|
||||
tags: ['access:copySavedObjectsToSpaces'],
|
||||
validate: {
|
||||
payload: {
|
||||
spaces: Joi.array()
|
||||
.items(
|
||||
Joi.string().regex(SPACE_ID_REGEX, `lower case, a-z, 0-9, "_", and "-" are allowed`)
|
||||
)
|
||||
.unique(),
|
||||
objects: Joi.array()
|
||||
.items(Joi.object({ type: Joi.string(), id: Joi.string() }))
|
||||
.unique(),
|
||||
includeReferences: Joi.bool().default(false),
|
||||
overwrite: Joi.bool().default(false),
|
||||
},
|
||||
},
|
||||
pre: [routePreCheckLicenseFn],
|
||||
},
|
||||
});
|
||||
|
||||
legacyRouter({
|
||||
method: 'POST',
|
||||
path: '/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
async handler(request: Legacy.Request, h: Legacy.ResponseToolkit) {
|
||||
const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(
|
||||
request,
|
||||
COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS
|
||||
);
|
||||
|
||||
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
|
||||
savedObjectsClient,
|
||||
savedObjects
|
||||
);
|
||||
|
||||
const { objects, includeReferences, retries } = request.payload as ResolveConflictsPayload;
|
||||
|
||||
const sourceSpaceId = spacesService.getSpaceId(request);
|
||||
|
||||
const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts(
|
||||
sourceSpaceId,
|
||||
{
|
||||
objects,
|
||||
includeReferences,
|
||||
retries,
|
||||
}
|
||||
);
|
||||
|
||||
return h.response(resolveConflictsResponse);
|
||||
},
|
||||
options: {
|
||||
tags: ['access:copySavedObjectsToSpaces'],
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
objects: Joi.array()
|
||||
.items(Joi.object({ type: Joi.string(), id: Joi.string() }))
|
||||
.required()
|
||||
.unique(),
|
||||
includeReferences: Joi.bool().default(false),
|
||||
retries: Joi.object()
|
||||
.pattern(
|
||||
SPACE_ID_REGEX,
|
||||
Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
overwrite: Joi.boolean().default(false),
|
||||
})
|
||||
)
|
||||
)
|
||||
.required(),
|
||||
}).default(),
|
||||
},
|
||||
pre: [routePreCheckLicenseFn],
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('../../../lib/route_pre_check_license', () => {
|
||||
return {
|
||||
routePreCheckLicense: () => (request: any, h: any) => h.continue,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../server/lib/get_client_shield', () => {
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
callWithInternalUser: jest.fn(() => {
|
||||
return;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
import Boom from 'boom';
|
||||
import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__';
|
||||
import { initDeleteSpacesApi } from './delete';
|
||||
|
||||
describe('Spaces Public API', () => {
|
||||
let request: RequestRunner;
|
||||
let teardowns: TeardownFn[];
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = createTestHandler(initDeleteSpacesApi);
|
||||
|
||||
request = setup.request;
|
||||
teardowns = setup.teardowns;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
test(`'DELETE spaces/{id}' deletes the space`, async () => {
|
||||
const { response } = await request('DELETE', '/api/spaces/space/a-space');
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(204);
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const { response } = await request('DELETE', '/api/spaces/space/a-space', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
expectSpacesClientCall: false,
|
||||
});
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(403);
|
||||
expect(JSON.parse(payload)).toMatchObject({
|
||||
message: 'test forbidden message',
|
||||
});
|
||||
});
|
||||
|
||||
test('DELETE spaces/{id} throws when deleting a non-existent space', async () => {
|
||||
const { response } = await request('DELETE', '/api/spaces/space/not-a-space');
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(404);
|
||||
});
|
||||
|
||||
test(`'DELETE spaces/{id}' cannot delete reserved spaces`, async () => {
|
||||
const { response } = await request('DELETE', '/api/spaces/space/default');
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(payload)).toEqual({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'This Space cannot be deleted because it is reserved.',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { wrapError } from '../../../lib/errors';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { ExternalRouteDeps, ExternalRouteRequestFacade } from '.';
|
||||
|
||||
export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { legacyRouter, savedObjects, spacesService, routePreCheckLicenseFn } = deps;
|
||||
|
||||
legacyRouter({
|
||||
method: 'DELETE',
|
||||
path: '/api/spaces/space/{id}',
|
||||
async handler(request: ExternalRouteRequestFacade, h: any) {
|
||||
const { SavedObjectsClient } = savedObjects;
|
||||
const spacesClient: SpacesClient = await spacesService.scopedClient(request);
|
||||
|
||||
const id = request.params.id;
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await spacesClient.delete(id);
|
||||
} catch (error) {
|
||||
if (SavedObjectsClient.errors.isNotFoundError(error)) {
|
||||
return Boom.notFound();
|
||||
}
|
||||
return wrapError(error);
|
||||
}
|
||||
|
||||
return h.response(result).code(204);
|
||||
},
|
||||
options: {
|
||||
pre: [routePreCheckLicenseFn],
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('../../../lib/route_pre_check_license', () => {
|
||||
return {
|
||||
routePreCheckLicense: () => (request: any, h: any) => h.continue,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../server/lib/get_client_shield', () => {
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
callWithInternalUser: jest.fn(() => {
|
||||
return;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
import Boom from 'boom';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { createSpaces, createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__';
|
||||
import { initGetSpacesApi } from './get';
|
||||
|
||||
describe('GET spaces', () => {
|
||||
let request: RequestRunner;
|
||||
let teardowns: TeardownFn[];
|
||||
const spaces = createSpaces();
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = createTestHandler(initGetSpacesApi);
|
||||
|
||||
request = setup.request;
|
||||
teardowns = setup.teardowns;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
test(`'GET spaces' returns all available spaces`, async () => {
|
||||
const { response } = await request('GET', '/api/spaces/space');
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
const resultSpaces: Space[] = JSON.parse(payload);
|
||||
expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id));
|
||||
});
|
||||
|
||||
test(`'GET spaces' returns all available spaces with the 'any' purpose`, async () => {
|
||||
const { response } = await request('GET', '/api/spaces/space?purpose=any');
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
const resultSpaces: Space[] = JSON.parse(payload);
|
||||
expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id));
|
||||
});
|
||||
|
||||
test(`'GET spaces' returns all available spaces with the 'copySavedObjectsIntoSpace' purpose`, async () => {
|
||||
const { response } = await request(
|
||||
'GET',
|
||||
'/api/spaces/space?purpose=copySavedObjectsIntoSpace'
|
||||
);
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
const resultSpaces: Space[] = JSON.parse(payload);
|
||||
expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id));
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const { response } = await request('GET', '/api/spaces/space', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
expectSpacesClientCall: false,
|
||||
});
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(403);
|
||||
expect(JSON.parse(payload)).toMatchObject({
|
||||
message: 'test forbidden message',
|
||||
});
|
||||
});
|
||||
|
||||
test(`'GET spaces/{id}' returns the space with that id`, async () => {
|
||||
const { response } = await request('GET', '/api/spaces/space/default');
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
const resultSpace = JSON.parse(payload);
|
||||
expect(resultSpace.id).toEqual('default');
|
||||
});
|
||||
|
||||
test(`'GET spaces/{id}' returns 404 when retrieving a non-existent space`, async () => {
|
||||
const { response } = await request('GET', '/api/spaces/space/not-a-space');
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(404);
|
||||
});
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Joi from 'joi';
|
||||
import { RequestQuery } from 'hapi';
|
||||
import { GetSpacePurpose } from '../../../../common/model/types';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { wrapError } from '../../../lib/errors';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { ExternalRouteDeps, ExternalRouteRequestFacade } from '.';
|
||||
|
||||
export function initGetSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { legacyRouter, log, spacesService, savedObjects, routePreCheckLicenseFn } = deps;
|
||||
|
||||
legacyRouter({
|
||||
method: 'GET',
|
||||
path: '/api/spaces/space',
|
||||
async handler(request: ExternalRouteRequestFacade) {
|
||||
log.debug(`Inside GET /api/spaces/space`);
|
||||
|
||||
const purpose: GetSpacePurpose = (request.query as RequestQuery).purpose as GetSpacePurpose;
|
||||
|
||||
const spacesClient: SpacesClient = await spacesService.scopedClient(request);
|
||||
|
||||
let spaces: Space[];
|
||||
|
||||
try {
|
||||
log.debug(`Attempting to retrieve all spaces for ${purpose} purpose`);
|
||||
spaces = await spacesClient.getAll(purpose);
|
||||
log.debug(`Retrieved ${spaces.length} spaces for ${purpose} purpose`);
|
||||
} catch (error) {
|
||||
log.debug(`Error retrieving spaces for ${purpose} purpose: ${error}`);
|
||||
return wrapError(error);
|
||||
}
|
||||
|
||||
return spaces;
|
||||
},
|
||||
options: {
|
||||
pre: [routePreCheckLicenseFn],
|
||||
validate: {
|
||||
query: Joi.object().keys({
|
||||
purpose: Joi.string()
|
||||
.valid('any', 'copySavedObjectsIntoSpace')
|
||||
.default('any'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
legacyRouter({
|
||||
method: 'GET',
|
||||
path: '/api/spaces/space/{id}',
|
||||
async handler(request: ExternalRouteRequestFacade) {
|
||||
const spaceId = request.params.id;
|
||||
|
||||
const { SavedObjectsClient } = savedObjects;
|
||||
const spacesClient: SpacesClient = await spacesService.scopedClient(request);
|
||||
|
||||
try {
|
||||
return await spacesClient.get(spaceId);
|
||||
} catch (error) {
|
||||
if (SavedObjectsClient.errors.isNotFoundError(error)) {
|
||||
return Boom.notFound();
|
||||
}
|
||||
return wrapError(error);
|
||||
}
|
||||
},
|
||||
options: {
|
||||
pre: [routePreCheckLicenseFn],
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { Logger, SavedObjectsLegacyService } from 'src/core/server';
|
||||
import { XPackMainPlugin } from '../../../../../xpack_main/xpack_main';
|
||||
import { routePreCheckLicense } from '../../../lib/route_pre_check_license';
|
||||
import { initDeleteSpacesApi } from './delete';
|
||||
import { initGetSpacesApi } from './get';
|
||||
import { initPostSpacesApi } from './post';
|
||||
import { initPutSpacesApi } from './put';
|
||||
import { SpacesServiceSetup } from '../../../new_platform/spaces_service/spaces_service';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
|
||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
interface RouteDeps {
|
||||
xpackMain: XPackMainPlugin;
|
||||
legacyRouter: Legacy.Server['route'];
|
||||
savedObjects: SavedObjectsLegacyService;
|
||||
spacesService: SpacesServiceSetup;
|
||||
log: Logger;
|
||||
}
|
||||
|
||||
export interface ExternalRouteDeps extends Omit<RouteDeps, 'xpackMain'> {
|
||||
routePreCheckLicenseFn: any;
|
||||
}
|
||||
|
||||
export type ExternalRouteRequestFacade = Legacy.Request;
|
||||
|
||||
export function initExternalSpacesApi({ xpackMain, ...rest }: RouteDeps) {
|
||||
const routePreCheckLicenseFn = routePreCheckLicense({ xpackMain });
|
||||
|
||||
const deps: ExternalRouteDeps = {
|
||||
...rest,
|
||||
routePreCheckLicenseFn,
|
||||
};
|
||||
|
||||
initDeleteSpacesApi(deps);
|
||||
initGetSpacesApi(deps);
|
||||
initPostSpacesApi(deps);
|
||||
initPutSpacesApi(deps);
|
||||
initCopyToSpacesApi(deps);
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('../../../lib/route_pre_check_license', () => {
|
||||
return {
|
||||
routePreCheckLicense: () => (request: any, h: any) => h.continue,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../server/lib/get_client_shield', () => {
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
callWithInternalUser: jest.fn(() => {
|
||||
return;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import Boom from 'boom';
|
||||
import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__';
|
||||
import { initPostSpacesApi } from './post';
|
||||
|
||||
describe('Spaces Public API', () => {
|
||||
let request: RequestRunner;
|
||||
let teardowns: TeardownFn[];
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = createTestHandler(initPostSpacesApi);
|
||||
|
||||
request = setup.request;
|
||||
teardowns = setup.teardowns;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
test('POST /space should create a new space with the provided ID', async () => {
|
||||
const payload = {
|
||||
id: 'my-space-id',
|
||||
name: 'my new space',
|
||||
description: 'with a description',
|
||||
disabledFeatures: ['foo'],
|
||||
};
|
||||
|
||||
const { mockSavedObjectsRepository, response } = await request('POST', '/api/spaces/space', {
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(mockSavedObjectsRepository.create).toHaveBeenCalledWith(
|
||||
'space',
|
||||
{ name: 'my new space', description: 'with a description', disabledFeatures: ['foo'] },
|
||||
{ id: 'my-space-id' }
|
||||
);
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const payload = {
|
||||
id: 'my-space-id',
|
||||
name: 'my new space',
|
||||
description: 'with a description',
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/space', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(403);
|
||||
expect(JSON.parse(responsePayload)).toMatchObject({
|
||||
message: 'test forbidden message',
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /space should not allow a space to be updated', async () => {
|
||||
const payload = {
|
||||
id: 'a-space',
|
||||
name: 'my updated space',
|
||||
description: 'with a description',
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/space', { payload });
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(409);
|
||||
expect(JSON.parse(responsePayload)).toEqual({
|
||||
error: 'Conflict',
|
||||
message: 'A space with the identifier a-space already exists.',
|
||||
statusCode: 409,
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /space should not require disabledFeatures to be specified', async () => {
|
||||
const payload = {
|
||||
id: 'my-space-id',
|
||||
name: 'my new space',
|
||||
description: 'with a description',
|
||||
};
|
||||
|
||||
const { mockSavedObjectsRepository, response } = await request('POST', '/api/spaces/space', {
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(mockSavedObjectsRepository.create).toHaveBeenCalledWith(
|
||||
'space',
|
||||
{ name: 'my new space', description: 'with a description', disabledFeatures: [] },
|
||||
{ id: 'my-space-id' }
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { wrapError } from '../../../lib/errors';
|
||||
import { spaceSchema } from '../../../lib/space_schema';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { ExternalRouteDeps, ExternalRouteRequestFacade } from '.';
|
||||
|
||||
export function initPostSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { legacyRouter, log, spacesService, savedObjects, routePreCheckLicenseFn } = deps;
|
||||
|
||||
legacyRouter({
|
||||
method: 'POST',
|
||||
path: '/api/spaces/space',
|
||||
async handler(request: ExternalRouteRequestFacade) {
|
||||
log.debug(`Inside POST /api/spaces/space`);
|
||||
const { SavedObjectsClient } = savedObjects;
|
||||
const spacesClient: SpacesClient = await spacesService.scopedClient(request);
|
||||
|
||||
const space = request.payload as Space;
|
||||
|
||||
try {
|
||||
log.debug(`Attempting to create space`);
|
||||
return await spacesClient.create(space);
|
||||
} catch (error) {
|
||||
if (SavedObjectsClient.errors.isConflictError(error)) {
|
||||
return Boom.conflict(`A space with the identifier ${space.id} already exists.`);
|
||||
}
|
||||
log.debug(`Error creating space: ${error}`);
|
||||
return wrapError(error);
|
||||
}
|
||||
},
|
||||
options: {
|
||||
validate: {
|
||||
payload: spaceSchema,
|
||||
},
|
||||
pre: [routePreCheckLicenseFn],
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
jest.mock('../../../lib/route_pre_check_license', () => {
|
||||
return {
|
||||
routePreCheckLicense: () => (request: any, h: any) => h.continue,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../server/lib/get_client_shield', () => {
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
callWithInternalUser: jest.fn(() => {
|
||||
return;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
import Boom from 'boom';
|
||||
import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__';
|
||||
import { initPutSpacesApi } from './put';
|
||||
|
||||
describe('Spaces Public API', () => {
|
||||
let request: RequestRunner;
|
||||
let teardowns: TeardownFn[];
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = createTestHandler(initPutSpacesApi);
|
||||
|
||||
request = setup.request;
|
||||
teardowns = setup.teardowns;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
test('PUT /space should update an existing space with the provided ID', async () => {
|
||||
const payload = {
|
||||
id: 'a-space',
|
||||
name: 'my updated space',
|
||||
description: 'with a description',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
const { mockSavedObjectsRepository, response } = await request(
|
||||
'PUT',
|
||||
'/api/spaces/space/a-space',
|
||||
{
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsRepository.update).toHaveBeenCalledTimes(1);
|
||||
expect(mockSavedObjectsRepository.update).toHaveBeenCalledWith('space', 'a-space', {
|
||||
name: 'my updated space',
|
||||
description: 'with a description',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT /space should allow an empty description', async () => {
|
||||
const payload = {
|
||||
id: 'a-space',
|
||||
name: 'my updated space',
|
||||
description: '',
|
||||
disabledFeatures: ['foo'],
|
||||
};
|
||||
|
||||
const { mockSavedObjectsRepository, response } = await request(
|
||||
'PUT',
|
||||
'/api/spaces/space/a-space',
|
||||
{
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsRepository.update).toHaveBeenCalledTimes(1);
|
||||
expect(mockSavedObjectsRepository.update).toHaveBeenCalledWith('space', 'a-space', {
|
||||
name: 'my updated space',
|
||||
description: '',
|
||||
disabledFeatures: ['foo'],
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT /space should not require disabledFeatures', async () => {
|
||||
const payload = {
|
||||
id: 'a-space',
|
||||
name: 'my updated space',
|
||||
description: '',
|
||||
};
|
||||
|
||||
const { mockSavedObjectsRepository, response } = await request(
|
||||
'PUT',
|
||||
'/api/spaces/space/a-space',
|
||||
{
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsRepository.update).toHaveBeenCalledTimes(1);
|
||||
expect(mockSavedObjectsRepository.update).toHaveBeenCalledWith('space', 'a-space', {
|
||||
name: 'my updated space',
|
||||
description: '',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const payload = {
|
||||
id: 'a-space',
|
||||
name: 'my updated space',
|
||||
description: 'with a description',
|
||||
};
|
||||
|
||||
const { response } = await request('PUT', '/api/spaces/space/a-space', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(403);
|
||||
expect(JSON.parse(responsePayload)).toMatchObject({
|
||||
message: 'test forbidden message',
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT /space should not allow a new space to be created', async () => {
|
||||
const payload = {
|
||||
id: 'a-new-space',
|
||||
name: 'my new space',
|
||||
description: 'with a description',
|
||||
};
|
||||
|
||||
const { response } = await request('PUT', '/api/spaces/space/a-new-space', { payload });
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(404);
|
||||
});
|
||||
});
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { wrapError } from '../../../lib/errors';
|
||||
import { spaceSchema } from '../../../lib/space_schema';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { ExternalRouteDeps, ExternalRouteRequestFacade } from '.';
|
||||
|
||||
export function initPutSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { legacyRouter, spacesService, savedObjects, routePreCheckLicenseFn } = deps;
|
||||
|
||||
legacyRouter({
|
||||
method: 'PUT',
|
||||
path: '/api/spaces/space/{id}',
|
||||
async handler(request: ExternalRouteRequestFacade) {
|
||||
const { SavedObjectsClient } = savedObjects;
|
||||
const spacesClient: SpacesClient = await spacesService.scopedClient(request);
|
||||
|
||||
const space: Space = request.payload as Space;
|
||||
const id = request.params.id;
|
||||
|
||||
let result: Space;
|
||||
try {
|
||||
result = await spacesClient.update(id, { ...space });
|
||||
} catch (error) {
|
||||
if (SavedObjectsClient.errors.isNotFoundError(error)) {
|
||||
return Boom.notFound();
|
||||
}
|
||||
return wrapError(error);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
options: {
|
||||
validate: {
|
||||
payload: spaceSchema,
|
||||
},
|
||||
pre: [routePreCheckLicenseFn],
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { SpacesClient } from '../../lib/spaces_client';
|
||||
import { convertSavedObjectToSpace } from './convert_saved_object_to_space';
|
||||
|
||||
export async function getSpaceById(
|
||||
client: SpacesClient,
|
||||
spaceId: string,
|
||||
errors: any
|
||||
): Promise<Space | null> {
|
||||
try {
|
||||
const existingSpace = await client.get(spaceId);
|
||||
return convertSavedObjectToSpace(existingSpace);
|
||||
} catch (error) {
|
||||
if (errors.isNotFoundError(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
28
x-pack/plugins/spaces/common/constants.ts
Normal file
28
x-pack/plugins/spaces/common/constants.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const DEFAULT_SPACE_ID = `default`;
|
||||
|
||||
/**
|
||||
* The minimum number of spaces required to show a search control.
|
||||
*/
|
||||
export const SPACE_SEARCH_COUNT_THRESHOLD = 8;
|
||||
|
||||
/**
|
||||
* The maximum number of characters allowed in the Space Avatar's initials
|
||||
*/
|
||||
export const MAX_SPACE_INITIALS = 2;
|
||||
|
||||
/**
|
||||
* The type name used within the Monitoring index to publish spaces stats.
|
||||
* @type {string}
|
||||
*/
|
||||
export const KIBANA_SPACES_STATS_TYPE = 'spaces';
|
||||
|
||||
/**
|
||||
* The path to enter a space.
|
||||
*/
|
||||
export const ENTER_SPACE_PATH = '/spaces/enter';
|
9
x-pack/plugins/spaces/common/index.ts
Normal file
9
x-pack/plugins/spaces/common/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { isReservedSpace } from './is_reserved_space';
|
||||
export { MAX_SPACE_INITIALS } from './constants';
|
||||
export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser';
|
34
x-pack/plugins/spaces/common/is_reserved_space.test.ts
Normal file
34
x-pack/plugins/spaces/common/is_reserved_space.test.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isReservedSpace } from './is_reserved_space';
|
||||
import { Space } from './model/space';
|
||||
|
||||
test('it returns true for reserved spaces', () => {
|
||||
const space: Space = {
|
||||
id: '',
|
||||
name: '',
|
||||
disabledFeatures: [],
|
||||
_reserved: true,
|
||||
};
|
||||
|
||||
expect(isReservedSpace(space)).toEqual(true);
|
||||
});
|
||||
|
||||
test('it returns false for non-reserved spaces', () => {
|
||||
const space: Space = {
|
||||
id: '',
|
||||
name: '',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
expect(isReservedSpace(space)).toEqual(false);
|
||||
});
|
||||
|
||||
test('it handles empty input', () => {
|
||||
// @ts-ignore
|
||||
expect(isReservedSpace()).toEqual(false);
|
||||
});
|
18
x-pack/plugins/spaces/common/is_reserved_space.ts
Normal file
18
x-pack/plugins/spaces/common/is_reserved_space.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { Space } from './model/space';
|
||||
|
||||
/**
|
||||
* Returns whether the given Space is reserved or not.
|
||||
*
|
||||
* @param space the space
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isReservedSpace(space?: Partial<Space> | null): boolean {
|
||||
return get(space, '_reserved', false);
|
||||
}
|
|
@ -4,9 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from 'src/core/server';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new Plugin(initializerContext);
|
||||
export interface Space {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
initials?: string;
|
||||
disabledFeatures: string[];
|
||||
_reserved?: boolean;
|
||||
imageUrl?: string;
|
||||
}
|
7
x-pack/plugins/spaces/common/model/types.ts
Normal file
7
x-pack/plugins/spaces/common/model/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type GetSpacePurpose = 'any' | 'copySavedObjectsIntoSpace';
|
9
x-pack/plugins/spaces/kibana.json
Normal file
9
x-pack/plugins/spaces/kibana.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "spaces",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "spaces"],
|
||||
"requiredPlugins": ["features", "licensing"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
22
x-pack/plugins/spaces/server/config.ts
Normal file
22
x-pack/plugins/spaces/server/config.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginInitializerContext } from 'src/core/server';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export const ConfigSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
maxSpaces: schema.number({ defaultValue: 1000 }),
|
||||
});
|
||||
|
||||
export function createConfig$(context: PluginInitializerContext) {
|
||||
return context.config.create<TypeOf<typeof ConfigSchema>>();
|
||||
}
|
||||
|
||||
export type ConfigType = ReturnType<typeof createConfig$> extends Observable<infer P>
|
||||
? P
|
||||
: ReturnType<typeof createConfig$>;
|
22
x-pack/plugins/spaces/server/index.ts
Normal file
22
x-pack/plugins/spaces/server/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '../../../../src/core/server';
|
||||
import { ConfigSchema } from './config';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
// These exports are part of public Spaces plugin contract, any change in signature of exported
|
||||
// functions or removal of exports should be considered as a breaking change. Ideally we should
|
||||
// reduce number of such exports to zero and provide everything we want to expose via Setup/Start
|
||||
// run-time contracts.
|
||||
|
||||
// end public contract exports
|
||||
|
||||
export { SpacesPluginSetup } from './plugin';
|
||||
|
||||
export const config = { schema: ConfigSchema };
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
new Plugin(initializerContext);
|
7
x-pack/plugins/spaces/server/lib/__fixtures__/index.ts
Normal file
7
x-pack/plugins/spaces/server/lib/__fixtures__/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { spacesConfig } from './spaces_config';
|
|
@ -4,12 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { ConfigSchema } from '../../config';
|
||||
|
||||
export const config = {
|
||||
schema: schema.object({
|
||||
maxSpaces: schema.number({ defaultValue: 1000 }),
|
||||
}),
|
||||
};
|
||||
|
||||
export type SpacesConfigType = TypeOf<typeof config.schema>;
|
||||
export const spacesConfig = ConfigSchema.validate({});
|
|
@ -3,23 +3,11 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
jest.mock('../../../../server/lib/get_client_shield', () => ({
|
||||
getClient: jest.fn(),
|
||||
}));
|
||||
import * as Rx from 'rxjs';
|
||||
import Boom from 'boom';
|
||||
import { getClient } from '../../../../server/lib/get_client_shield';
|
||||
import { createDefaultSpace } from './create_default_space';
|
||||
import { SavedObjectsLegacyService } from 'src/core/server';
|
||||
import { ElasticsearchServiceSetup } from 'src/core/server';
|
||||
|
||||
let mockCallWithRequest;
|
||||
beforeEach(() => {
|
||||
mockCallWithRequest = jest.fn();
|
||||
(getClient as jest.Mock).mockReturnValue({
|
||||
callWithRequest: mockCallWithRequest,
|
||||
});
|
||||
});
|
||||
import Boom from 'boom';
|
||||
import { createDefaultSpace } from './create_default_space';
|
||||
import { SavedObjectsLegacyService, IClusterClient } from 'src/core/server';
|
||||
|
||||
interface MockServerSettings {
|
||||
defaultExists?: boolean;
|
||||
simulateGetErrorCondition?: boolean;
|
||||
|
@ -84,11 +72,9 @@ const createMockDeps = (settings: MockServerSettings = {}) => {
|
|||
return {
|
||||
config: mockServer.config(),
|
||||
savedObjects: (mockServer.savedObjects as unknown) as SavedObjectsLegacyService,
|
||||
elasticsearch: ({
|
||||
dataClient$: Rx.of({
|
||||
callAsInternalUser: jest.fn(),
|
||||
}),
|
||||
} as unknown) as ElasticsearchServiceSetup,
|
||||
esClient: ({
|
||||
callAsInternalUser: jest.fn(),
|
||||
} as unknown) as jest.Mocked<IClusterClient>,
|
||||
};
|
||||
};
|
||||
|
|
@ -5,22 +5,18 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { first } from 'rxjs/operators';
|
||||
import { SavedObjectsLegacyService, CoreSetup } from 'src/core/server';
|
||||
import { SavedObjectsLegacyService, IClusterClient } from 'src/core/server';
|
||||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
|
||||
interface Deps {
|
||||
elasticsearch: CoreSetup['elasticsearch'];
|
||||
esClient: Pick<IClusterClient, 'callAsInternalUser'>;
|
||||
savedObjects: SavedObjectsLegacyService;
|
||||
}
|
||||
|
||||
export async function createDefaultSpace({ elasticsearch, savedObjects }: Deps) {
|
||||
export async function createDefaultSpace({ esClient, savedObjects }: Deps) {
|
||||
const { getSavedObjectsRepository, SavedObjectsClient } = savedObjects;
|
||||
|
||||
const client = await elasticsearch.dataClient$.pipe(first()).toPromise();
|
||||
|
||||
const savedObjectsRepository = getSavedObjectsRepository(client.callAsInternalUser, ['space']);
|
||||
const savedObjectsRepository = getSavedObjectsRepository(esClient.callAsInternalUser, ['space']);
|
||||
|
||||
const defaultSpaceExists = await doesDefaultSpaceExist(
|
||||
SavedObjectsClient,
|
17
x-pack/plugins/spaces/server/lib/errors.ts
Normal file
17
x-pack/plugins/spaces/server/lib/errors.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { boomify, isBoom } from 'boom';
|
||||
import { ResponseError, CustomHttpResponseOptions } from 'src/core/server';
|
||||
|
||||
export function wrapError(error: any): CustomHttpResponseOptions<ResponseError> {
|
||||
const boom = isBoom(error) ? error : boomify(error);
|
||||
return {
|
||||
body: boom,
|
||||
headers: boom.output.headers,
|
||||
statusCode: boom.output.statusCode,
|
||||
};
|
||||
}
|
|
@ -4,6 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export function getSpaceSelectorUrl(serverBasePath: string = '') {
|
||||
export function getSpaceSelectorUrl(serverBasePath: string) {
|
||||
return `${serverBasePath}/spaces/space_selector`;
|
||||
}
|
|
@ -5,12 +5,24 @@
|
|||
*/
|
||||
|
||||
import { getSpacesUsageCollector, UsageStats } from './get_spaces_usage_collector';
|
||||
import * as Rx from 'rxjs';
|
||||
import { PluginsSetup } from '../plugin';
|
||||
import { Feature } from '../../../features/server';
|
||||
import { ILicense, LicensingPluginSetup } from '../../../licensing/server';
|
||||
|
||||
function getServerMock(customization?: any) {
|
||||
interface SetupOpts {
|
||||
license?: Partial<ILicense>;
|
||||
features?: Feature[];
|
||||
}
|
||||
|
||||
function setup({
|
||||
license = { isAvailable: true },
|
||||
features = [{ id: 'feature1' } as Feature, { id: 'feature2' } as Feature],
|
||||
}: SetupOpts = {}) {
|
||||
class MockUsageCollector {
|
||||
private fetch: any;
|
||||
|
||||
constructor(server: any, { fetch }: any) {
|
||||
constructor({ fetch }: any) {
|
||||
this.fetch = fetch;
|
||||
}
|
||||
// to make typescript happy
|
||||
|
@ -19,39 +31,23 @@ function getServerMock(customization?: any) {
|
|||
}
|
||||
}
|
||||
|
||||
const getLicenseCheckResults = jest.fn().mockReturnValue({});
|
||||
const defaultServerMock = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
isAvailable: jest.fn().mockReturnValue(true),
|
||||
feature: () => ({
|
||||
getLicenseCheckResults,
|
||||
}),
|
||||
license: {
|
||||
isOneOf: jest.fn().mockReturnValue(false),
|
||||
getType: jest.fn().mockReturnValue('platinum'),
|
||||
},
|
||||
toJSON: () => ({ b: 1 }),
|
||||
},
|
||||
getFeatures: jest.fn().mockReturnValue([{ id: 'feature1' }, { id: 'feature2' }]),
|
||||
},
|
||||
},
|
||||
expose: () => {
|
||||
return;
|
||||
},
|
||||
log: () => {
|
||||
return;
|
||||
},
|
||||
const licensing = {
|
||||
license$: Rx.of(license),
|
||||
} as LicensingPluginSetup;
|
||||
|
||||
const featuresSetup = ({
|
||||
getFeatures: jest.fn().mockReturnValue(features),
|
||||
} as unknown) as PluginsSetup['features'];
|
||||
|
||||
return {
|
||||
licensing,
|
||||
features: featuresSetup,
|
||||
usage: {
|
||||
collectorSet: {
|
||||
makeUsageCollector: (options: any) => {
|
||||
return new MockUsageCollector(defaultServerMock, options);
|
||||
},
|
||||
makeUsageCollector: (options: any) => new MockUsageCollector(options),
|
||||
},
|
||||
},
|
||||
};
|
||||
return Object.assign(defaultServerMock, customization);
|
||||
}
|
||||
|
||||
const defaultCallClusterMock = jest.fn().mockResolvedValue({
|
||||
|
@ -73,17 +69,14 @@ const defaultCallClusterMock = jest.fn().mockResolvedValue({
|
|||
});
|
||||
|
||||
describe('with a basic license', () => {
|
||||
let serverWithBasicLicenseMock: any;
|
||||
let usageStats: UsageStats;
|
||||
beforeAll(async () => {
|
||||
serverWithBasicLicenseMock = getServerMock();
|
||||
serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = jest
|
||||
.fn()
|
||||
.mockReturnValue('basic');
|
||||
const { features, licensing, usage } = setup({ license: { isAvailable: true, type: 'basic' } });
|
||||
const { fetch: getSpacesUsage } = getSpacesUsageCollector({
|
||||
kibanaIndex: '.kibana',
|
||||
usage: serverWithBasicLicenseMock.usage,
|
||||
xpackMain: serverWithBasicLicenseMock.plugins.xpack_main,
|
||||
usage,
|
||||
features,
|
||||
licensing,
|
||||
});
|
||||
usageStats = await getSpacesUsage(defaultCallClusterMock);
|
||||
});
|
||||
|
@ -113,13 +106,12 @@ describe('with a basic license', () => {
|
|||
describe('with no license', () => {
|
||||
let usageStats: UsageStats;
|
||||
beforeAll(async () => {
|
||||
const serverWithNoLicenseMock = getServerMock();
|
||||
serverWithNoLicenseMock.plugins.xpack_main.info.isAvailable = jest.fn().mockReturnValue(false);
|
||||
|
||||
const { features, licensing, usage } = setup({ license: { isAvailable: false } });
|
||||
const { fetch: getSpacesUsage } = getSpacesUsageCollector({
|
||||
kibanaIndex: '.kibana',
|
||||
usage: serverWithNoLicenseMock.usage,
|
||||
xpackMain: serverWithNoLicenseMock.plugins.xpack_main,
|
||||
usage,
|
||||
features,
|
||||
licensing,
|
||||
});
|
||||
usageStats = await getSpacesUsage(defaultCallClusterMock);
|
||||
});
|
||||
|
@ -142,17 +134,16 @@ describe('with no license', () => {
|
|||
});
|
||||
|
||||
describe('with platinum license', () => {
|
||||
let serverWithPlatinumLicenseMock: any;
|
||||
let usageStats: UsageStats;
|
||||
beforeAll(async () => {
|
||||
serverWithPlatinumLicenseMock = getServerMock();
|
||||
serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = jest
|
||||
.fn()
|
||||
.mockReturnValue('platinum');
|
||||
const { features, licensing, usage } = setup({
|
||||
license: { isAvailable: true, type: 'platinum' },
|
||||
});
|
||||
const { fetch: getSpacesUsage } = getSpacesUsageCollector({
|
||||
kibanaIndex: '.kibana',
|
||||
usage: serverWithPlatinumLicenseMock.usage,
|
||||
xpackMain: serverWithPlatinumLicenseMock.plugins.xpack_main,
|
||||
usage,
|
||||
features,
|
||||
licensing,
|
||||
});
|
||||
usageStats = await getSpacesUsage(defaultCallClusterMock);
|
||||
});
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import { get } from 'lodash';
|
||||
import { CallAPIOptions } from 'src/core/server';
|
||||
import { XPackMainPlugin } from '../../../xpack_main/xpack_main';
|
||||
import { take } from 'rxjs/operators';
|
||||
// @ts-ignore
|
||||
import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants';
|
||||
import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants';
|
||||
import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants';
|
||||
import { PluginsSetup } from '../plugin';
|
||||
|
||||
type CallCluster = <T = unknown>(
|
||||
endpoint: string,
|
||||
|
@ -30,22 +31,23 @@ interface SpacesAggregationResponse {
|
|||
|
||||
/**
|
||||
*
|
||||
* @param callCluster
|
||||
* @param server
|
||||
* @param {CallCluster} callCluster
|
||||
* @param {string} kibanaIndex
|
||||
* @param {PluginsSetup['features']} features
|
||||
* @param {boolean} spacesAvailable
|
||||
* @return {UsageStats}
|
||||
*/
|
||||
async function getSpacesUsage(
|
||||
callCluster: CallCluster,
|
||||
kibanaIndex: string,
|
||||
xpackMainPlugin: XPackMainPlugin,
|
||||
features: PluginsSetup['features'],
|
||||
spacesAvailable: boolean
|
||||
) {
|
||||
if (!spacesAvailable) {
|
||||
return {} as UsageStats;
|
||||
}
|
||||
|
||||
const knownFeatureIds = xpackMainPlugin.getFeatures().map(feature => feature.id);
|
||||
const knownFeatureIds = features.getFeatures().map(feature => feature.id);
|
||||
|
||||
const resp = await callCluster<SpacesAggregationResponse>('search', {
|
||||
index: kibanaIndex,
|
||||
|
@ -115,7 +117,8 @@ export interface UsageStats {
|
|||
interface CollectorDeps {
|
||||
kibanaIndex: string;
|
||||
usage: { collectorSet: any };
|
||||
xpackMain: XPackMainPlugin;
|
||||
features: PluginsSetup['features'];
|
||||
licensing: PluginsSetup['licensing'];
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -128,13 +131,13 @@ export function getSpacesUsageCollector(deps: CollectorDeps) {
|
|||
type: KIBANA_SPACES_STATS_TYPE,
|
||||
isReady: () => true,
|
||||
fetch: async (callCluster: CallCluster) => {
|
||||
const xpackInfo = deps.xpackMain.info;
|
||||
const available = xpackInfo && xpackInfo.isAvailable(); // some form of spaces is available for all valid licenses
|
||||
const license = await deps.licensing.license$.pipe(take(1)).toPromise();
|
||||
const available = license.isAvailable; // some form of spaces is available for all valid licenses
|
||||
|
||||
const usageStats = await getSpacesUsage(
|
||||
callCluster,
|
||||
deps.kibanaIndex,
|
||||
deps.xpackMain,
|
||||
deps.features,
|
||||
available
|
||||
);
|
||||
|
7
x-pack/plugins/spaces/server/lib/migrations/index.ts
Normal file
7
x-pack/plugins/spaces/server/lib/migrations/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { migrateToKibana660 } from './migrate_6x';
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { migrateToKibana660 } from './migrate_6x';
|
||||
|
||||
describe('migrateTo660', () => {
|
||||
it('adds a "disabledFeatures" attribute initialized as an empty array', () => {
|
||||
expect(
|
||||
migrateToKibana660({
|
||||
id: 'space:foo',
|
||||
attributes: {},
|
||||
})
|
||||
).toEqual({
|
||||
id: 'space:foo',
|
||||
attributes: {
|
||||
disabledFeatures: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not initialize "disabledFeatures" if the property already exists', () => {
|
||||
// This scenario shouldn't happen organically. Protecting against defects in the migration.
|
||||
expect(
|
||||
migrateToKibana660({
|
||||
id: 'space:foo',
|
||||
attributes: {
|
||||
disabledFeatures: ['foo', 'bar', 'baz'],
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
id: 'space:foo',
|
||||
attributes: {
|
||||
disabledFeatures: ['foo', 'bar', 'baz'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
12
x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts
Normal file
12
x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export function migrateToKibana660(doc: Record<string, any>) {
|
||||
if (!doc.attributes.hasOwnProperty('disabledFeatures')) {
|
||||
doc.attributes.disabledFeatures = [];
|
||||
}
|
||||
return doc;
|
||||
}
|
|
@ -14,26 +14,25 @@ import {
|
|||
CoreSetup,
|
||||
SavedObjectsLegacyService,
|
||||
SavedObjectsErrorHelpers,
|
||||
} from '../../../../../../../src/core/server';
|
||||
} from '../../../../../../src/core/server';
|
||||
import {
|
||||
elasticsearchServiceMock,
|
||||
loggingServiceMock,
|
||||
} from '../../../../../../../src/core/server/mocks';
|
||||
import * as kbnTestServer from '../../../../../../../src/test_utils/kbn_server';
|
||||
import { LegacyAPI } from '../../new_platform/plugin';
|
||||
import { SpacesService } from '../../new_platform/spaces_service';
|
||||
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server';
|
||||
import { LegacyAPI, PluginsSetup } from '../../plugin';
|
||||
import { SpacesService } from '../../spaces_service';
|
||||
import { SpacesAuditLogger } from '../audit_logger';
|
||||
import { SecurityPlugin } from '../../../../security';
|
||||
import { convertSavedObjectToSpace } from '../../routes/lib';
|
||||
import { XPackMainPlugin } from '../../../../xpack_main/xpack_main';
|
||||
import { Feature } from '../../../../../../plugins/features/server';
|
||||
import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor';
|
||||
import { Feature } from '../../../../features/server';
|
||||
import { OptionalPlugin } from '../../../../../legacy/server/lib/optional_plugin';
|
||||
import { SecurityPlugin } from '../../../../../legacy/plugins/security';
|
||||
import { spacesConfig } from '../__fixtures__';
|
||||
|
||||
describe('onPostAuthInterceptor', () => {
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
|
||||
const defaultRoute = '/app/kibana';
|
||||
const headers = {
|
||||
authorization: `Basic ${Buffer.from(
|
||||
`${kibanaTestUser.username}:${kibanaTestUser.password}`
|
||||
|
@ -117,7 +116,7 @@ describe('onPostAuthInterceptor', () => {
|
|||
.asLoggerFactory()
|
||||
.get('xpack', 'spaces');
|
||||
|
||||
const xpackMainPlugin = {
|
||||
const featuresPlugin = {
|
||||
getFeatures: () =>
|
||||
[
|
||||
{
|
||||
|
@ -141,7 +140,7 @@ describe('onPostAuthInterceptor', () => {
|
|||
app: ['kibana'],
|
||||
},
|
||||
] as Feature[],
|
||||
} as XPackMainPlugin;
|
||||
} as PluginsSetup['features'];
|
||||
|
||||
const savedObjectsService = {
|
||||
SavedObjectsClient: {
|
||||
|
@ -164,10 +163,6 @@ describe('onPostAuthInterceptor', () => {
|
|||
};
|
||||
|
||||
const legacyAPI = {
|
||||
legacyConfig: {
|
||||
serverDefaultRoute: defaultRoute,
|
||||
serverBasePath: '',
|
||||
},
|
||||
savedObjects: (savedObjectsService as unknown) as SavedObjectsLegacyService,
|
||||
} as LegacyAPI;
|
||||
|
||||
|
@ -176,9 +171,9 @@ describe('onPostAuthInterceptor', () => {
|
|||
const spacesService = await service.setup({
|
||||
http: (http as unknown) as CoreSetup['http'],
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
security: {} as OptionalPlugin<SecurityPlugin>,
|
||||
getSecurity: () => ({} as OptionalPlugin<SecurityPlugin>),
|
||||
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
|
||||
config$: Rx.of({ maxSpaces: 1000 }),
|
||||
config$: Rx.of(spacesConfig),
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn().mockResolvedValue({
|
||||
|
@ -212,7 +207,7 @@ describe('onPostAuthInterceptor', () => {
|
|||
getLegacyAPI: () => legacyAPI,
|
||||
http: (http as unknown) as CoreSetup['http'],
|
||||
log: loggingMock,
|
||||
xpackMain: xpackMainPlugin,
|
||||
features: featuresPlugin,
|
||||
spacesService,
|
||||
});
|
||||
|
|
@ -6,9 +6,8 @@
|
|||
import { Logger, CoreSetup } from 'src/core/server';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { wrapError } from '../errors';
|
||||
import { XPackMainPlugin } from '../../../../xpack_main/xpack_main';
|
||||
import { SpacesServiceSetup } from '../../new_platform/spaces_service/spaces_service';
|
||||
import { LegacyAPI } from '../../new_platform/plugin';
|
||||
import { SpacesServiceSetup } from '../../spaces_service/spaces_service';
|
||||
import { LegacyAPI, PluginsSetup } from '../../plugin';
|
||||
import { getSpaceSelectorUrl } from '../get_space_selector_url';
|
||||
import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants';
|
||||
import { addSpaceIdToPath } from '../../../common';
|
||||
|
@ -16,21 +15,21 @@ import { addSpaceIdToPath } from '../../../common';
|
|||
export interface OnPostAuthInterceptorDeps {
|
||||
getLegacyAPI(): LegacyAPI;
|
||||
http: CoreSetup['http'];
|
||||
xpackMain: XPackMainPlugin;
|
||||
features: PluginsSetup['features'];
|
||||
spacesService: SpacesServiceSetup;
|
||||
log: Logger;
|
||||
}
|
||||
|
||||
export function initSpacesOnPostAuthRequestInterceptor({
|
||||
xpackMain,
|
||||
features,
|
||||
getLegacyAPI,
|
||||
spacesService,
|
||||
log,
|
||||
http,
|
||||
}: OnPostAuthInterceptorDeps) {
|
||||
const { serverBasePath } = getLegacyAPI().legacyConfig;
|
||||
|
||||
http.registerOnPostAuth(async (request, response, toolkit) => {
|
||||
const serverBasePath = http.basePath.serverBasePath;
|
||||
|
||||
const path = request.url.pathname!;
|
||||
|
||||
const spaceId = spacesService.getSpaceId(request);
|
||||
|
@ -66,12 +65,7 @@ export function initSpacesOnPostAuthRequestInterceptor({
|
|||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const wrappedError = wrapError(error);
|
||||
return response.customError({
|
||||
body: wrappedError,
|
||||
headers: wrappedError.output.headers,
|
||||
statusCode: wrappedError.output.statusCode,
|
||||
});
|
||||
return response.customError(wrapError(error));
|
||||
}
|
||||
} else if (isRequestingSpaceRoot) {
|
||||
const destination = addSpaceIdToPath(serverBasePath, spaceId, ENTER_SPACE_PATH);
|
||||
|
@ -89,7 +83,7 @@ export function initSpacesOnPostAuthRequestInterceptor({
|
|||
} catch (error) {
|
||||
const wrappedError = wrapError(error);
|
||||
|
||||
const statusCode = wrappedError.output.statusCode;
|
||||
const statusCode = wrappedError.statusCode;
|
||||
|
||||
// If user is not authorized, or the space cannot be found, allow them to select another space
|
||||
// by redirecting to the space selector.
|
||||
|
@ -106,11 +100,7 @@ export function initSpacesOnPostAuthRequestInterceptor({
|
|||
});
|
||||
} else {
|
||||
log.error(`Unable to navigate to space "${spaceId}". ${error}`);
|
||||
return response.customError({
|
||||
body: wrappedError,
|
||||
headers: wrappedError.output.headers,
|
||||
statusCode: wrappedError.output.statusCode,
|
||||
});
|
||||
return response.customError(wrappedError);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,7 +110,7 @@ export function initSpacesOnPostAuthRequestInterceptor({
|
|||
if (appId !== 'kibana' && space && space.disabledFeatures.length > 0) {
|
||||
log.debug(`Verifying application is available: "${appId}"`);
|
||||
|
||||
const allFeatures = xpackMain.getFeatures();
|
||||
const allFeatures = features.getFeatures();
|
||||
|
||||
const isRegisteredApp = allFeatures.some(feature => feature.app.includes(appId));
|
||||
if (isRegisteredApp) {
|
|
@ -12,10 +12,10 @@ import {
|
|||
KibanaRequest,
|
||||
KibanaResponseFactory,
|
||||
CoreSetup,
|
||||
} from '../../../../../../../src/core/server';
|
||||
} from '../../../../../../src/core/server';
|
||||
|
||||
import * as kbnTestServer from '../../../../../../../src/test_utils/kbn_server';
|
||||
import { LegacyAPI } from '../../new_platform/plugin';
|
||||
import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server';
|
||||
import { LegacyAPI } from '../../plugin';
|
||||
|
||||
describe('onRequestInterceptor', () => {
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
|
@ -110,9 +110,7 @@ describe('onRequestInterceptor', () => {
|
|||
initSpacesOnRequestInterceptor({
|
||||
getLegacyAPI: () =>
|
||||
({
|
||||
legacyConfig: {
|
||||
serverBasePath: opts.basePath,
|
||||
},
|
||||
legacyConfig: {},
|
||||
} as LegacyAPI),
|
||||
http: (http as unknown) as CoreSetup['http'],
|
||||
});
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { format } from 'url';
|
||||
import { DEFAULT_SPACE_ID } from '../../../common/constants';
|
||||
import { modifyUrl } from '../utils/url';
|
||||
import { LegacyAPI } from '../../new_platform/plugin';
|
||||
import { LegacyAPI } from '../../plugin';
|
||||
import { getSpaceIdFromPath } from '../../../common';
|
||||
|
||||
export interface OnRequestInterceptorDeps {
|
||||
|
@ -25,7 +25,7 @@ export function initSpacesOnRequestInterceptor({ getLegacyAPI, http }: OnRequest
|
|||
response: LifecycleResponseFactory,
|
||||
toolkit: OnPreAuthToolkit
|
||||
) {
|
||||
const { serverBasePath } = getLegacyAPI().legacyConfig;
|
||||
const serverBasePath = http.basePath.serverBasePath;
|
||||
const path = request.url.pathname;
|
||||
|
||||
// If navigating within the context of a space, then we store the Space's URL Context on the request,
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { SavedObjectsClientWrapperFactory } from 'src/core/server';
|
||||
import { SpacesSavedObjectsClient } from './spaces_saved_objects_client';
|
||||
import { SpacesServiceSetup } from '../../new_platform/spaces_service/spaces_service';
|
||||
import { SpacesServiceSetup } from '../../spaces_service/spaces_service';
|
||||
|
||||
export function spacesSavedObjectsClientWrapperFactory(
|
||||
spacesService: SpacesServiceSetup,
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { DEFAULT_SPACE_ID } from '../../../common/constants';
|
||||
import { SpacesSavedObjectsClient } from './spaces_saved_objects_client';
|
||||
import { spacesServiceMock } from '../../new_platform/spaces_service/spaces_service.mock';
|
||||
import { spacesServiceMock } from '../../spaces_service/spaces_service.mock';
|
||||
|
||||
const types = ['foo', 'bar', 'space'];
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
SavedObjectsFindOptions,
|
||||
SavedObjectsUpdateOptions,
|
||||
} from 'src/core/server';
|
||||
import { SpacesServiceSetup } from '../../new_platform/spaces_service/spaces_service';
|
||||
import { SpacesServiceSetup } from '../../spaces_service/spaces_service';
|
||||
import { spaceIdToNamespace } from '../utils/namespace';
|
||||
|
||||
interface SpacesSavedObjectsClientOptions {
|
231
x-pack/plugins/spaces/server/lib/space_schema.test.ts
Normal file
231
x-pack/plugins/spaces/server/lib/space_schema.test.ts
Normal file
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { spaceSchema } from './space_schema';
|
||||
|
||||
const defaultProperties = {
|
||||
id: 'foo',
|
||||
name: 'foo',
|
||||
};
|
||||
|
||||
describe('#id', () => {
|
||||
test('is required', () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: undefined,
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[id]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('allows lowercase a-z, 0-9, "_" and "-"', () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: 'abcdefghijklmnopqrstuvwxyz0123456789_-',
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test(`doesn't allow uppercase`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: 'Foo',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[id]: must be lower case, a-z, 0-9, '_', and '-' are allowed"`
|
||||
);
|
||||
});
|
||||
|
||||
test(`doesn't allow an empty string`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: '',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[id]: must be lower case, a-z, 0-9, '_', and '-' are allowed"`
|
||||
);
|
||||
});
|
||||
|
||||
['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', ',', '.', '/', '?'].forEach(
|
||||
invalidCharacter => {
|
||||
test(`doesn't allow ${invalidCharacter}`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
id: `foo-${invalidCharacter}`,
|
||||
})
|
||||
).toThrowError();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('#disabledFeatures', () => {
|
||||
test('is optional', () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
disabledFeatures: undefined,
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('defaults to an empty array', () => {
|
||||
const result = spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
disabledFeatures: undefined,
|
||||
});
|
||||
expect(result.disabledFeatures).toEqual([]);
|
||||
});
|
||||
|
||||
test('must be an array if provided', () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
disabledFeatures: 'foo',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[disabledFeatures]: expected value of type [array] but got [string]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('allows an array of strings', () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
disabledFeatures: ['foo', 'bar'],
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('does not allow an array containing non-string elements', () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
disabledFeatures: ['foo', true],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[disabledFeatures.1]: expected value of type [string] but got [boolean]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#color', () => {
|
||||
test('is optional', () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: undefined,
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test(`doesn't allow an empty string`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[color]: must be a 6 digit hex color, starting with a #"`
|
||||
);
|
||||
});
|
||||
|
||||
test(`allows lower case hex color code`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '#aabbcc',
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test(`allows upper case hex color code`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '#AABBCC',
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test(`allows numeric hex color code`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '#123456',
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test(`must start with a hash`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '123456',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[color]: must be a 6 digit hex color, starting with a #"`
|
||||
);
|
||||
});
|
||||
|
||||
test(`cannot exceed 6 digits following the hash`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '1234567',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[color]: must be a 6 digit hex color, starting with a #"`
|
||||
);
|
||||
});
|
||||
|
||||
test(`cannot be fewer than 6 digits following the hash`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
color: '12345',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[color]: must be a 6 digit hex color, starting with a #"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#imageUrl', () => {
|
||||
test('is optional', () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
imageUrl: undefined,
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test(`must start with data:image`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
imageUrl: 'notValid',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[imageUrl]: must start with 'data:image'"`);
|
||||
});
|
||||
|
||||
test(`checking that a valid image is accepted as imageUrl`, () => {
|
||||
expect(() =>
|
||||
spaceSchema.validate({
|
||||
...defaultProperties,
|
||||
imageUrl:
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTnU1rJkAAAB3klEQVRYR+2WzUrDQBCARzwqehE8ir1WPfgqRRA1bePBXgpe/MGCB9/Aiw+j+ASCB6kotklaEwW1F0WwNSaps9lV69awGzBpDzt8pJP9mXxsmk3ABH2oUEIilJAIJSRCCYlQQiKUkIh4QgY5agZodVjBowFrBktWQzDBU2ykiYaDuQpCYgnl3QunGzM6Z6YF+b5SkcgK1UH/aLbYReQiYL9d9/o+XFop5IU0Vl4uapAzoXC3eEBPw9vH1/wT6Vs2otPSkoH/IZzlzO/TU2vgQm8nl69Hp0H7nZ4OXogLJSSKBIUC3w88n+Ueyfv56fVZnqCQNVnCHbLrkV0Gd2d+GNkglsk438dhaTxloZDutV4wb06Vf40JcWZ2sMttPpE8NaHGeBnzIAhwPXqHseVB11EyLD0hxLUeaYud2a3B0g3k7GyFtrhX7F2RqhC+yV3jgTb2Rqdqf7/kUxYiWBOlTtXxfPJEtc8b5thGb+8AhL4ohnCNqQjZ2T2+K5rnw2M6KwEhKNDSGM3pTdxjhDgLbHkw/v/zw4AiPuSsfMzAiTidKxiF/ArpFqyzK8SMOlkwvloUMYRCtNvZLWeuIomd2Za/WZS4QomjhEQoIRFKSIQSEqGERAyfEH4YDBFQ/ARU6BiBxCAIQQAAAABJRU5ErkJggg==',
|
||||
})
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
43
x-pack/plugins/spaces/server/lib/space_schema.ts
Normal file
43
x-pack/plugins/spaces/server/lib/space_schema.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { MAX_SPACE_INITIALS } from '../../common';
|
||||
|
||||
export const SPACE_ID_REGEX = /^[a-z0-9_\-]+$/;
|
||||
|
||||
export const spaceSchema = schema.object({
|
||||
id: schema.string({
|
||||
validate: value => {
|
||||
if (!SPACE_ID_REGEX.test(value)) {
|
||||
return `must be lower case, a-z, 0-9, '_', and '-' are allowed`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
name: schema.string({ minLength: 1 }),
|
||||
description: schema.maybe(schema.string()),
|
||||
initials: schema.maybe(schema.string({ maxLength: MAX_SPACE_INITIALS })),
|
||||
color: schema.maybe(
|
||||
schema.string({
|
||||
validate: value => {
|
||||
if (!/^#[a-zA-Z0-9]{6}$/.test(value)) {
|
||||
return `must be a 6 digit hex color, starting with a #`;
|
||||
}
|
||||
},
|
||||
})
|
||||
),
|
||||
disabledFeatures: schema.arrayOf(schema.string(), { defaultValue: [] }),
|
||||
_reserved: schema.maybe(schema.boolean()),
|
||||
imageUrl: schema.maybe(
|
||||
schema.string({
|
||||
validate: value => {
|
||||
if (value !== '' && !/^data:image.*$/.test(value)) {
|
||||
return `must start with 'data:image'`;
|
||||
}
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
|
@ -29,7 +29,7 @@ const createSpacesClientMock = () =>
|
|||
create: jest.fn().mockImplementation((space: Space) => Promise.resolve(space)),
|
||||
update: jest.fn().mockImplementation((space: Space) => Promise.resolve(space)),
|
||||
delete: jest.fn(),
|
||||
} as unknown) as SpacesClient);
|
||||
} as unknown) as jest.Mocked<SpacesClient>);
|
||||
|
||||
export const spacesClientMock = {
|
||||
create: createSpacesClientMock,
|
|
@ -5,9 +5,9 @@
|
|||
*/
|
||||
|
||||
import { SpacesClient } from './spaces_client';
|
||||
import { AuthorizationService } from '../../../../security/server/lib/authorization/service';
|
||||
import { actionsFactory } from '../../../../security/server/lib/authorization/actions';
|
||||
import { SpacesConfigType, config } from '../../new_platform/config';
|
||||
import { AuthorizationService } from '../../../../../legacy/plugins/security/server/lib/authorization/service';
|
||||
import { actionsFactory } from '../../../../../legacy/plugins/security/server/lib/authorization/actions';
|
||||
import { ConfigType, ConfigSchema } from '../../config';
|
||||
import { GetSpacePurpose } from '../../../common/model/types';
|
||||
|
||||
const createMockAuditLogger = () => {
|
||||
|
@ -69,8 +69,8 @@ const createMockAuthorization = () => {
|
|||
};
|
||||
};
|
||||
|
||||
const createMockConfig = (mockConfig: SpacesConfigType = { maxSpaces: 1000 }) => {
|
||||
return config.schema.validate(mockConfig);
|
||||
const createMockConfig = (mockConfig: ConfigType = { maxSpaces: 1000, enabled: true }) => {
|
||||
return ConfigSchema.validate(mockConfig);
|
||||
};
|
||||
|
||||
describe('#getAll', () => {
|
||||
|
@ -123,6 +123,7 @@ describe('#getAll', () => {
|
|||
const maxSpaces = 1234;
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const client = new SpacesClient(
|
||||
|
@ -162,6 +163,7 @@ describe('#getAll', () => {
|
|||
const maxSpaces = 1234;
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
enabled: true,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
||||
|
@ -287,6 +289,7 @@ describe('#getAll', () => {
|
|||
const maxSpaces = 1234;
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
enabled: true,
|
||||
});
|
||||
const mockInternalRepository = {
|
||||
find: jest.fn().mockReturnValue({
|
||||
|
@ -355,6 +358,7 @@ describe('#getAll', () => {
|
|||
const maxSpaces = 1234;
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
enabled: true,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
||||
|
@ -725,6 +729,7 @@ describe('#create', () => {
|
|||
};
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces,
|
||||
enabled: true,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
||||
|
@ -766,6 +771,7 @@ describe('#create', () => {
|
|||
};
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces,
|
||||
enabled: true,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
||||
|
@ -807,6 +813,7 @@ describe('#create', () => {
|
|||
};
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces,
|
||||
enabled: true,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
||||
|
@ -850,6 +857,7 @@ describe('#create', () => {
|
|||
};
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces,
|
||||
enabled: true,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
||||
|
@ -931,6 +939,7 @@ describe('#create', () => {
|
|||
};
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces,
|
||||
enabled: true,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
||||
|
@ -983,6 +992,7 @@ describe('#create', () => {
|
|||
};
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces,
|
||||
enabled: true,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
|
@ -7,11 +7,11 @@ import Boom from 'boom';
|
|||
import { omit } from 'lodash';
|
||||
import { Legacy } from 'kibana';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { AuthorizationService } from '../../../../security/server/lib/authorization/service';
|
||||
import { AuthorizationService } from '../../../../../legacy/plugins/security/server/lib/authorization/service';
|
||||
import { isReservedSpace } from '../../../common/is_reserved_space';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { SpacesAuditLogger } from '../audit_logger';
|
||||
import { SpacesConfigType } from '../../new_platform/config';
|
||||
import { ConfigType } from '../../config';
|
||||
import { GetSpacePurpose } from '../../../common/model/types';
|
||||
|
||||
type SpacesClientRequestFacade = Legacy.Request | KibanaRequest;
|
||||
|
@ -33,7 +33,7 @@ export class SpacesClient {
|
|||
private readonly debugLogger: (message: string) => void,
|
||||
private readonly authorization: AuthorizationService | null,
|
||||
private readonly callWithRequestSavedObjectRepository: any,
|
||||
private readonly config: SpacesConfigType,
|
||||
private readonly config: ConfigType,
|
||||
private readonly internalSavedObjectRepository: any,
|
||||
private readonly request: SpacesClientRequestFacade
|
||||
) {}
|
|
@ -7,13 +7,14 @@
|
|||
import * as Rx from 'rxjs';
|
||||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
import { createSpacesTutorialContextFactory } from './spaces_tutorial_context_factory';
|
||||
import { SpacesService } from '../new_platform/spaces_service';
|
||||
import { SpacesService } from '../spaces_service';
|
||||
import { SavedObjectsLegacyService } from 'src/core/server';
|
||||
import { SpacesAuditLogger } from './audit_logger';
|
||||
import { elasticsearchServiceMock, coreMock } from '../../../../../../src/core/server/mocks';
|
||||
import { spacesServiceMock } from '../new_platform/spaces_service/spaces_service.mock';
|
||||
import { createOptionalPlugin } from '../../../../server/lib/optional_plugin';
|
||||
import { LegacyAPI } from '../new_platform/plugin';
|
||||
import { elasticsearchServiceMock, coreMock } from '../../../../../src/core/server/mocks';
|
||||
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
|
||||
import { createOptionalPlugin } from '../../../../legacy/server/lib/optional_plugin';
|
||||
import { LegacyAPI } from '../plugin';
|
||||
import { spacesConfig } from './__fixtures__';
|
||||
|
||||
const log = {
|
||||
log: jest.fn(),
|
||||
|
@ -26,9 +27,7 @@ const log = {
|
|||
};
|
||||
|
||||
const legacyAPI: LegacyAPI = {
|
||||
legacyConfig: {
|
||||
serverBasePath: '/foo',
|
||||
},
|
||||
legacyConfig: {},
|
||||
savedObjects: {} as SavedObjectsLegacyService,
|
||||
} as LegacyAPI;
|
||||
|
||||
|
@ -56,9 +55,10 @@ describe('createSpacesTutorialContextFactory', () => {
|
|||
const spacesService = await service.setup({
|
||||
http: coreMock.createSetup().http,
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
security: createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
|
||||
getSecurity: () =>
|
||||
createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
|
||||
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
|
||||
config$: Rx.of({ maxSpaces: 1000 }),
|
||||
config$: Rx.of(spacesConfig),
|
||||
});
|
||||
const contextFactory = createSpacesTutorialContextFactory(spacesService);
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesServiceSetup } from '../new_platform/spaces_service/spaces_service';
|
||||
import { SpacesServiceSetup } from '../spaces_service/spaces_service';
|
||||
|
||||
export function createSpacesTutorialContextFactory(spacesService: SpacesServiceSetup) {
|
||||
return function spacesTutorialContextFactory(request: any) {
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Feature } from '../../../../../plugins/features/server';
|
||||
import { Feature } from '../../../../plugins/features/server';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { toggleUICapabilities } from './toggle_ui_capabilities';
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import _ from 'lodash';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Feature } from '../../../../../plugins/features/server';
|
||||
import { Feature } from '../../../../plugins/features/server';
|
||||
import { Space } from '../../common/model/space';
|
||||
|
||||
export function toggleUICapabilities(
|
|
@ -5,30 +5,33 @@
|
|||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { SavedObjectsLegacyService, CoreSetup } from 'src/core/server';
|
||||
import { Logger, PluginInitializerContext } from 'src/core/server';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { CapabilitiesModifier } from 'src/legacy/server/capabilities';
|
||||
import { Legacy } from 'kibana';
|
||||
import { OptionalPlugin } from '../../../../server/lib/optional_plugin';
|
||||
import { XPackMainPlugin } from '../../../xpack_main/xpack_main';
|
||||
import { createDefaultSpace } from '../lib/create_default_space';
|
||||
import {
|
||||
SavedObjectsLegacyService,
|
||||
CoreSetup,
|
||||
KibanaRequest,
|
||||
Logger,
|
||||
PluginInitializerContext,
|
||||
} from '../../../../src/core/server';
|
||||
import { SecurityPlugin } from '../../../legacy/plugins/security';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { OptionalPlugin } from '../../../legacy/server/lib/optional_plugin';
|
||||
import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/xpack_main';
|
||||
import { createDefaultSpace } from './lib/create_default_space';
|
||||
// @ts-ignore
|
||||
import { AuditLogger } from '../../../../server/lib/audit_logger';
|
||||
// @ts-ignore
|
||||
import { watchStatusAndLicenseToInitialize } from '../../../../server/lib/watch_status_and_license_to_initialize';
|
||||
import { checkLicense } from '../lib/check_license';
|
||||
import { spacesSavedObjectsClientWrapperFactory } from '../lib/saved_objects_client/saved_objects_client_wrapper_factory';
|
||||
import { SpacesAuditLogger } from '../lib/audit_logger';
|
||||
import { createSpacesTutorialContextFactory } from '../lib/spaces_tutorial_context_factory';
|
||||
import { initExternalSpacesApi } from '../routes/api/external';
|
||||
import { getSpacesUsageCollector } from '../lib/get_spaces_usage_collector';
|
||||
import { spacesSavedObjectsClientWrapperFactory } from './lib/saved_objects_client/saved_objects_client_wrapper_factory';
|
||||
import { SpacesAuditLogger } from './lib/audit_logger';
|
||||
import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory';
|
||||
import { getSpacesUsageCollector } from './lib/get_spaces_usage_collector';
|
||||
import { SpacesService } from './spaces_service';
|
||||
import { SecurityPlugin } from '../../../security';
|
||||
import { SpacesServiceSetup } from './spaces_service/spaces_service';
|
||||
import { SpacesConfigType } from './config';
|
||||
import { getActiveSpace } from '../lib/get_active_space';
|
||||
import { toggleUICapabilities } from '../lib/toggle_ui_capabilities';
|
||||
import { initSpacesRequestInterceptors } from '../lib/request_interceptors';
|
||||
import { ConfigType } from './config';
|
||||
import { toggleUICapabilities } from './lib/toggle_ui_capabilities';
|
||||
import { initSpacesRequestInterceptors } from './lib/request_interceptors';
|
||||
import { initExternalSpacesApi } from './routes/api/external';
|
||||
|
||||
/**
|
||||
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
|
||||
|
@ -52,30 +55,33 @@ export interface LegacyAPI {
|
|||
};
|
||||
legacyConfig: {
|
||||
kibanaIndex: string;
|
||||
serverBasePath: string;
|
||||
serverDefaultRoute: string;
|
||||
};
|
||||
router: Legacy.Server['route'];
|
||||
}
|
||||
|
||||
export interface PluginsSetup {
|
||||
xpackMain: XPackMainPlugin;
|
||||
// TODO: Spaces has a circular dependency with Security right now.
|
||||
// Security is not yet available when init runs, so this is wrapped in an optional plugin for the time being.
|
||||
security: OptionalPlugin<SecurityPlugin>;
|
||||
xpackMain: XPackMainPlugin;
|
||||
// TODO: this is temporary for `watchLicenseAndStatusToInitialize`
|
||||
spaces: any;
|
||||
}
|
||||
|
||||
export interface PluginsSetup {
|
||||
features: FeaturesPluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
export interface SpacesPluginSetup {
|
||||
spacesService: SpacesServiceSetup;
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
|
||||
__legacyCompat: {
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
|
||||
// TODO: We currently need the legacy plugin to inform this plugin when it is safe to create the default space.
|
||||
// The NP does not have the equivilent ES connection/health/comapt checks that the legacy world does.
|
||||
// See: https://github.com/elastic/kibana/issues/43456
|
||||
createDefaultSpace: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
export class Plugin {
|
||||
private readonly pluginId = 'spaces';
|
||||
|
||||
private readonly config$: Observable<SpacesConfigType>;
|
||||
private readonly config$: Observable<ConfigType>;
|
||||
|
||||
private readonly log: Logger;
|
||||
|
||||
|
@ -98,91 +104,29 @@ export class Plugin {
|
|||
};
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config$ = initializerContext.config.create<SpacesConfigType>();
|
||||
this.config$ = initializerContext.config.create<ConfigType>();
|
||||
this.log = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public async start() {}
|
||||
|
||||
public async setup(core: CoreSetup, plugins: PluginsSetup): Promise<SpacesPluginSetup> {
|
||||
const xpackMainPlugin: XPackMainPlugin = plugins.xpackMain;
|
||||
watchStatusAndLicenseToInitialize(xpackMainPlugin, plugins.spaces, async () => {
|
||||
await createDefaultSpace({
|
||||
elasticsearch: core.elasticsearch,
|
||||
savedObjects: this.getLegacyAPI().savedObjects,
|
||||
});
|
||||
});
|
||||
|
||||
// Register a function that is called whenever the xpack info changes,
|
||||
// to re-compute the license check results for this plugin.
|
||||
xpackMainPlugin.info.feature(this.pluginId).registerLicenseCheckResultsGenerator(checkLicense);
|
||||
|
||||
const service = new SpacesService(this.log, this.getLegacyAPI);
|
||||
|
||||
const spacesService = await service.setup({
|
||||
http: core.http,
|
||||
elasticsearch: core.elasticsearch,
|
||||
security: plugins.security,
|
||||
getSecurity: () => this.getLegacyAPI().security,
|
||||
getSpacesAuditLogger: this.getSpacesAuditLogger,
|
||||
config$: this.config$,
|
||||
});
|
||||
|
||||
return {
|
||||
spacesService,
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
|
||||
this.legacyAPI = legacyAPI;
|
||||
this.setupLegacyComponents(core, spacesService, plugins.xpackMain);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private setupLegacyComponents(
|
||||
core: CoreSetup,
|
||||
spacesService: SpacesServiceSetup,
|
||||
xpackMainPlugin: XPackMainPlugin
|
||||
) {
|
||||
const legacyAPI = this.getLegacyAPI();
|
||||
|
||||
const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects;
|
||||
addScopedSavedObjectsClientWrapperFactory(
|
||||
Number.MIN_SAFE_INTEGER,
|
||||
'spaces',
|
||||
spacesSavedObjectsClientWrapperFactory(spacesService, types)
|
||||
);
|
||||
|
||||
legacyAPI.tutorial.addScopedTutorialContextFactory(
|
||||
createSpacesTutorialContextFactory(spacesService)
|
||||
);
|
||||
|
||||
legacyAPI.capabilities.registerCapabilitiesModifier(async (request, uiCapabilities) => {
|
||||
const spacesClient = await spacesService.scopedClient(request);
|
||||
try {
|
||||
const activeSpace = await getActiveSpace(
|
||||
spacesClient,
|
||||
core.http.basePath.get(request),
|
||||
legacyAPI.legacyConfig.serverBasePath
|
||||
);
|
||||
|
||||
const features = xpackMainPlugin.getFeatures();
|
||||
return toggleUICapabilities(features, uiCapabilities, activeSpace);
|
||||
} catch (e) {
|
||||
return uiCapabilities;
|
||||
}
|
||||
});
|
||||
|
||||
// Register a function with server to manage the collection of usage stats
|
||||
legacyAPI.usage.collectorSet.register(
|
||||
getSpacesUsageCollector({
|
||||
kibanaIndex: legacyAPI.legacyConfig.kibanaIndex,
|
||||
usage: legacyAPI.usage,
|
||||
xpackMain: xpackMainPlugin,
|
||||
})
|
||||
);
|
||||
|
||||
const externalRouter = core.http.createRouter();
|
||||
initExternalSpacesApi({
|
||||
legacyRouter: legacyAPI.router,
|
||||
externalRouter,
|
||||
log: this.log,
|
||||
savedObjects: legacyAPI.savedObjects,
|
||||
getSavedObjects: () => this.getLegacyAPI().savedObjects,
|
||||
spacesService,
|
||||
xpackMain: xpackMainPlugin,
|
||||
});
|
||||
|
||||
initSpacesRequestInterceptors({
|
||||
|
@ -190,7 +134,61 @@ export class Plugin {
|
|||
log: this.log,
|
||||
getLegacyAPI: this.getLegacyAPI,
|
||||
spacesService,
|
||||
xpackMain: xpackMainPlugin,
|
||||
features: plugins.features,
|
||||
});
|
||||
|
||||
return {
|
||||
spacesService,
|
||||
__legacyCompat: {
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
|
||||
this.legacyAPI = legacyAPI;
|
||||
this.setupLegacyComponents(spacesService, plugins.features, plugins.licensing);
|
||||
},
|
||||
createDefaultSpace: async () => {
|
||||
const esClient = await core.elasticsearch.adminClient$.pipe(take(1)).toPromise();
|
||||
return createDefaultSpace({
|
||||
esClient,
|
||||
savedObjects: this.getLegacyAPI().savedObjects,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
||||
private setupLegacyComponents(
|
||||
spacesService: SpacesServiceSetup,
|
||||
featuresSetup: FeaturesPluginSetup,
|
||||
licensingSetup: LicensingPluginSetup
|
||||
) {
|
||||
const legacyAPI = this.getLegacyAPI();
|
||||
const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects;
|
||||
addScopedSavedObjectsClientWrapperFactory(
|
||||
Number.MIN_SAFE_INTEGER,
|
||||
'spaces',
|
||||
spacesSavedObjectsClientWrapperFactory(spacesService, types)
|
||||
);
|
||||
legacyAPI.tutorial.addScopedTutorialContextFactory(
|
||||
createSpacesTutorialContextFactory(spacesService)
|
||||
);
|
||||
legacyAPI.capabilities.registerCapabilitiesModifier(async (request, uiCapabilities) => {
|
||||
try {
|
||||
const activeSpace = await spacesService.getActiveSpace(KibanaRequest.from(request));
|
||||
const features = featuresSetup.getFeatures();
|
||||
return toggleUICapabilities(features, uiCapabilities, activeSpace);
|
||||
} catch (e) {
|
||||
return uiCapabilities;
|
||||
}
|
||||
});
|
||||
// Register a function with server to manage the collection of usage stats
|
||||
legacyAPI.usage.collectorSet.register(
|
||||
getSpacesUsageCollector({
|
||||
kibanaIndex: legacyAPI.legacyConfig.kibanaIndex,
|
||||
usage: legacyAPI.usage,
|
||||
features: featuresSetup,
|
||||
licensing: licensingSetup,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Readable } from 'stream';
|
||||
import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams';
|
||||
import { SavedObjectsSchema, SavedObjectsLegacyService } from 'src/core/server';
|
||||
import { LegacyAPI } from '../../../plugin';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { createSpaces } from '.';
|
||||
|
||||
async function readStreamToCompletion(stream: Readable) {
|
||||
return (await (createPromiseFromStreams([stream, createConcatStream([])]) as unknown)) as any[];
|
||||
}
|
||||
|
||||
interface LegacyAPIOpts {
|
||||
spaces?: Space[];
|
||||
}
|
||||
|
||||
export const createLegacyAPI = ({
|
||||
spaces = createSpaces().map(s => ({ id: s.id, ...s.attributes })),
|
||||
}: LegacyAPIOpts = {}) => {
|
||||
const mockSavedObjectsClientContract = {
|
||||
get: jest.fn((type, id) => {
|
||||
const result = spaces.filter(s => s.id === id);
|
||||
if (!result.length) {
|
||||
throw new Error(`not found: [${type}:${id}]`);
|
||||
}
|
||||
return result[0];
|
||||
}),
|
||||
find: jest.fn(() => {
|
||||
return {
|
||||
total: spaces.length,
|
||||
saved_objects: spaces,
|
||||
};
|
||||
}),
|
||||
create: jest.fn((type, attributes, { id }) => {
|
||||
if (spaces.find(s => s.id === id)) {
|
||||
throw new Error('conflict');
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
update: jest.fn((type, id) => {
|
||||
if (!spaces.find(s => s.id === id)) {
|
||||
throw new Error('not found: during update');
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
delete: jest.fn((type: string, id: string) => {
|
||||
return {};
|
||||
}),
|
||||
deleteByNamespace: jest.fn(),
|
||||
};
|
||||
|
||||
const savedObjectsService = ({
|
||||
types: ['visualization', 'dashboard', 'index-pattern', 'globalType'],
|
||||
schema: new SavedObjectsSchema({
|
||||
space: {
|
||||
isNamespaceAgnostic: true,
|
||||
hidden: true,
|
||||
},
|
||||
globalType: {
|
||||
isNamespaceAgnostic: true,
|
||||
},
|
||||
}),
|
||||
getScopedSavedObjectsClient: jest.fn().mockResolvedValue(mockSavedObjectsClientContract),
|
||||
importExport: {
|
||||
objectLimit: 10000,
|
||||
getSortedObjectsForExport: jest.fn().mockResolvedValue(
|
||||
new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
this.push(null);
|
||||
},
|
||||
})
|
||||
),
|
||||
importSavedObjects: jest.fn().mockImplementation(async (opts: Record<string, any>) => {
|
||||
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
|
||||
return {
|
||||
success: true,
|
||||
successCount: objectsToImport.length,
|
||||
};
|
||||
}),
|
||||
resolveImportErrors: jest.fn().mockImplementation(async (opts: Record<string, any>) => {
|
||||
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
|
||||
return {
|
||||
success: true,
|
||||
successCount: objectsToImport.length,
|
||||
};
|
||||
}),
|
||||
},
|
||||
SavedObjectsClient: {
|
||||
errors: {
|
||||
isNotFoundError: jest.fn((e: any) => e.message.startsWith('not found:')),
|
||||
isConflictError: jest.fn((e: any) => e.message.startsWith('conflict')),
|
||||
},
|
||||
},
|
||||
} as unknown) as jest.Mocked<SavedObjectsLegacyService>;
|
||||
|
||||
const legacyAPI: jest.Mocked<LegacyAPI> = {
|
||||
legacyConfig: {
|
||||
kibanaIndex: '',
|
||||
},
|
||||
auditLogger: {} as any,
|
||||
capabilities: {} as any,
|
||||
security: {} as any,
|
||||
tutorial: {} as any,
|
||||
usage: {} as any,
|
||||
xpackMain: {} as any,
|
||||
savedObjects: savedObjectsService,
|
||||
};
|
||||
|
||||
return legacyAPI;
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from 'src/core/server';
|
||||
|
||||
export const createMockSavedObjectsRepository = (spaces: any[] = []) => {
|
||||
const mockSavedObjectsClientContract = ({
|
||||
get: jest.fn((type, id) => {
|
||||
const result = spaces.filter(s => s.id === id);
|
||||
if (!result.length) {
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
|
||||
}
|
||||
return result[0];
|
||||
}),
|
||||
find: jest.fn(() => {
|
||||
return {
|
||||
total: spaces.length,
|
||||
saved_objects: spaces,
|
||||
};
|
||||
}),
|
||||
create: jest.fn((type, attributes, { id }) => {
|
||||
if (spaces.find(s => s.id === id)) {
|
||||
throw SavedObjectsErrorHelpers.decorateConflictError(new Error(), 'space conflict');
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
update: jest.fn((type, id) => {
|
||||
if (!spaces.find(s => s.id === id)) {
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
delete: jest.fn((type: string, id: string) => {
|
||||
return {};
|
||||
}),
|
||||
deleteByNamespace: jest.fn(),
|
||||
} as unknown) as jest.Mocked<SavedObjectsClientContract>;
|
||||
|
||||
return mockSavedObjectsClientContract;
|
||||
};
|
|
@ -10,18 +10,21 @@ export function createSpaces() {
|
|||
id: 'a-space',
|
||||
attributes: {
|
||||
name: 'a space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b-space',
|
||||
attributes: {
|
||||
name: 'b space',
|
||||
disabledFeatures: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'default',
|
||||
attributes: {
|
||||
name: 'Default Space',
|
||||
disabledFeatures: [],
|
||||
_reserved: true,
|
||||
},
|
||||
},
|
|
@ -5,11 +5,6 @@
|
|||
*/
|
||||
|
||||
export { createSpaces } from './create_spaces';
|
||||
export {
|
||||
createTestHandler,
|
||||
TestConfig,
|
||||
TestOptions,
|
||||
TeardownFn,
|
||||
RequestRunner,
|
||||
RequestRunnerResult,
|
||||
} from './create_test_handler';
|
||||
export { createLegacyAPI } from './create_legacy_api';
|
||||
export { createMockSavedObjectsRepository } from './create_mock_so_repository';
|
||||
export { mockRouteContext, mockRouteContextWithInvalidLicense } from './route_contexts';
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RequestHandlerContext } from 'src/core/server';
|
||||
import { LICENSE_STATUS } from '../../../../../licensing/server/constants';
|
||||
|
||||
export const mockRouteContext = ({
|
||||
licensing: {
|
||||
license: {
|
||||
check: jest.fn().mockReturnValue({
|
||||
check: LICENSE_STATUS.Valid,
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown) as RequestHandlerContext;
|
||||
|
||||
export const mockRouteContextWithInvalidLicense = ({
|
||||
licensing: {
|
||||
license: {
|
||||
check: jest.fn().mockReturnValue({
|
||||
check: LICENSE_STATUS.Invalid,
|
||||
message: 'License is invalid for spaces',
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown) as RequestHandlerContext;
|
450
x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
vendored
Normal file
450
x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
vendored
Normal file
|
@ -0,0 +1,450 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as Rx from 'rxjs';
|
||||
import {
|
||||
createSpaces,
|
||||
createLegacyAPI,
|
||||
createMockSavedObjectsRepository,
|
||||
mockRouteContext,
|
||||
mockRouteContextWithInvalidLicense,
|
||||
} from '../__fixtures__';
|
||||
import { CoreSetup, IRouter, kibanaResponseFactory } from 'src/core/server';
|
||||
import {
|
||||
loggingServiceMock,
|
||||
elasticsearchServiceMock,
|
||||
httpServiceMock,
|
||||
httpServerMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { createOptionalPlugin } from '../../../../../../legacy/server/lib/optional_plugin';
|
||||
import { SpacesAuditLogger } from '../../../lib/audit_logger';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { RouteSchemas } from 'src/core/server/http/router/route';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
|
||||
describe('copy to space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes }));
|
||||
|
||||
const setup = async () => {
|
||||
const httpService = httpServiceMock.createSetupContract();
|
||||
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
|
||||
|
||||
const legacyAPI = createLegacyAPI({ spaces });
|
||||
|
||||
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
|
||||
|
||||
const log = loggingServiceMock.create().get('spaces');
|
||||
|
||||
const service = new SpacesService(log, () => legacyAPI);
|
||||
const spacesService = await service.setup({
|
||||
http: (httpService as unknown) as CoreSetup['http'],
|
||||
elasticsearch: elasticsearchServiceMock.createSetupContract(),
|
||||
getSecurity: () =>
|
||||
createOptionalPlugin({ get: () => null }, 'xpack.security', {}, 'security'),
|
||||
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
|
||||
config$: Rx.of(spacesConfig),
|
||||
});
|
||||
|
||||
spacesService.scopedClient = jest.fn((req: any) => {
|
||||
return Promise.resolve(
|
||||
new SpacesClient(
|
||||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
savedObjectsRepositoryMock,
|
||||
spacesConfig,
|
||||
savedObjectsRepositoryMock,
|
||||
req
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
initCopyToSpacesApi({
|
||||
externalRouter: router,
|
||||
getSavedObjects: () => legacyAPI.savedObjects,
|
||||
log,
|
||||
spacesService,
|
||||
});
|
||||
|
||||
const [
|
||||
[ctsRouteDefinition, ctsRouteHandler],
|
||||
[resolveRouteDefinition, resolveRouteHandler],
|
||||
] = router.post.mock.calls;
|
||||
|
||||
return {
|
||||
copyToSpace: {
|
||||
routeValidation: ctsRouteDefinition.validate as RouteSchemas<
|
||||
ObjectType,
|
||||
ObjectType,
|
||||
ObjectType
|
||||
>,
|
||||
routeHandler: ctsRouteHandler,
|
||||
},
|
||||
resolveConflicts: {
|
||||
routeValidation: resolveRouteDefinition.validate as RouteSchemas<
|
||||
ObjectType,
|
||||
ObjectType,
|
||||
ObjectType
|
||||
>,
|
||||
routeHandler: resolveRouteHandler,
|
||||
},
|
||||
savedObjectsRepositoryMock,
|
||||
legacyAPI,
|
||||
};
|
||||
};
|
||||
|
||||
describe('POST /api/spaces/_copy_saved_objects', () => {
|
||||
it(`returns http/403 when the license is invalid`, async () => {
|
||||
const { copyToSpace } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const response = await copyToSpace.routeHandler(
|
||||
mockRouteContextWithInvalidLicense,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(403);
|
||||
expect(response.payload).toEqual({
|
||||
message: 'License is invalid for spaces',
|
||||
});
|
||||
});
|
||||
|
||||
it(`uses a Saved Objects Client instance without the spaces wrapper`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { copyToSpace, legacyAPI } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
body: payload,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
await copyToSpace.routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(legacyAPI.savedObjects.getScopedSavedObjectsClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{
|
||||
excludedWrappers: ['spaces'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it(`requires space IDs to be unique`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'a-space'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { copyToSpace } = await setup();
|
||||
|
||||
expect(() =>
|
||||
copyToSpace.routeValidation.body!.validate(payload)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[spaces]: duplicate space ids are not allowed"`);
|
||||
});
|
||||
|
||||
it(`requires well-formed space IDS`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'a-space-invalid-!@#$%^&*()'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { copyToSpace } = await setup();
|
||||
|
||||
expect(() =>
|
||||
copyToSpace.routeValidation.body!.validate(payload)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[spaces.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed"`
|
||||
);
|
||||
});
|
||||
|
||||
it(`requires objects to be unique`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [{ type: 'foo', id: 'bar' }, { type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { copyToSpace } = await setup();
|
||||
|
||||
expect(() =>
|
||||
copyToSpace.routeValidation.body!.validate(payload)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`);
|
||||
});
|
||||
|
||||
it('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [{ type: 'globalType', id: 'bar' }, { type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { copyToSpace, legacyAPI } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
body: payload,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const response = await copyToSpace.routeHandler(
|
||||
mockRouteContext,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
const { status } = response;
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(legacyAPI.savedObjects.importExport.importSavedObjects).toHaveBeenCalledTimes(1);
|
||||
const [importCallOptions] = (legacyAPI.savedObjects.importExport
|
||||
.importSavedObjects as any).mock.calls[0];
|
||||
|
||||
expect(importCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
|
||||
it('copies to multiple spaces', async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'b-space'],
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { copyToSpace, legacyAPI } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
body: payload,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const response = await copyToSpace.routeHandler(
|
||||
mockRouteContext,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
const { status } = response;
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(legacyAPI.savedObjects.importExport.importSavedObjects).toHaveBeenCalledTimes(2);
|
||||
const [firstImportCallOptions] = (legacyAPI.savedObjects.importExport
|
||||
.importSavedObjects as any).mock.calls[0];
|
||||
|
||||
expect(firstImportCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
});
|
||||
|
||||
const [secondImportCallOptions] = (legacyAPI.savedObjects.importExport
|
||||
.importSavedObjects as any).mock.calls[1];
|
||||
|
||||
expect(secondImportCallOptions).toMatchObject({
|
||||
namespace: 'b-space',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/spaces/_resolve_copy_saved_objects_errors', () => {
|
||||
it(`returns http/403 when the license is invalid`, async () => {
|
||||
const { resolveConflicts } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const response = await resolveConflicts.routeHandler(
|
||||
mockRouteContextWithInvalidLicense,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(403);
|
||||
expect(response.payload).toEqual({
|
||||
message: 'License is invalid for spaces',
|
||||
});
|
||||
});
|
||||
|
||||
it(`uses a Saved Objects Client instance without the spaces wrapper`, async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { resolveConflicts, legacyAPI } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
body: payload,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
await resolveConflicts.routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(legacyAPI.savedObjects.getScopedSavedObjectsClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{
|
||||
excludedWrappers: ['spaces'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it(`requires objects to be unique`, async () => {
|
||||
const payload = {
|
||||
retries: {},
|
||||
objects: [{ type: 'foo', id: 'bar' }, { type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { resolveConflicts } = await setup();
|
||||
|
||||
expect(() =>
|
||||
resolveConflicts.routeValidation.body!.validate(payload)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`);
|
||||
});
|
||||
|
||||
it(`requires well-formed space ids`, async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['invalid-space-id!@#$%^&*()']: [
|
||||
{
|
||||
type: 'foo',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [{ type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { resolveConflicts } = await setup();
|
||||
|
||||
expect(() =>
|
||||
resolveConflicts.routeValidation.body!.validate(payload)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[key(\\"invalid-space-id!@#$%^&*()\\")]: Invalid space id: invalid-space-id!@#$%^&*()"`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
},
|
||||
{ type: 'visualization', id: 'bar' },
|
||||
],
|
||||
};
|
||||
|
||||
const { resolveConflicts, legacyAPI } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
body: payload,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const response = await resolveConflicts.routeHandler(
|
||||
mockRouteContext,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
const { status } = response;
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(legacyAPI.savedObjects.importExport.resolveImportErrors).toHaveBeenCalledTimes(1);
|
||||
const [resolveImportErrorsCallOptions] = (legacyAPI.savedObjects.importExport
|
||||
.resolveImportErrors as any).mock.calls[0];
|
||||
|
||||
expect(resolveImportErrorsCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves conflicts for multiple spaces', async () => {
|
||||
const payload = {
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
['b-space']: [
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const { resolveConflicts, legacyAPI } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
body: payload,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const response = await resolveConflicts.routeHandler(
|
||||
mockRouteContext,
|
||||
request,
|
||||
kibanaResponseFactory
|
||||
);
|
||||
|
||||
const { status } = response;
|
||||
|
||||
expect(status).toEqual(200);
|
||||
expect(legacyAPI.savedObjects.importExport.resolveImportErrors).toHaveBeenCalledTimes(2);
|
||||
const [resolveImportErrorsFirstCallOptions] = (legacyAPI.savedObjects.importExport
|
||||
.resolveImportErrors as any).mock.calls[0];
|
||||
|
||||
expect(resolveImportErrorsFirstCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
|
||||
const [resolveImportErrorsSecondCallOptions] = (legacyAPI.savedObjects.importExport
|
||||
.resolveImportErrors as any).mock.calls[1];
|
||||
|
||||
expect(resolveImportErrorsSecondCallOptions).toMatchObject({
|
||||
namespace: 'b-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
152
x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts
vendored
Normal file
152
x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts
vendored
Normal file
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import _ from 'lodash';
|
||||
import { SavedObject } from 'src/core/server';
|
||||
import {
|
||||
copySavedObjectsToSpacesFactory,
|
||||
resolveCopySavedObjectsToSpacesConflictsFactory,
|
||||
} from '../../../lib/copy_to_spaces';
|
||||
import { ExternalRouteDeps } from '.';
|
||||
import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from '../../../lib/copy_to_spaces/copy_to_spaces';
|
||||
import { SPACE_ID_REGEX } from '../../../lib/space_schema';
|
||||
import { createLicensedRouteHandler } from '../../lib';
|
||||
|
||||
type SavedObjectIdentifier = Pick<SavedObject, 'id' | 'type'>;
|
||||
|
||||
const areObjectsUnique = (objects: SavedObjectIdentifier[]) =>
|
||||
_.uniq(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length;
|
||||
|
||||
export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, spacesService, getSavedObjects } = deps;
|
||||
|
||||
externalRouter.post(
|
||||
{
|
||||
path: '/api/spaces/_copy_saved_objects',
|
||||
options: {
|
||||
tags: ['access:copySavedObjectsToSpaces'],
|
||||
},
|
||||
validate: {
|
||||
body: schema.object({
|
||||
spaces: schema.arrayOf(
|
||||
schema.string({
|
||||
validate: value => {
|
||||
if (!SPACE_ID_REGEX.test(value)) {
|
||||
return `lower case, a-z, 0-9, "_", and "-" are allowed`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
validate: spaceIds => {
|
||||
if (_.uniq(spaceIds).length !== spaceIds.length) {
|
||||
return 'duplicate space ids are not allowed';
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
objects: schema.arrayOf(
|
||||
schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
}),
|
||||
{
|
||||
validate: objects => {
|
||||
if (!areObjectsUnique(objects)) {
|
||||
return 'duplicate objects are not allowed';
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
includeReferences: schema.boolean({ defaultValue: false }),
|
||||
overwrite: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
const savedObjectsClient = getSavedObjects().getScopedSavedObjectsClient(
|
||||
request,
|
||||
COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS
|
||||
);
|
||||
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
|
||||
savedObjectsClient,
|
||||
getSavedObjects()
|
||||
);
|
||||
const { spaces: destinationSpaceIds, objects, includeReferences, overwrite } = request.body;
|
||||
const sourceSpaceId = spacesService.getSpaceId(request);
|
||||
const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, {
|
||||
objects,
|
||||
includeReferences,
|
||||
overwrite,
|
||||
});
|
||||
return response.ok({ body: copyResponse });
|
||||
})
|
||||
);
|
||||
|
||||
externalRouter.post(
|
||||
{
|
||||
path: '/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
options: {
|
||||
tags: ['access:copySavedObjectsToSpaces'],
|
||||
},
|
||||
validate: {
|
||||
body: schema.object({
|
||||
retries: schema.recordOf(
|
||||
schema.string({
|
||||
validate: spaceId => {
|
||||
if (!SPACE_ID_REGEX.test(spaceId)) {
|
||||
return `Invalid space id: ${spaceId}`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
overwrite: schema.boolean({ defaultValue: false }),
|
||||
})
|
||||
)
|
||||
),
|
||||
objects: schema.arrayOf(
|
||||
schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
}),
|
||||
{
|
||||
validate: objects => {
|
||||
if (!areObjectsUnique(objects)) {
|
||||
return 'duplicate objects are not allowed';
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
includeReferences: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
const savedObjectsClient = getSavedObjects().getScopedSavedObjectsClient(
|
||||
request,
|
||||
COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS
|
||||
);
|
||||
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
|
||||
savedObjectsClient,
|
||||
getSavedObjects()
|
||||
);
|
||||
const { objects, includeReferences, retries } = request.body;
|
||||
const sourceSpaceId = spacesService.getSpaceId(request);
|
||||
const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts(
|
||||
sourceSpaceId,
|
||||
{
|
||||
objects,
|
||||
includeReferences,
|
||||
retries,
|
||||
}
|
||||
);
|
||||
return response.ok({ body: resolveConflictsResponse });
|
||||
})
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue