diff --git a/common/index.ts b/common/index.ts index b7626df83..4fd01c8d7 100644 --- a/common/index.ts +++ b/common/index.ts @@ -31,6 +31,13 @@ export const ERROR_MISSING_ROLE_PATH = '/missing-role'; export const MAX_INTEGER = 2147483647; +export const GLOBAL_TENANT_SYMBOL = ''; +export const PRIVATE_TENANT_SYMBOL = '__user__'; +export const DEFAULT_TENANT = 'default'; +export const GLOBAL_TENANT_RENDERING_TEXT = 'Global'; +export const PRIVATE_TENANT_RENDERING_TEXT = 'Private'; +export const globalTenantName = 'global_tenant'; + export enum AuthType { BASIC = 'basicauth', OPEN_ID = 'openid', @@ -48,3 +55,7 @@ export function isValidResourceName(resourceName: string): boolean { // see: https://javascript.info/regexp-unicode return !/[\p{C}%]/u.test(resourceName) && resourceName.length > 0; } + +export function isGlobalTenant(selectedTenant: string | null) { + return selectedTenant !== null && selectedTenant === GLOBAL_TENANT_SYMBOL; +} diff --git a/server/multitenancy/tenant_resolver.test.ts b/server/multitenancy/tenant_resolver.test.ts new file mode 100644 index 000000000..166c3a91d --- /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 f0aa4548f..6d787e0cc 100644 --- a/server/multitenancy/tenant_resolver.ts +++ b/server/multitenancy/tenant_resolver.ts @@ -14,13 +14,15 @@ */ import { isEmpty, findKey, cloneDeep } from 'lodash'; -import { OpenSearchDashboardsRequest } from '../../../../src/core/server'; +import { OpenSearchDashboardsRequest } from 'opensearch-dashboards/server'; +import { ResponseObject } from '@hapi/hapi'; import { SecuritySessionCookie } from '../session/security_cookie'; +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'; import { SecurityPluginConfigType } from '..'; -const PRIVATE_TENANT_SYMBOL: string = '__user__'; -const GLOBAL_TENANT_SYMBOL: string = ''; - export const PRIVATE_TENANTS: string[] = [PRIVATE_TENANT_SYMBOL, 'private']; export const GLOBAL_TENANTS: string[] = ['global', GLOBAL_TENANT_SYMBOL]; /** @@ -170,3 +172,29 @@ 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 8c04eb275..605efd9aa 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, @@ -43,6 +44,7 @@ import { getAuthenticationHandler } from './auth/auth_handler_factory'; import { setupMultitenantRoutes } from './multitenancy/routes'; import { defineAuthTypeRoutes } from './routes/auth_type_routes'; import { createMigrationOpenSearchClient } from '../../../src/core/server/saved_objects/migrations/core'; +import { addTenantParameterToResolvedShortLink } from './multitenancy/tenant_resolver'; export interface SecurityPluginRequestContext { logger: Logger; @@ -118,6 +120,14 @@ export class SecurityPlugin implements Plugin { + addTenantParameterToResolvedShortLink(request); + return toolkit.next(); + }); + } + // Register server side APIs defineRoutes(router); defineAuthTypeRoutes(router, config);