From 91330d24933345e63dad8357a52d3a03168dba7f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 28 Feb 2020 09:06:48 -0500 Subject: [PATCH] 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 --- src/core/server/http/http_server.mocks.ts | 6 +- src/plugins/usage_collection/server/mocks.ts | 38 +++++++ x-pack/legacy/plugins/spaces/index.ts | 18 --- x-pack/plugins/features/server/index.ts | 2 +- x-pack/plugins/features/server/mocks.ts | 27 +++++ x-pack/plugins/features/server/plugin.ts | 23 ++-- .../capabilities_provider.test.ts | 24 ++++ .../capabilities/capabilities_provider.ts | 16 +++ .../capabilities_switcher.test.ts} | 104 ++++++++++++++++-- .../capabilities_switcher.ts} | 40 +++++-- .../spaces/server/capabilities/index.ts | 20 ++++ .../on_post_auth_interceptor.test.ts | 2 - .../on_post_auth_interceptor.ts | 4 +- .../on_request_interceptor.test.ts | 5 - .../on_request_interceptor.ts | 4 +- .../spaces_tutorial_context_factory.test.ts | 1 - x-pack/plugins/spaces/server/plugin.test.ts | 72 ++++++++++++ x-pack/plugins/spaces/server/plugin.ts | 63 +++++------ .../api/__fixtures__/create_legacy_api.ts | 3 - .../spaces_service/spaces_service.test.ts | 1 - .../spaces/server/usage_collection/index.ts | 7 ++ .../spaces_usage_collector.test.ts | 25 ++++- .../spaces_usage_collector.ts | 23 ++-- 23 files changed, 409 insertions(+), 119 deletions(-) create mode 100644 src/plugins/usage_collection/server/mocks.ts create mode 100644 x-pack/plugins/features/server/mocks.ts create mode 100644 x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts create mode 100644 x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts rename x-pack/plugins/spaces/server/{lib/toggle_ui_capabilities.test.ts => capabilities/capabilities_switcher.test.ts} (53%) rename x-pack/plugins/spaces/server/{lib/toggle_ui_capabilities.ts => capabilities/capabilities_switcher.ts} (66%) create mode 100644 x-pack/plugins/spaces/server/capabilities/index.ts create mode 100644 x-pack/plugins/spaces/server/plugin.test.ts create mode 100644 x-pack/plugins/spaces/server/usage_collection/index.ts rename x-pack/plugins/spaces/server/{lib => usage_collection}/spaces_usage_collector.test.ts (85%) rename x-pack/plugins/spaces/server/{lib => usage_collection}/spaces_usage_collector.ts (90%) diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index c586cf6a9825f..0a9541393284e 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -43,6 +43,7 @@ interface RequestFixtureOptions

{ method?: RouteMethod; socket?: Socket; routeTags?: string[]; + routeAuthRequired?: false; validation?: { params?: RouteValidationSpec

; query?: RouteValidationSpec; @@ -59,6 +60,7 @@ function createKibanaRequestMock

({ method = 'get', socket = new Socket(), routeTags, + routeAuthRequired, validation = {}, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); @@ -77,7 +79,9 @@ function createKibanaRequestMock

({ query: queryString, search: queryString ? `?${queryString}` : queryString, }, - route: { settings: { tags: routeTags } }, + route: { + settings: { tags: routeTags, auth: routeAuthRequired }, + }, raw: { req: { socket }, }, diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts new file mode 100644 index 0000000000000..2194b1fb83f6e --- /dev/null +++ b/src/plugins/usage_collection/server/mocks.ts @@ -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, +}; diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index ab3388ae96475..757c1eb557c54 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -34,19 +34,6 @@ export const spaces = (kibana: Record) => 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) => 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) => diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 2b4f85aa04f04..48ef97a494f7e 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -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); diff --git a/x-pack/plugins/features/server/mocks.ts b/x-pack/plugins/features/server/mocks.ts new file mode 100644 index 0000000000000..ebaa5f1a504ca --- /dev/null +++ b/x-pack/plugins/features/server/mocks.ts @@ -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 => { + return { + getFeatures: jest.fn(), + getFeaturesUICapabilities: jest.fn(), + registerFeature: jest.fn(), + registerLegacyAPI: jest.fn(), + }; +}; + +const createStart = (): jest.Mocked => { + return { + getFeatures: jest.fn(), + }; +}; + +export const featuresPluginMock = { + createSetup, + createStart, +}; diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 96a8e68f8326d..e77fa218c0681 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -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> { - 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 { this.logger.debug('Starting plugin'); + return deepFreeze({ + getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + }); } public stop() { diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts new file mode 100644 index 0000000000000..8678bdceb70f9 --- /dev/null +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts @@ -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, + }, + } + `); + }); +}); diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts new file mode 100644 index 0000000000000..5976aabfa66e8 --- /dev/null +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts @@ -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, + }, + }, +}); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts similarity index 53% rename from x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts rename to x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index b92922def2eb8..3f7b93c754aef 100644 --- a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -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, + 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('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', () => { + 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(); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts similarity index 66% rename from x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts rename to x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 2de84ec05017b..317cc7fe0e3c3 100644 --- a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -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, + 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; diff --git a/x-pack/plugins/spaces/server/capabilities/index.ts b/x-pack/plugins/spaces/server/capabilities/index.ts new file mode 100644 index 0000000000000..56a72a2eeaf19 --- /dev/null +++ b/x-pack/plugins/spaces/server/capabilities/index.ts @@ -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, + spacesService: SpacesServiceSetup, + logger: Logger +) => { + core.capabilities.registerProvider(capabilitiesProvider); + core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, spacesService, logger)); +}; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 92be88b91c652..61157a9318781 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -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, diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 4674f3641084a..b07ff11f6efc6 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -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, diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 5e6cf67ee8c90..448bc39eb606e 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -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'], }); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 22d704c1b7e13..c59851f8b8061 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -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, diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index a3396e98c3512..094ca8a11816e 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -23,7 +23,6 @@ import { securityMock } from '../../../security/server/mocks'; const log = loggingServiceMock.createLogger(); const legacyAPI: LegacyAPI = { - legacyConfig: {}, savedObjects: {} as SavedObjectsLegacyService, } as LegacyAPI; diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts new file mode 100644 index 0000000000000..4e3f4f52cbeb4 --- /dev/null +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -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; + 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; + 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; + 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(); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 90c2da6e69df8..d125e0f54e9c1 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -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; + 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(); + this.kibanaIndexConfig$ = initializerContext.config.legacy.globalConfig$; this.log = initializerContext.logger.get(); } public async start() {} - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public async setup( + core: CoreSetup, + plugins: PluginsSetup + ): Promise { 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, - }); } } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts index 812b02e94f591..7765cc3c52e96 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts @@ -100,9 +100,6 @@ export const createLegacyAPI = ({ } as unknown) as jest.Mocked; const legacyAPI: jest.Mocked = { - legacyConfig: { - kibanaIndex: '', - }, auditLogger: {} as any, savedObjects: savedObjectsService, }; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index 68d096e046ed4..fc5ff39780524 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -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) => { diff --git a/x-pack/plugins/spaces/server/usage_collection/index.ts b/x-pack/plugins/spaces/server/usage_collection/index.ts new file mode 100644 index 0000000000000..01df2b815f5ff --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_collection/index.ts @@ -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'; diff --git a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts similarity index 85% rename from x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts rename to x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index c0a6a152c8322..57ec688ab70e8 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -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; @@ -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, }); diff --git a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts similarity index 90% rename from x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts rename to x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index af77f2d3a72ba..90187b7853185 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -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); }