diff --git a/server/multitenancy/tenant_resolver.test.ts b/server/multitenancy/tenant_resolver.test.ts new file mode 100644 index 0000000000..166c3a91dd --- /dev/null +++ b/server/multitenancy/tenant_resolver.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 { httpServerMock } from '../../../../src/core/server/mocks'; +import { OpenSearchDashboardsRequest } from '../../../../src/core/server'; +import { addTenantParameterToResolvedShortLink } from './tenant_resolver'; +import { Request, ResponseObject } from '@hapi/hapi'; + +describe('Preserve the tenant parameter in short urls', () => { + it(`adds the tenant as a query parameter for goto short links`, async () => { + const resolvedUrl = '/url/resolved'; + const rawRequest = httpServerMock.createRawRequest({ + url: { + pathname: '/goto/123', + }, + headers: { + securitytenant: 'dummy_tenant', + }, + response: { + headers: { + location: resolvedUrl, + }, + }, + }) as Request; + + const osRequest = OpenSearchDashboardsRequest.from(rawRequest); + addTenantParameterToResolvedShortLink(osRequest); + + expect((rawRequest.response as ResponseObject).headers.location).toEqual( + resolvedUrl + '?security_tenant=dummy_tenant' + ); + }); + + it(`ignores links not starting with /goto`, async () => { + const resolvedUrl = '/url/resolved'; + const rawRequest = httpServerMock.createRawRequest({ + url: { + pathname: '/dontgoto/123', + }, + headers: { + securitytenant: 'dummy_tenant', + }, + response: { + headers: { + location: resolvedUrl, + }, + }, + }) as Request; + + const osRequest = OpenSearchDashboardsRequest.from(rawRequest); + addTenantParameterToResolvedShortLink(osRequest); + + expect((rawRequest.response as ResponseObject).headers.location).toEqual(resolvedUrl); + }); + + it(`checks that a redirect location is present before applying the query parameter`, async () => { + const rawRequest = httpServerMock.createRawRequest({ + url: { + pathname: '/goto/123', + }, + headers: { + securitytenant: 'dummy_tenant', + }, + response: { + headers: { + someotherheader: 'abc', + }, + }, + }) as Request; + + const osRequest = OpenSearchDashboardsRequest.from(rawRequest); + addTenantParameterToResolvedShortLink(osRequest); + + expect((rawRequest.response as ResponseObject).headers.location).toBeFalsy(); + }); +}); diff --git a/server/multitenancy/tenant_resolver.ts b/server/multitenancy/tenant_resolver.ts index c4ca9d5d45..a8b8b4e5e5 100755 --- a/server/multitenancy/tenant_resolver.ts +++ b/server/multitenancy/tenant_resolver.ts @@ -14,9 +14,14 @@ */ import { isEmpty, findKey, cloneDeep } from 'lodash'; +import { OpenSearchDashboardsRequest } from 'opensearch-dashboards/server'; +import { ResponseObject } from '@hapi/hapi'; import { SecuritySessionCookie } from '../session/security_cookie'; import { SecurityPluginConfigType } from '..'; import { GLOBAL_TENANT_SYMBOL, PRIVATE_TENANT_SYMBOL, globalTenantName } from '../../common'; +import { modifyUrl } from '../../../../packages/osd-std'; +import { ensureRawRequest } from '../../../../src/core/server/http/router'; +import { GOTO_PREFIX } from '../../../../src/plugins/share/common/short_url_routes'; export const PRIVATE_TENANTS: string[] = [PRIVATE_TENANT_SYMBOL, 'private']; export const GLOBAL_TENANTS: string[] = ['global', GLOBAL_TENANT_SYMBOL, 'Global']; @@ -194,3 +199,29 @@ export function resolve( export function isValidTenant(tenant: string | undefined | null): boolean { return tenant !== undefined && tenant !== null; } + +/** + * If multitenancy is enabled & the URL entered starts with /goto, + * We will modify the rawResponse to add a new parameter to the URL, the security_tenant (or value for tenant when in multitenancy) + * With the security_tenant added, the resolved short URL now contains the security_tenant information (so the short URL retains the tenant information). + **/ + +export function addTenantParameterToResolvedShortLink(request: OpenSearchDashboardsRequest) { + if (request.url.pathname.startsWith(`${GOTO_PREFIX}/`)) { + const rawRequest = ensureRawRequest(request); + const rawResponse = rawRequest.response as ResponseObject; + + // Make sure the request really should redirect + if (rawResponse.headers.location) { + const modifiedUrl = modifyUrl(rawResponse.headers.location as string, (parts) => { + if (parts.query.security_tenant === undefined) { + parts.query.security_tenant = request.headers.securitytenant as string; + } + // Mutating the headers toolkit.next({headers: ...}) logs a warning about headers being overwritten + }); + rawResponse.headers.location = modifiedUrl; + } + } + + return request; +} diff --git a/server/plugin.ts b/server/plugin.ts index 84d94e4f17..c6aec6e583 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -15,6 +15,7 @@ import { first } from 'rxjs/operators'; import { Observable } from 'rxjs'; +import { ResponseObject } from '@hapi/hapi'; import { PluginInitializerContext, CoreSetup, @@ -47,6 +48,7 @@ import { setupMultitenantRoutes } from './multitenancy/routes'; import { defineAuthTypeRoutes } from './routes/auth_type_routes'; import { createMigrationOpenSearchClient } from '../../../src/core/server/saved_objects/migrations/core'; import { SecuritySavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper'; +import { addTenantParameterToResolvedShortLink } from './multitenancy/tenant_resolver'; export interface SecurityPluginRequestContext { logger: Logger; @@ -125,6 +127,14 @@ export class SecurityPlugin implements Plugin { + addTenantParameterToResolvedShortLink(request); + return toolkit.next(); + }); + } + // Register server side APIs defineRoutes(router); defineAuthTypeRoutes(router, config);