diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 264197994180..ceac62858caa 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -10,6 +10,7 @@ - [Client_management_design](multi-datasource/client_management_design.md) - [High_level_design](multi-datasource/high_level_design.md) - [User_stories](multi-datasource/user_stories.md) + - [Openapi](openapi/README.md) - Plugins - [Data_persistence](plugins/data_persistence.md) - Saved_objects @@ -159,6 +160,7 @@ - [Opensearch dashboards.release notes 2.11.1](../release-notes/opensearch-dashboards.release-notes-2.11.1.md) - [Opensearch dashboards.release notes 2.12.0](../release-notes/opensearch-dashboards.release-notes-2.12.0.md) - [Opensearch dashboards.release notes 2.13.0](../release-notes/opensearch-dashboards.release-notes-2.13.0.md) + - [Opensearch dashboards.release notes 2.14.0](../release-notes/opensearch-dashboards.release-notes-2.14.0.md) - [Opensearch dashboards.release notes 2.2.0](../release-notes/opensearch-dashboards.release-notes-2.2.0.md) - [Opensearch dashboards.release notes 2.2.1](../release-notes/opensearch-dashboards.release-notes-2.2.1.md) - [Opensearch dashboards.release notes 2.3.0](../release-notes/opensearch-dashboards.release-notes-2.3.0.md) @@ -172,6 +174,7 @@ - scripts - [README](../scripts/README.md) - [DOCS_README](DOCS_README.md) + - [Theme](theme.md) - [CHANGELOG](../CHANGELOG.md) - [CODE_OF_CONDUCT](../CODE_OF_CONDUCT.md) - [COMMUNICATIONS](../COMMUNICATIONS.md) diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 4db4c4fac17f..d4398baf2702 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -172,7 +172,7 @@ test('valid params', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -202,7 +202,7 @@ test('invalid params', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -237,7 +237,7 @@ test('valid query', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -267,7 +267,7 @@ test('invalid query', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -302,7 +302,7 @@ test('valid body', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -340,7 +340,7 @@ test('valid body with validate function', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -383,7 +383,7 @@ test('not inline validation - specifying params', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -426,7 +426,7 @@ test('not inline validation - specifying validation handler', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -476,7 +476,7 @@ test('not inline handler - OpenSearchDashboardsRequest', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -525,7 +525,7 @@ test('not inline handler - RequestHandler', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -559,7 +559,7 @@ test('invalid body', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -594,7 +594,7 @@ test('handles putting', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -625,7 +625,7 @@ test('handles deleting', async () => { ); const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); @@ -655,7 +655,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { ); const { registerRouter, server: innerServer } = await server.setup(configWithBasePath); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); innerServerListener = innerServer.listener; @@ -710,7 +710,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { ); const { registerRouter, server: innerServer } = await server.setup(configWithBasePath); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); innerServerListener = innerServer.listener; @@ -757,7 +757,7 @@ test('with defined `redirectHttpFromPort`', async () => { router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'value:/' })); const { registerRouter } = await server.setup(configWithSSL); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); }); @@ -790,7 +790,7 @@ test('allows attaching metadata to attach meta-data tag strings to a route', asy router.get({ path: '/without-tags', validate: false }, (context, req, res) => res.ok({ body: { tags: req.route.options.tags } }) ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener).get('/with-tags').expect(200, { tags }); @@ -803,7 +803,7 @@ test('exposes route details of incoming request to a route handler', async () => const router = new Router('', logger, enhanceWithContext); router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: req.route })); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener) @@ -830,7 +830,7 @@ describe('conditional compression', () => { headers: { 'Content-Type': 'text/html; charset=UTF-8' }, }; router.get({ path: '/', validate: false }, (_context, _req, res) => res.ok(largeRequest)); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); return innerServer.listener; } @@ -908,7 +908,7 @@ describe('conditional compression', () => { router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: req.route }) ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); const response = await supertest(innerServer.listener) @@ -927,7 +927,7 @@ describe('conditional compression', () => { router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: req.route }) ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); const response = await supertest(innerServer.listener).get('/').expect(200); @@ -957,7 +957,7 @@ test('exposes route details of incoming request to a route handler (POST + paylo }, (context, req, res) => res.ok({ body: req.route }) ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener) @@ -996,7 +996,7 @@ describe('body options', () => { }, (context, req, res) => res.ok({ body: req.route }) ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener).post('/').send({ test: 1 }).expect(415, { @@ -1018,7 +1018,7 @@ describe('body options', () => { }, (context, req, res) => res.ok({ body: req.route }) ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener).post('/').send({ test: 1 }).expect(413, { @@ -1048,7 +1048,7 @@ describe('body options', () => { } } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener).post('/').send({ test: 1 }).expect(200, { @@ -1087,7 +1087,7 @@ describe('timeout options', () => { } } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener) .post('/') @@ -1125,7 +1125,7 @@ describe('timeout options', () => { } } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener) .delete('/') @@ -1162,7 +1162,7 @@ describe('timeout options', () => { } } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener) .put('/') @@ -1199,7 +1199,7 @@ describe('timeout options', () => { } } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener) .patch('/') @@ -1232,7 +1232,7 @@ describe('timeout options', () => { }); } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener) @@ -1266,7 +1266,7 @@ describe('timeout options', () => { }); } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener) @@ -1300,7 +1300,7 @@ describe('timeout options', () => { } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); }); @@ -1325,7 +1325,7 @@ test('should return a stream in the body', async () => { } } ); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); await supertest(innerServer.listener).put('/').send({ test: 1 }).expect(200, { diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index c0d2caaab727..20a0ff2d9ee2 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -55,6 +55,7 @@ import { IsAuthenticated, AuthStateStorage, GetAuthState } from './auth_state_st import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; import { BasePath } from './base_path_service'; import { HttpServiceSetup, HttpServerInfo } from './types'; +import { PluginOpaqueId } from '../plugins'; /** @internal */ export interface HttpServerSetup { @@ -63,7 +64,8 @@ export interface HttpServerSetup { * Add all the routes registered with `router` to HTTP server request listeners. * @param router {@link IRouter} - a router with registered route handlers. */ - registerRouter: (router: IRouter) => void; + getRouter: (pluginId: PluginOpaqueId) => Set | undefined; + registerRouter: (pluginId: PluginOpaqueId, router: IRouter) => void; registerStaticDir: (path: string, dirPath: string) => void; basePath: HttpServiceSetup['basePath']; csp: HttpServiceSetup['csp']; @@ -94,7 +96,8 @@ export type LifecycleRegistrar = Pick< export class HttpServer { private server?: Server; private config?: HttpConfig; - private registeredRouters = new Set(); + // private registeredRouters = new Set(); + private registeredRouters = new Map>(); private authRegistered = false; private cookieSessionStorageCreated = false; private stopped = false; @@ -115,12 +118,24 @@ export class HttpServer { return this.server !== undefined && this.server.listener.listening; } - private registerRouter(router: IRouter) { + private registerRouter(pluginId: PluginOpaqueId, router: IRouter) { if (this.isListening()) { throw new Error('Routers can be registered only when HTTP server is stopped.'); } - this.registeredRouters.add(router); + const existingRouters = this.registeredRouters.get(String(pluginId)); + if (existingRouters) { + existingRouters.add(router); + this.registeredRouters.set(String(pluginId), existingRouters); + } else { + const routerSet: Set = new Set(); + routerSet.add(router); + this.registeredRouters.set(String(pluginId), routerSet); + } + } + + private getRouter(pluginId: PluginOpaqueId) { + return this.registeredRouters.get(String(pluginId)); } public async setup(config: HttpConfig): Promise { @@ -136,6 +151,7 @@ export class HttpServer { this.setupRequestStateAssignment(config); return { + getRouter: this.getRouter.bind(this), registerRouter: this.registerRouter.bind(this), registerStaticDir: this.registerStaticDir.bind(this), registerOnPreRouting: this.registerOnPreRouting.bind(this), @@ -175,47 +191,51 @@ export class HttpServer { } this.log.debug('starting http server'); - for (const router of this.registeredRouters) { - for (const route of router.getRoutes()) { - this.log.debug(`registering route handler for [${route.path}]`); - // Hapi does not allow payload validation to be specified for 'head' or 'get' requests - const validate = isSafeMethod(route.method) ? undefined : { payload: true }; - const { authRequired, tags, body = {}, timeout } = route.options; - const { accepts: allow, maxBytes, output, parse } = body; - - const opensearchDashboardsRouteOptions: OpenSearchDashboardsRouteOptions = { - xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method), - }; - - this.server.route({ - handler: route.handler, - method: route.method, - path: route.path, - options: { - auth: this.getAuthOption(authRequired), - app: opensearchDashboardsRouteOptions, - tags: tags ? Array.from(tags) : undefined, - // TODO: This 'validate' section can be removed once the legacy platform is completely removed. - // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default - // validation applied in ./http_tools#getServerOptions - // (All NP routes are already required to specify their own validation in order to access the payload) - validate, - // @ts-expect-error Types are outdated and doesn't allow `payload.multipart` to be `true` - payload: [allow, maxBytes, output, parse, timeout?.payload].some((x) => x !== undefined) - ? { - allow, - maxBytes, - output, - parse, - timeout: timeout?.payload, - multipart: true, - } - : undefined, - timeout: { - socket: timeout?.idleSocket ?? this.config!.socketTimeout, + for (const [_, routers] of this.registeredRouters) { + for (const router of routers) { + for (const route of router.getRoutes()) { + this.log.debug(`registering route handler for [${route.path}]`); + // Hapi does not allow payload validation to be specified for 'head' or 'get' requests + const validate = isSafeMethod(route.method) ? undefined : { payload: true }; + const { authRequired, tags, body = {}, timeout } = route.options; + const { accepts: allow, maxBytes, output, parse } = body; + + const opensearchDashboardsRouteOptions: OpenSearchDashboardsRouteOptions = { + xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method), + }; + + this.server.route({ + handler: route.handler, + method: route.method, + path: route.path, + options: { + auth: this.getAuthOption(authRequired), + app: opensearchDashboardsRouteOptions, + tags: tags ? Array.from(tags) : undefined, + // TODO: This 'validate' section can be removed once the legacy platform is completely removed. + // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default + // validation applied in ./http_tools#getServerOptions + // (All NP routes are already required to specify their own validation in order to access the payload) + validate, + // @ts-expect-error Types are outdated and doesn't allow `payload.multipart` to be `true` + payload: [allow, maxBytes, output, parse, timeout?.payload].some( + (x) => x !== undefined + ) + ? { + allow, + maxBytes, + output, + parse, + timeout: timeout?.payload, + multipart: true, + } + : undefined, + timeout: { + socket: timeout?.idleSocket ?? this.config!.socketTimeout, + }, }, - }, - }); + }); + } } } diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index ed1da8754721..63f0df5e36b5 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -107,7 +107,7 @@ export class HttpService await this.runNotReadyServer(config); } - const { registerRouter, ...serverContract } = await this.httpServer.setup(config); + const { registerRouter, getRouter, ...serverContract } = await this.httpServer.setup(config); registerCoreHandlers(serverContract, config, this.env); @@ -115,9 +115,18 @@ export class HttpService ...serverContract, createRouter: (path: string, pluginId: PluginOpaqueId = this.coreContext.coreId) => { + const existingRouter = getRouter(pluginId); + if ( + !!existingRouter && + existingRouter.size === 1 && + (String(pluginId) === 'Symbol(securityAdminDashboards)' || + String(pluginId) === 'Symbol(securityDashboards)') + ) { + return existingRouter.values().next().value; + } const enhanceHandler = this.requestHandlerContext!.createHandler.bind(null, pluginId); const router = new Router(path, this.log, enhanceHandler); - registerRouter(router); + registerRouter(pluginId, router); return router; }, diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index 0759e4f2430d..ff6a3c77978b 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -119,7 +119,7 @@ describe('timeouts', () => { ipAllowlist: [], }, } as any); - registerRouter(router); + registerRouter(Symbol(), router); await server.start(); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index c0eb5b29bb63..19f29d240c0b 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -47,7 +47,7 @@ import { } from '../opensearch_dashboards_config'; import { OpenSearchConfigType, config as opensearchConfig } from '../opensearch/opensearch_config'; import { SavedObjectsConfigType, savedObjectsConfig } from '../saved_objects/saved_objects_config'; -import { CoreSetup, CoreStart } from '..'; +import { CoreSetup, CoreStart, IRouter } from '..'; export interface InstanceInfo { uuid: string; @@ -161,7 +161,12 @@ export function createPluginSetupContext( deps: PluginsServiceSetupDeps, plugin: PluginWrapper ): CoreSetup { - const router = deps.http.createRouter('', plugin.opaqueId); + let router: IRouter; + if (String(plugin.opaqueId) === 'Symbol(securityAdminDashboards)') { + router = deps.http.createRouter('', Symbol('securityDashboards')); + } else { + router = deps.http.createRouter('', plugin.opaqueId); + } return { capabilities: { diff --git a/src/core/utils/context.ts b/src/core/utils/context.ts index 6109cb70eafc..9465b269a0d3 100644 --- a/src/core/utils/context.ts +++ b/src/core/utils/context.ts @@ -214,7 +214,7 @@ export class ContextContainer> } >(); /** Used to keep track of which plugins registered which contexts for dependency resolution. */ - private readonly contextNamesBySource: Map>>; + private readonly contextNamesBySource: Map>>; /** * @param pluginDependencies - A map of plugins to an array of their dependencies. @@ -223,8 +223,8 @@ export class ContextContainer> private readonly pluginDependencies: ReadonlyMap, private readonly coreId: CoreId ) { - this.contextNamesBySource = new Map>>([ - [coreId, []], + this.contextNamesBySource = new Map>>([ + [String(coreId), []], ]); } @@ -233,16 +233,20 @@ export class ContextContainer> contextName: TContextName, provider: IContextProvider ): this => { - if (this.contextProviders.has(contextName)) { - throw new Error(`Context provider for ${contextName} has already been registered.`); + // if (this.contextProviders.has(contextName)) { + // throw new Error(`Context provider for ${contextName} has already been registered.`); + // } + const pluginIdSet = new Set(); + for (const pluginId of this.pluginDependencies.keys()) { + pluginIdSet.add(String(pluginId)); } - if (source !== this.coreId && !this.pluginDependencies.has(source)) { + if (source !== this.coreId && !pluginIdSet.has(String(source))) { throw new Error(`Cannot register context for unknown plugin: ${source.toString()}`); } this.contextProviders.set(contextName, { provider, source }); - this.contextNamesBySource.set(source, [ - ...(this.contextNamesBySource.get(source) || []), + this.contextNamesBySource.set(String(source), [ + ...(this.contextNamesBySource.get(String(source)) || []), contextName, ]); @@ -250,7 +254,11 @@ export class ContextContainer> }; public createHandler = (source: symbol, handler: THandler) => { - if (source !== this.coreId && !this.pluginDependencies.has(source)) { + const pluginIdSet = new Set(); + for (const pluginId of this.pluginDependencies.keys()) { + pluginIdSet.add(String(pluginId)); + } + if (source !== this.coreId && !pluginIdSet.has(String(source))) { throw new Error(`Cannot create handler for unknown plugin: ${source.toString()}`); } @@ -265,7 +273,7 @@ export class ContextContainer> ...contextArgs: HandlerParameters ): Promise> { const contextsToBuild: ReadonlySet> = new Set( - this.getContextNamesForSource(source) + this.getContextNamesForSource(String(source)) ); return [...this.contextProviders] @@ -277,7 +285,7 @@ export class ContextContainer> // For the next provider, only expose the context available based on the dependencies of the plugin that // registered that provider. const exposedContext = pick(resolvedContext, [ - ...this.getContextNamesForSource(providerSource), + ...this.getContextNamesForSource(String(providerSource)), ]) as PartialExceptFor, 'core'>; return { @@ -288,9 +296,9 @@ export class ContextContainer> } private getContextNamesForSource( - source: symbol + source: string ): ReadonlySet> { - if (source === this.coreId) { + if (String(source) === String(this.coreId)) { return this.getContextNamesForCore(); } else { return this.getContextNamesForPluginId(source); @@ -298,12 +306,18 @@ export class ContextContainer> } private getContextNamesForCore() { - return new Set(this.contextNamesBySource.get(this.coreId)!); + return new Set(this.contextNamesBySource.get(String(this.coreId))!); } - private getContextNamesForPluginId(pluginId: symbol) { + private getContextNamesForPluginId(pluginId: string) { // If the source is a plugin... - const pluginDeps = this.pluginDependencies.get(pluginId); + let pluginDepsKey: symbol = Symbol('not-exists'); + for (const pluginKey of this.pluginDependencies.keys()) { + if (pluginId === String(pluginKey)) { + pluginDepsKey = pluginKey; + } + } + const pluginDeps = this.pluginDependencies.get(pluginDepsKey); if (!pluginDeps) { // This case should never be hit, but let's be safe. throw new Error(`Cannot create context for unknown plugin: ${pluginId.toString()}`); @@ -311,11 +325,11 @@ export class ContextContainer> return new Set([ // Core contexts - ...this.contextNamesBySource.get(this.coreId)!, + ...this.contextNamesBySource.get(String(this.coreId))!, // Contexts source created ...(this.contextNamesBySource.get(pluginId) || []), // Contexts sources's dependencies created - ...flatten(pluginDeps.map((p) => this.contextNamesBySource.get(p) || [])), + ...flatten(pluginDeps.map((p) => this.contextNamesBySource.get(String(p)) || [])), ]); } }