mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Spaces - NP updates for usage collection and capabilities (#57693)
* remove kibanaIndex from LegacyAPI * moving capabilities, adding tests * moving usage collection * cleanup * don't toggle capabilities on unauthenticated routes * reintroduce exception handling * pipe dat config * start addressing PR feedback * fix CoreSetup's generic type * fix usage collector tests * PR review updates Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
3f7abe3c55
commit
91330d2493
23 changed files with 409 additions and 119 deletions
|
@ -43,6 +43,7 @@ interface RequestFixtureOptions<P = any, Q = any, B = any> {
|
|||
method?: RouteMethod;
|
||||
socket?: Socket;
|
||||
routeTags?: string[];
|
||||
routeAuthRequired?: false;
|
||||
validation?: {
|
||||
params?: RouteValidationSpec<P>;
|
||||
query?: RouteValidationSpec<Q>;
|
||||
|
@ -59,6 +60,7 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
|
|||
method = 'get',
|
||||
socket = new Socket(),
|
||||
routeTags,
|
||||
routeAuthRequired,
|
||||
validation = {},
|
||||
}: RequestFixtureOptions<P, Q, B> = {}) {
|
||||
const queryString = stringify(query, { sort: false });
|
||||
|
@ -77,7 +79,9 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
|
|||
query: queryString,
|
||||
search: queryString ? `?${queryString}` : queryString,
|
||||
},
|
||||
route: { settings: { tags: routeTags } },
|
||||
route: {
|
||||
settings: { tags: routeTags, auth: routeAuthRequired },
|
||||
},
|
||||
raw: {
|
||||
req: { socket },
|
||||
},
|
||||
|
|
38
src/plugins/usage_collection/server/mocks.ts
Normal file
38
src/plugins/usage_collection/server/mocks.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { loggingServiceMock } from '../../../core/server/mocks';
|
||||
import { UsageCollectionSetup } from './plugin';
|
||||
import { CollectorSet } from './collector';
|
||||
|
||||
const createSetupContract = () => {
|
||||
return {
|
||||
...new CollectorSet({
|
||||
logger: loggingServiceMock.createLogger(),
|
||||
maximumWaitTimeForAllCollectorsInS: 1,
|
||||
}),
|
||||
registerLegacySavedObjects: jest.fn() as jest.Mocked<
|
||||
UsageCollectionSetup['registerLegacySavedObjects']
|
||||
>,
|
||||
} as UsageCollectionSetup;
|
||||
};
|
||||
|
||||
export const usageCollectionPluginMock = {
|
||||
createSetupContract,
|
||||
};
|
|
@ -34,19 +34,6 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
|
||||
uiCapabilities() {
|
||||
return {
|
||||
spaces: {
|
||||
manage: true,
|
||||
},
|
||||
management: {
|
||||
kibana: {
|
||||
spaces: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
uiExports: {
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
managementSections: [],
|
||||
|
@ -110,14 +97,9 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
throw new Error('New Platform XPack Spaces plugin is not available.');
|
||||
}
|
||||
|
||||
const config = server.config();
|
||||
|
||||
const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat;
|
||||
|
||||
registerLegacyAPI({
|
||||
legacyConfig: {
|
||||
kibanaIndex: config.get('kibana.index'),
|
||||
},
|
||||
savedObjects: server.savedObjects,
|
||||
auditLogger: {
|
||||
create: (pluginId: string) =>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Plugin } from './plugin';
|
|||
export { uiCapabilitiesRegex } from './feature_schema';
|
||||
|
||||
export { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common';
|
||||
export { PluginSetupContract } from './plugin';
|
||||
export { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
new Plugin(initializerContext);
|
||||
|
|
27
x-pack/plugins/features/server/mocks.ts
Normal file
27
x-pack/plugins/features/server/mocks.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
|
||||
const createSetup = (): jest.Mocked<PluginSetupContract> => {
|
||||
return {
|
||||
getFeatures: jest.fn(),
|
||||
getFeaturesUICapabilities: jest.fn(),
|
||||
registerFeature: jest.fn(),
|
||||
registerLegacyAPI: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const createStart = (): jest.Mocked<PluginStartContract> => {
|
||||
return {
|
||||
getFeatures: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
export const featuresPluginMock = {
|
||||
createSetup,
|
||||
createStart,
|
||||
};
|
|
@ -30,6 +30,10 @@ export interface PluginSetupContract {
|
|||
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
|
||||
}
|
||||
|
||||
export interface PluginStartContract {
|
||||
getFeatures(): Feature[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a set of APIs that are available in the legacy platform only and required by this plugin
|
||||
* to function properly.
|
||||
|
@ -45,6 +49,8 @@ export interface LegacyAPI {
|
|||
export class Plugin {
|
||||
private readonly logger: Logger;
|
||||
|
||||
private readonly featureRegistry: FeatureRegistry = new FeatureRegistry();
|
||||
|
||||
private legacyAPI?: LegacyAPI;
|
||||
private readonly getLegacyAPI = () => {
|
||||
if (!this.legacyAPI) {
|
||||
|
@ -61,18 +67,16 @@ export class Plugin {
|
|||
core: CoreSetup,
|
||||
{ timelion }: { timelion?: TimelionSetupContract }
|
||||
): Promise<RecursiveReadonly<PluginSetupContract>> {
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
||||
defineRoutes({
|
||||
router: core.http.createRouter(),
|
||||
featureRegistry,
|
||||
featureRegistry: this.featureRegistry,
|
||||
getLegacyAPI: this.getLegacyAPI,
|
||||
});
|
||||
|
||||
return deepFreeze({
|
||||
registerFeature: featureRegistry.register.bind(featureRegistry),
|
||||
getFeatures: featureRegistry.getAll.bind(featureRegistry),
|
||||
getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(featureRegistry.getAll()),
|
||||
registerFeature: this.featureRegistry.register.bind(this.featureRegistry),
|
||||
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry),
|
||||
getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()),
|
||||
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
|
||||
this.legacyAPI = legacyAPI;
|
||||
|
@ -82,14 +86,17 @@ export class Plugin {
|
|||
savedObjectTypes: this.legacyAPI.savedObjectTypes,
|
||||
includeTimelion: timelion !== undefined && timelion.uiEnabled,
|
||||
})) {
|
||||
featureRegistry.register(feature);
|
||||
this.featureRegistry.register(feature);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {
|
||||
public start(): RecursiveReadonly<PluginStartContract> {
|
||||
this.logger.debug('Starting plugin');
|
||||
return deepFreeze({
|
||||
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry),
|
||||
});
|
||||
}
|
||||
|
||||
public stop() {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { capabilitiesProvider } from './capabilities_provider';
|
||||
|
||||
describe('Capabilities provider', () => {
|
||||
it('provides the expected capabilities', () => {
|
||||
expect(capabilitiesProvider()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"management": Object {
|
||||
"kibana": Object {
|
||||
"spaces": true,
|
||||
},
|
||||
},
|
||||
"spaces": Object {
|
||||
"manage": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 capabilitiesProvider = () => ({
|
||||
spaces: {
|
||||
manage: true,
|
||||
},
|
||||
management: {
|
||||
kibana: {
|
||||
spaces: true,
|
||||
},
|
||||
},
|
||||
});
|
|
@ -6,8 +6,12 @@
|
|||
|
||||
import { Feature } from '../../../../plugins/features/server';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { toggleUICapabilities } from './toggle_ui_capabilities';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { setupCapabilitiesSwitcher } from './capabilities_switcher';
|
||||
import { Capabilities, CoreSetup } from 'src/core/server';
|
||||
import { coreMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks';
|
||||
import { featuresPluginMock } from '../../../features/server/mocks';
|
||||
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
|
||||
import { PluginsStart } from '../plugin';
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
|
@ -91,8 +95,33 @@ const buildCapabilities = () =>
|
|||
},
|
||||
}) as Capabilities;
|
||||
|
||||
describe('toggleUiCapabilities', () => {
|
||||
it('does not toggle capabilities when the space has no disabled features', () => {
|
||||
const setup = (space: Space) => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
||||
const featuresStart = featuresPluginMock.createStart();
|
||||
featuresStart.getFeatures.mockReturnValue(features);
|
||||
|
||||
coreSetup.getStartServices.mockResolvedValue([
|
||||
coreMock.createStart(),
|
||||
{ features: featuresStart },
|
||||
]);
|
||||
|
||||
const spacesService = spacesServiceMock.createSetupContract();
|
||||
spacesService.getActiveSpace.mockResolvedValue(space);
|
||||
|
||||
const logger = loggingServiceMock.createLogger();
|
||||
|
||||
const switcher = setupCapabilitiesSwitcher(
|
||||
(coreSetup as unknown) as CoreSetup<PluginsStart>,
|
||||
spacesService,
|
||||
logger
|
||||
);
|
||||
|
||||
return { switcher, logger, spacesService };
|
||||
};
|
||||
|
||||
describe('capabilitiesSwitcher', () => {
|
||||
it('does not toggle capabilities when the space has no disabled features', async () => {
|
||||
const space: Space = {
|
||||
id: 'space',
|
||||
name: '',
|
||||
|
@ -100,11 +129,54 @@ describe('toggleUiCapabilities', () => {
|
|||
};
|
||||
|
||||
const capabilities = buildCapabilities();
|
||||
const result = toggleUICapabilities(features, capabilities, space);
|
||||
|
||||
const { switcher } = setup(space);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const result = await switcher(request, capabilities);
|
||||
|
||||
expect(result).toEqual(buildCapabilities());
|
||||
});
|
||||
|
||||
it('ignores unknown disabledFeatures', () => {
|
||||
it('does not toggle capabilities when the request is not authenticated', async () => {
|
||||
const space: Space = {
|
||||
id: 'space',
|
||||
name: '',
|
||||
disabledFeatures: ['feature_1', 'feature_2', 'feature_3'],
|
||||
};
|
||||
|
||||
const capabilities = buildCapabilities();
|
||||
|
||||
const { switcher } = setup(space);
|
||||
const request = httpServerMock.createKibanaRequest({ routeAuthRequired: false });
|
||||
|
||||
const result = await switcher(request, capabilities);
|
||||
|
||||
expect(result).toEqual(buildCapabilities());
|
||||
});
|
||||
|
||||
it('logs a warning, and does not toggle capabilities if an error is encountered', async () => {
|
||||
const space: Space = {
|
||||
id: 'space',
|
||||
name: '',
|
||||
disabledFeatures: ['feature_1', 'feature_2', 'feature_3'],
|
||||
};
|
||||
|
||||
const capabilities = buildCapabilities();
|
||||
|
||||
const { switcher, logger, spacesService } = setup(space);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
spacesService.getActiveSpace.mockRejectedValue(new Error('Something terrible happened'));
|
||||
|
||||
const result = await switcher(request, capabilities);
|
||||
|
||||
expect(result).toEqual(buildCapabilities());
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Error toggling capabilities for request to /path: Error: Something terrible happened`
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores unknown disabledFeatures', async () => {
|
||||
const space: Space = {
|
||||
id: 'space',
|
||||
name: '',
|
||||
|
@ -112,11 +184,15 @@ describe('toggleUiCapabilities', () => {
|
|||
};
|
||||
|
||||
const capabilities = buildCapabilities();
|
||||
const result = toggleUICapabilities(features, capabilities, space);
|
||||
|
||||
const { switcher } = setup(space);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const result = await switcher(request, capabilities);
|
||||
|
||||
expect(result).toEqual(buildCapabilities());
|
||||
});
|
||||
|
||||
it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', () => {
|
||||
it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', async () => {
|
||||
const space: Space = {
|
||||
id: 'space',
|
||||
name: '',
|
||||
|
@ -124,7 +200,10 @@ describe('toggleUiCapabilities', () => {
|
|||
};
|
||||
|
||||
const capabilities = buildCapabilities();
|
||||
const result = toggleUICapabilities(features, capabilities, space);
|
||||
|
||||
const { switcher } = setup(space);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const result = await switcher(request, capabilities);
|
||||
|
||||
const expectedCapabilities = buildCapabilities();
|
||||
|
||||
|
@ -137,7 +216,7 @@ describe('toggleUiCapabilities', () => {
|
|||
expect(result).toEqual(expectedCapabilities);
|
||||
});
|
||||
|
||||
it('can disable everything', () => {
|
||||
it('can disable everything', async () => {
|
||||
const space: Space = {
|
||||
id: 'space',
|
||||
name: '',
|
||||
|
@ -145,7 +224,10 @@ describe('toggleUiCapabilities', () => {
|
|||
};
|
||||
|
||||
const capabilities = buildCapabilities();
|
||||
const result = toggleUICapabilities(features, capabilities, space);
|
||||
|
||||
const { switcher } = setup(space);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const result = await switcher(request, capabilities);
|
||||
|
||||
const expectedCapabilities = buildCapabilities();
|
||||
|
|
@ -4,15 +4,41 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import _ from 'lodash';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server';
|
||||
import { Feature } from '../../../../plugins/features/server';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { SpacesServiceSetup } from '../spaces_service';
|
||||
import { PluginsStart } from '../plugin';
|
||||
|
||||
export function toggleUICapabilities(
|
||||
features: Feature[],
|
||||
capabilities: UICapabilities,
|
||||
activeSpace: Space
|
||||
) {
|
||||
export function setupCapabilitiesSwitcher(
|
||||
core: CoreSetup<PluginsStart>,
|
||||
spacesService: SpacesServiceSetup,
|
||||
logger: Logger
|
||||
): CapabilitiesSwitcher {
|
||||
return async (request, capabilities) => {
|
||||
const isAnonymousRequest = !request.route.options.authRequired;
|
||||
|
||||
if (isAnonymousRequest) {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
try {
|
||||
const [activeSpace, [, { features }]] = await Promise.all([
|
||||
spacesService.getActiveSpace(request),
|
||||
core.getStartServices(),
|
||||
]);
|
||||
|
||||
const registeredFeatures = features.getFeatures();
|
||||
|
||||
return toggleCapabilities(registeredFeatures, capabilities, activeSpace);
|
||||
} catch (e) {
|
||||
logger.warn(`Error toggling capabilities for request to ${request.url.pathname}: ${e}`);
|
||||
return capabilities;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function toggleCapabilities(features: Feature[], capabilities: Capabilities, activeSpace: Space) {
|
||||
const clonedCapabilities = _.cloneDeep(capabilities);
|
||||
|
||||
toggleDisabledFeatures(features, clonedCapabilities, activeSpace);
|
||||
|
@ -22,7 +48,7 @@ export function toggleUICapabilities(
|
|||
|
||||
function toggleDisabledFeatures(
|
||||
features: Feature[],
|
||||
capabilities: UICapabilities,
|
||||
capabilities: Capabilities,
|
||||
activeSpace: Space
|
||||
) {
|
||||
const disabledFeatureKeys = activeSpace.disabledFeatures;
|
20
x-pack/plugins/spaces/server/capabilities/index.ts
Normal file
20
x-pack/plugins/spaces/server/capabilities/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { CoreSetup, Logger } from 'src/core/server';
|
||||
import { capabilitiesProvider } from './capabilities_provider';
|
||||
import { setupCapabilitiesSwitcher } from './capabilities_switcher';
|
||||
import { PluginsStart } from '../plugin';
|
||||
import { SpacesServiceSetup } from '../spaces_service';
|
||||
|
||||
export const setupCapabilities = (
|
||||
core: CoreSetup<PluginsStart>,
|
||||
spacesService: SpacesServiceSetup,
|
||||
logger: Logger
|
||||
) => {
|
||||
core.capabilities.registerProvider(capabilitiesProvider);
|
||||
core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, spacesService, logger));
|
||||
};
|
|
@ -201,12 +201,10 @@ describe('onPostAuthInterceptor', () => {
|
|||
// interceptor to parse out the space id and rewrite the request's URL. Rather than duplicating that logic,
|
||||
// we are including the already tested interceptor here in the test chain.
|
||||
initSpacesOnRequestInterceptor({
|
||||
getLegacyAPI: () => legacyAPI,
|
||||
http: (http as unknown) as CoreSetup['http'],
|
||||
});
|
||||
|
||||
initSpacesOnPostAuthRequestInterceptor({
|
||||
getLegacyAPI: () => legacyAPI,
|
||||
http: (http as unknown) as CoreSetup['http'],
|
||||
log: loggingMock,
|
||||
features: featuresPlugin,
|
||||
|
|
|
@ -7,13 +7,12 @@ import { Logger, CoreSetup } from 'src/core/server';
|
|||
import { Space } from '../../../common/model/space';
|
||||
import { wrapError } from '../errors';
|
||||
import { SpacesServiceSetup } from '../../spaces_service/spaces_service';
|
||||
import { LegacyAPI, PluginsSetup } from '../../plugin';
|
||||
import { PluginsSetup } from '../../plugin';
|
||||
import { getSpaceSelectorUrl } from '../get_space_selector_url';
|
||||
import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants';
|
||||
import { addSpaceIdToPath } from '../../../common';
|
||||
|
||||
export interface OnPostAuthInterceptorDeps {
|
||||
getLegacyAPI(): LegacyAPI;
|
||||
http: CoreSetup['http'];
|
||||
features: PluginsSetup['features'];
|
||||
spacesService: SpacesServiceSetup;
|
||||
|
@ -22,7 +21,6 @@ export interface OnPostAuthInterceptorDeps {
|
|||
|
||||
export function initSpacesOnPostAuthRequestInterceptor({
|
||||
features,
|
||||
getLegacyAPI,
|
||||
spacesService,
|
||||
log,
|
||||
http,
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
} from '../../../../../../src/core/server';
|
||||
|
||||
import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server';
|
||||
import { LegacyAPI } from '../../plugin';
|
||||
import { elasticsearchServiceMock } from 'src/core/server/mocks';
|
||||
|
||||
describe('onRequestInterceptor', () => {
|
||||
|
@ -110,10 +109,6 @@ describe('onRequestInterceptor', () => {
|
|||
elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$;
|
||||
|
||||
initSpacesOnRequestInterceptor({
|
||||
getLegacyAPI: () =>
|
||||
({
|
||||
legacyConfig: {},
|
||||
} as LegacyAPI),
|
||||
http: (http as unknown) as CoreSetup['http'],
|
||||
});
|
||||
|
||||
|
|
|
@ -12,14 +12,12 @@ import {
|
|||
import { format } from 'url';
|
||||
import { DEFAULT_SPACE_ID } from '../../../common/constants';
|
||||
import { modifyUrl } from '../utils/url';
|
||||
import { LegacyAPI } from '../../plugin';
|
||||
import { getSpaceIdFromPath } from '../../../common';
|
||||
|
||||
export interface OnRequestInterceptorDeps {
|
||||
getLegacyAPI(): LegacyAPI;
|
||||
http: CoreSetup['http'];
|
||||
}
|
||||
export function initSpacesOnRequestInterceptor({ getLegacyAPI, http }: OnRequestInterceptorDeps) {
|
||||
export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDeps) {
|
||||
http.registerOnPreAuth(async function spacesOnPreAuthHandler(
|
||||
request: KibanaRequest,
|
||||
response: LifecycleResponseFactory,
|
||||
|
|
|
@ -23,7 +23,6 @@ import { securityMock } from '../../../security/server/mocks';
|
|||
const log = loggingServiceMock.createLogger();
|
||||
|
||||
const legacyAPI: LegacyAPI = {
|
||||
legacyConfig: {},
|
||||
savedObjects: {} as SavedObjectsLegacyService,
|
||||
} as LegacyAPI;
|
||||
|
||||
|
|
72
x-pack/plugins/spaces/server/plugin.test.ts
Normal file
72
x-pack/plugins/spaces/server/plugin.test.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { CoreSetup } from 'src/core/server';
|
||||
import { coreMock } from 'src/core/server/mocks';
|
||||
import { featuresPluginMock } from '../../features/server/mocks';
|
||||
import { licensingMock } from '../../licensing/server/mocks';
|
||||
import { Plugin, PluginsSetup } from './plugin';
|
||||
import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks';
|
||||
|
||||
describe('Spaces Plugin', () => {
|
||||
describe('#setup', () => {
|
||||
it('can setup with all optional plugins disabled, exposing the expected contract', async () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext({});
|
||||
const core = coreMock.createSetup() as CoreSetup<PluginsSetup>;
|
||||
const features = featuresPluginMock.createSetup();
|
||||
const licensing = licensingMock.createSetup();
|
||||
|
||||
const plugin = new Plugin(initializerContext);
|
||||
const spacesSetup = await plugin.setup(core, { features, licensing });
|
||||
expect(spacesSetup).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"__legacyCompat": Object {
|
||||
"createDefaultSpace": [Function],
|
||||
"registerLegacyAPI": [Function],
|
||||
},
|
||||
"spacesService": Object {
|
||||
"getActiveSpace": [Function],
|
||||
"getBasePath": [Function],
|
||||
"getSpaceId": [Function],
|
||||
"isInDefaultSpace": [Function],
|
||||
"namespaceToSpaceId": [Function],
|
||||
"scopedClient": [Function],
|
||||
"spaceIdToNamespace": [Function],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers the capabilities provider and switcher', async () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext({});
|
||||
const core = coreMock.createSetup() as CoreSetup<PluginsSetup>;
|
||||
const features = featuresPluginMock.createSetup();
|
||||
const licensing = licensingMock.createSetup();
|
||||
|
||||
const plugin = new Plugin(initializerContext);
|
||||
|
||||
await plugin.setup(core, { features, licensing });
|
||||
|
||||
expect(core.capabilities.registerProvider).toHaveBeenCalledTimes(1);
|
||||
expect(core.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('registers the usage collector', async () => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext({});
|
||||
const core = coreMock.createSetup() as CoreSetup<PluginsSetup>;
|
||||
const features = featuresPluginMock.createSetup();
|
||||
const licensing = licensingMock.createSetup();
|
||||
|
||||
const usageCollection = usageCollectionPluginMock.createSetupContract();
|
||||
|
||||
const plugin = new Plugin(initializerContext);
|
||||
|
||||
await plugin.setup(core, { features, licensing, usageCollection });
|
||||
|
||||
expect(usageCollection.getCollectorByType('spaces')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -13,7 +13,10 @@ import {
|
|||
Logger,
|
||||
PluginInitializerContext,
|
||||
} from '../../../../src/core/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import {
|
||||
PluginSetupContract as FeaturesPluginSetup,
|
||||
PluginStartContract as FeaturesPluginStart,
|
||||
} from '../../features/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { createDefaultSpace } from './lib/create_default_space';
|
||||
|
@ -22,15 +25,15 @@ import { AuditLogger } from '../../../../server/lib/audit_logger';
|
|||
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 { registerSpacesUsageCollector } from './lib/spaces_usage_collector';
|
||||
import { registerSpacesUsageCollector } from './usage_collection';
|
||||
import { SpacesService } from './spaces_service';
|
||||
import { SpacesServiceSetup } from './spaces_service';
|
||||
import { ConfigType } from './config';
|
||||
import { toggleUICapabilities } from './lib/toggle_ui_capabilities';
|
||||
import { initSpacesRequestInterceptors } from './lib/request_interceptors';
|
||||
import { initExternalSpacesApi } from './routes/api/external';
|
||||
import { initInternalSpacesApi } from './routes/api/internal';
|
||||
import { initSpacesViewsRoutes } from './routes/views';
|
||||
import { setupCapabilities } from './capabilities';
|
||||
|
||||
/**
|
||||
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
|
||||
|
@ -41,9 +44,6 @@ export interface LegacyAPI {
|
|||
auditLogger: {
|
||||
create: (pluginId: string) => AuditLogger;
|
||||
};
|
||||
legacyConfig: {
|
||||
kibanaIndex: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PluginsSetup {
|
||||
|
@ -54,6 +54,10 @@ export interface PluginsSetup {
|
|||
home?: HomeServerPluginSetup;
|
||||
}
|
||||
|
||||
export interface PluginsStart {
|
||||
features: FeaturesPluginStart;
|
||||
}
|
||||
|
||||
export interface SpacesPluginSetup {
|
||||
spacesService: SpacesServiceSetup;
|
||||
__legacyCompat: {
|
||||
|
@ -70,6 +74,8 @@ export class Plugin {
|
|||
|
||||
private readonly config$: Observable<ConfigType>;
|
||||
|
||||
private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>;
|
||||
|
||||
private readonly log: Logger;
|
||||
|
||||
private legacyAPI?: LegacyAPI;
|
||||
|
@ -92,12 +98,16 @@ export class Plugin {
|
|||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config$ = initializerContext.config.create<ConfigType>();
|
||||
this.kibanaIndexConfig$ = initializerContext.config.legacy.globalConfig$;
|
||||
this.log = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public async start() {}
|
||||
|
||||
public async setup(core: CoreSetup, plugins: PluginsSetup): Promise<SpacesPluginSetup> {
|
||||
public async setup(
|
||||
core: CoreSetup<PluginsStart>,
|
||||
plugins: PluginsSetup
|
||||
): Promise<SpacesPluginSetup> {
|
||||
const service = new SpacesService(this.log, this.getLegacyAPI);
|
||||
|
||||
const spacesService = await service.setup({
|
||||
|
@ -131,20 +141,19 @@ export class Plugin {
|
|||
initSpacesRequestInterceptors({
|
||||
http: core.http,
|
||||
log: this.log,
|
||||
getLegacyAPI: this.getLegacyAPI,
|
||||
spacesService,
|
||||
features: plugins.features,
|
||||
});
|
||||
|
||||
core.capabilities.registerSwitcher(async (request, uiCapabilities) => {
|
||||
try {
|
||||
const activeSpace = await spacesService.getActiveSpace(request);
|
||||
const features = plugins.features.getFeatures();
|
||||
return toggleUICapabilities(features, uiCapabilities, activeSpace);
|
||||
} catch (e) {
|
||||
return uiCapabilities;
|
||||
}
|
||||
});
|
||||
setupCapabilities(core, spacesService, this.log);
|
||||
|
||||
if (plugins.usageCollection) {
|
||||
registerSpacesUsageCollector(plugins.usageCollection, {
|
||||
kibanaIndexConfig$: this.kibanaIndexConfig$,
|
||||
features: plugins.features,
|
||||
licensing: plugins.licensing,
|
||||
});
|
||||
}
|
||||
|
||||
if (plugins.security) {
|
||||
plugins.security.registerSpacesService(spacesService);
|
||||
|
@ -161,12 +170,7 @@ export class Plugin {
|
|||
__legacyCompat: {
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
|
||||
this.legacyAPI = legacyAPI;
|
||||
this.setupLegacyComponents(
|
||||
spacesService,
|
||||
plugins.features,
|
||||
plugins.licensing,
|
||||
plugins.usageCollection
|
||||
);
|
||||
this.setupLegacyComponents(spacesService);
|
||||
},
|
||||
createDefaultSpace: async () => {
|
||||
return await createDefaultSpace({
|
||||
|
@ -180,12 +184,7 @@ export class Plugin {
|
|||
|
||||
public stop() {}
|
||||
|
||||
private setupLegacyComponents(
|
||||
spacesService: SpacesServiceSetup,
|
||||
featuresSetup: FeaturesPluginSetup,
|
||||
licensingSetup: LicensingPluginSetup,
|
||||
usageCollectionSetup?: UsageCollectionSetup
|
||||
) {
|
||||
private setupLegacyComponents(spacesService: SpacesServiceSetup) {
|
||||
const legacyAPI = this.getLegacyAPI();
|
||||
const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects;
|
||||
addScopedSavedObjectsClientWrapperFactory(
|
||||
|
@ -193,11 +192,5 @@ export class Plugin {
|
|||
'spaces',
|
||||
spacesSavedObjectsClientWrapperFactory(spacesService, types)
|
||||
);
|
||||
// Register a function with server to manage the collection of usage stats
|
||||
registerSpacesUsageCollector(usageCollectionSetup, {
|
||||
kibanaIndex: legacyAPI.legacyConfig.kibanaIndex,
|
||||
features: featuresSetup,
|
||||
licensing: licensingSetup,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,9 +100,6 @@ export const createLegacyAPI = ({
|
|||
} as unknown) as jest.Mocked<SavedObjectsLegacyService>;
|
||||
|
||||
const legacyAPI: jest.Mocked<LegacyAPI> = {
|
||||
legacyConfig: {
|
||||
kibanaIndex: '',
|
||||
},
|
||||
auditLogger: {} as any,
|
||||
savedObjects: savedObjectsService,
|
||||
};
|
||||
|
|
|
@ -28,7 +28,6 @@ const mockLogger = loggingServiceMock.createLogger();
|
|||
|
||||
const createService = async (serverBasePath: string = '') => {
|
||||
const legacyAPI = {
|
||||
legacyConfig: {},
|
||||
savedObjects: ({
|
||||
getSavedObjectsRepository: jest.fn().mockReturnValue({
|
||||
get: jest.fn().mockImplementation((type, id) => {
|
||||
|
|
7
x-pack/plugins/spaces/server/usage_collection/index.ts
Normal file
7
x-pack/plugins/spaces/server/usage_collection/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 { registerSpacesUsageCollector } from './spaces_usage_collector';
|
|
@ -9,6 +9,7 @@ import * as Rx from 'rxjs';
|
|||
import { PluginsSetup } from '../plugin';
|
||||
import { Feature } from '../../../features/server';
|
||||
import { ILicense, LicensingPluginSetup } from '../../../licensing/server';
|
||||
import { pluginInitializerContextConfigMock } from 'src/core/server/mocks';
|
||||
|
||||
interface SetupOpts {
|
||||
license?: Partial<ILicense>;
|
||||
|
@ -72,7 +73,7 @@ describe('error handling', () => {
|
|||
license: { isAvailable: true, type: 'basic' },
|
||||
});
|
||||
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
|
||||
kibanaIndex: '.kibana',
|
||||
kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }),
|
||||
features,
|
||||
licensing,
|
||||
});
|
||||
|
@ -85,7 +86,7 @@ describe('error handling', () => {
|
|||
license: { isAvailable: true, type: 'basic' },
|
||||
});
|
||||
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
|
||||
kibanaIndex: '.kibana',
|
||||
kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }),
|
||||
features,
|
||||
licensing,
|
||||
});
|
||||
|
@ -105,11 +106,25 @@ describe('with a basic license', () => {
|
|||
license: { isAvailable: true, type: 'basic' },
|
||||
});
|
||||
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
|
||||
kibanaIndex: '.kibana',
|
||||
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
|
||||
features,
|
||||
licensing,
|
||||
});
|
||||
usageStats = await getSpacesUsage(defaultCallClusterMock);
|
||||
|
||||
expect(defaultCallClusterMock).toHaveBeenCalledWith('search', {
|
||||
body: {
|
||||
aggs: {
|
||||
disabledFeatures: {
|
||||
terms: { field: 'space.disabledFeatures', include: ['feature1', 'feature2'], size: 2 },
|
||||
},
|
||||
},
|
||||
query: { term: { type: { value: 'space' } } },
|
||||
size: 0,
|
||||
track_total_hits: true,
|
||||
},
|
||||
index: '.kibana-tests',
|
||||
});
|
||||
});
|
||||
|
||||
test('sets enabled to true', () => {
|
||||
|
@ -139,7 +154,7 @@ describe('with no license', () => {
|
|||
beforeAll(async () => {
|
||||
const { features, licensing, usageCollecion } = setup({ license: { isAvailable: false } });
|
||||
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
|
||||
kibanaIndex: '.kibana',
|
||||
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
|
||||
features,
|
||||
licensing,
|
||||
});
|
||||
|
@ -170,7 +185,7 @@ describe('with platinum license', () => {
|
|||
license: { isAvailable: true, type: 'platinum' },
|
||||
});
|
||||
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
|
||||
kibanaIndex: '.kibana',
|
||||
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
|
||||
features,
|
||||
licensing,
|
||||
});
|
|
@ -4,11 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { CallAPIOptions } from 'src/core/server';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
// @ts-ignore
|
||||
import { Observable } from 'rxjs';
|
||||
import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants';
|
||||
import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants';
|
||||
import { PluginsSetup } from '../plugin';
|
||||
|
@ -85,8 +84,8 @@ async function getSpacesUsage(
|
|||
|
||||
const { hits, aggregations } = resp!;
|
||||
|
||||
const count = get(hits, 'total.value', 0);
|
||||
const disabledFeatureBuckets = get(aggregations, 'disabledFeatures.buckets', []);
|
||||
const count = hits?.total?.value ?? 0;
|
||||
const disabledFeatureBuckets = aggregations?.disabledFeatures?.buckets ?? [];
|
||||
|
||||
const initialCounts = knownFeatureIds.reduce(
|
||||
(acc, featureId) => ({ ...acc, [featureId]: 0 }),
|
||||
|
@ -125,7 +124,7 @@ export interface UsageStats {
|
|||
}
|
||||
|
||||
interface CollectorDeps {
|
||||
kibanaIndex: string;
|
||||
kibanaIndexConfig$: Observable<{ kibana: { index: string } }>;
|
||||
features: PluginsSetup['features'];
|
||||
licensing: PluginsSetup['licensing'];
|
||||
}
|
||||
|
@ -145,12 +144,9 @@ export function getSpacesUsageCollector(
|
|||
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.features,
|
||||
available
|
||||
);
|
||||
const kibanaIndex = (await deps.kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index;
|
||||
|
||||
const usageStats = await getSpacesUsage(callCluster, kibanaIndex, deps.features, available);
|
||||
|
||||
return {
|
||||
available,
|
||||
|
@ -178,12 +174,9 @@ export function getSpacesUsageCollector(
|
|||
}
|
||||
|
||||
export function registerSpacesUsageCollector(
|
||||
usageCollection: UsageCollectionSetup | undefined,
|
||||
usageCollection: UsageCollectionSetup,
|
||||
deps: CollectorDeps
|
||||
) {
|
||||
if (!usageCollection) {
|
||||
return;
|
||||
}
|
||||
const collector = getSpacesUsageCollector(usageCollection, deps);
|
||||
usageCollection.registerCollector(collector);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue