From dc050835d1ffb0192a83c6a3f0fed97e36898547 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Fri, 8 Dec 2023 11:33:47 -0500 Subject: [PATCH 1/3] [1.3] Add 1.3.14 release notes (#1690) Signed-off-by: Derek Ho --- ...h-security-dashboards-plugin.release-notes-1.3.14.0.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 release-notes/opensearch-security-dashboards-plugin.release-notes-1.3.14.0.md diff --git a/release-notes/opensearch-security-dashboards-plugin.release-notes-1.3.14.0.md b/release-notes/opensearch-security-dashboards-plugin.release-notes-1.3.14.0.md new file mode 100644 index 000000000..eea854aed --- /dev/null +++ b/release-notes/opensearch-security-dashboards-plugin.release-notes-1.3.14.0.md @@ -0,0 +1,8 @@ +## 2023-12-08 Version 1.3.14.0 + +Compatible with OpenSearch-Dashboards 1.3.14 + +### Maintenance + +* Update `yarn.lock` file ([#1669](https://github.com/opensearch-project/security-dashboards-plugin/pull/1669)) +* Bump `debug` to `4.3.4` and `browserify-sign` to `4.2.2` to address CVEs ([#1674](https://github.com/opensearch-project/security-dashboards-plugin/pull/1674)) From fce71db34db4da56d1e8345a22c62bdd22fefb44 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:33:59 -0500 Subject: [PATCH 2/3] Increment version to 1.3.15.0 (#1698) Signed-off-by: opensearch-ci-bot Co-authored-by: opensearch-ci-bot (cherry picked from commit 81910ea688f0446fec4aa64ba8c5136eabf74f08) --- opensearch_dashboards.json | 4 ++-- package.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index e9ab46d2a..9c98e59b4 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -1,7 +1,7 @@ { "id": "securityDashboards", - "version": "1.3.14.0", - "opensearchDashboardsVersion": "1.3.14", + "version": "1.3.15.0", + "opensearchDashboardsVersion": "1.3.15", "configPath": [ "opensearch_security" ], diff --git a/package.json b/package.json index 2f11bcfa0..2369c2434 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "opensearch-security-dashboards", - "version": "1.3.14.0", + "version": "1.3.15.0", "main": "target/plugins/opensearch_security_dashboards", "opensearchDashboards": { - "version": "1.3.14", - "templateVersion": "1.3.14" + "version": "1.3.15", + "templateVersion": "1.3.15" }, "license": "Apache-2.0", "homepage": "https://github.com/opensearch-project/security-dashboards-plugin", From 5f3efab32876b629e4f1b418f33a04b80767122a Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 19 Mar 2024 16:20:42 -0400 Subject: [PATCH 3/3] [Backport 1.3] Split up a value into multiple cookie payload (#1831) Signed-off-by: Craig Perkins (cherry picked from commit 6e581cc7a668bc200fe55f2b944cbc0144eec483) --- .github/workflows/integration-test.yml | 2 +- common/index.ts | 3 +- server/auth/types/authentication_type.ts | 18 +- server/auth/types/openid/openid_auth.test.ts | 158 ++++++++++++ server/auth/types/openid/openid_auth.ts | 95 +++++++- server/auth/types/openid/routes.ts | 44 +++- server/auth/types/saml/routes.ts | 45 +++- server/auth/types/saml/saml_auth.test.ts | 145 +++++++++++ server/auth/types/saml/saml_auth.ts | 93 +++++++- server/index.ts | 14 ++ server/session/cookie_splitter.test.ts | 239 +++++++++++++++++++ server/session/cookie_splitter.ts | 181 ++++++++++++++ server/utils/compression.test.ts | 28 +++ server/utils/compression.ts | 28 +++ test/jest_integration/saml_auth.test.ts | 14 +- 15 files changed, 1073 insertions(+), 34 deletions(-) create mode 100644 server/auth/types/openid/openid_auth.test.ts create mode 100644 server/auth/types/saml/saml_auth.test.ts create mode 100644 server/session/cookie_splitter.test.ts create mode 100644 server/session/cookie_splitter.ts create mode 100644 server/utils/compression.test.ts create mode 100644 server/utils/compression.ts diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 9c43ab0a7..7eddb6b49 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -59,7 +59,7 @@ jobs: plugin-version: ${{ env.PLUGIN_VERSION }} - name: Run Opensearch with A Single Plugin - uses: opensearch-project/security/.github/actions/start-opensearch-with-one-plugin@main + uses: opensearch-project/security/.github/actions/start-opensearch-with-one-plugin@1.3 with: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} diff --git a/common/index.ts b/common/index.ts index 4fd01c8d7..fa60375c8 100644 --- a/common/index.ts +++ b/common/index.ts @@ -37,7 +37,8 @@ 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 const MAX_LENGTH_OF_COOKIE_BYTES = 4000; +export const ESTIMATED_IRON_COOKIE_OVERHEAD = 1.5; export enum AuthType { BASIC = 'basicauth', OPEN_ID = 'openid', diff --git a/server/auth/types/authentication_type.ts b/server/auth/types/authentication_type.ts index 53df57309..ca4ae5bdb 100644 --- a/server/auth/types/authentication_type.ts +++ b/server/auth/types/authentication_type.ts @@ -118,7 +118,7 @@ export abstract class AuthenticationType implements IAuthenticationType { cookie = undefined; } - if (!cookie || !(await this.isValidCookie(cookie))) { + if (!cookie || !(await this.isValidCookie(cookie, request))) { // clear cookie this.sessionStorageFactory.asScoped(request).clear(); @@ -140,7 +140,7 @@ export abstract class AuthenticationType implements IAuthenticationType { } // cookie is valid // build auth header - const authHeadersFromCookie = this.buildAuthHeaderFromCookie(cookie!); + const authHeadersFromCookie = this.buildAuthHeaderFromCookie(cookie!, request); Object.assign(authHeaders, authHeadersFromCookie); const additonalAuthHeader = this.getAdditionalAuthHeader(request); Object.assign(authHeaders, additonalAuthHeader); @@ -236,11 +236,21 @@ export abstract class AuthenticationType implements IAuthenticationType { request: OpenSearchDashboardsRequest, authInfo: any ): SecuritySessionCookie; - protected abstract async isValidCookie(cookie: SecuritySessionCookie): Promise; + + public abstract isValidCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): Promise; + protected abstract handleUnauthedRequest( request: OpenSearchDashboardsRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit ): IOpenSearchDashboardsResponse | AuthResult; - protected abstract buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any; + + public abstract buildAuthHeaderFromCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): any; + public abstract init(): Promise; } diff --git a/server/auth/types/openid/openid_auth.test.ts b/server/auth/types/openid/openid_auth.test.ts new file mode 100644 index 000000000..24d986bac --- /dev/null +++ b/server/auth/types/openid/openid_auth.test.ts @@ -0,0 +1,158 @@ +/* + * 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/http/http_server.mocks'; + +import { OpenSearchDashboardsRequest } from '../../../../../../src/core/server/http/router'; + +import { OpenIdAuthentication } from './openid_auth'; +import { SecurityPluginConfigType } from '../../../index'; +import { SecuritySessionCookie } from '../../../session/security_cookie'; +import { deflateValue } from '../../../utils/compression'; +import { + IRouter, + CoreSetup, + ILegacyClusterClient, + Logger, + SessionStorageFactory, +} from '../../../../../../src/core/server'; + +describe('test OpenId authHeaderValue', () => { + let esClient: ILegacyClusterClient; + let logger: Logger; + + const router: Partial = { + get: jest.fn(), + post: jest.fn(), + }; + const core = ({ + http: { + basePath: { + serverBasePath: '/', + }, + resources: { + register: jest.fn(), + }, + }, + } as unknown) as CoreSetup; + + const sessionStorageFactory: SessionStorageFactory = { + asScoped: jest.fn().mockImplementation(() => { + return { + server: { + states: { + add: jest.fn(), + }, + }, + }; + }), + }; + + // Consistent with auth_handler_factory.test.ts + beforeEach(() => { + // @ts-ignore + jest.spyOn(OpenIdAuthentication.prototype, 'init').mockImplementation(async () => {}); + }); + + const config = ({ + cookie: { + secure: false, + }, + openid: { + header: 'authorization', + scope: [], + extra_storage: { + cookie_prefix: 'testcookie', + additional_cookies: 5, + }, + }, + } as unknown) as SecurityPluginConfigType; + + test('make sure that cookies with authHeaderValue are still valid', async () => { + const openIdAuthentication = new OpenIdAuthentication( + config, + sessionStorageFactory, + router as IRouter, + esClient, + core, + logger + ); + + // The init method has a spyOn and is not executed, so we call createExtraStorage separately. + // This is not really needed for the test, but may help in spotting errors. + openIdAuthentication.createExtraStorage(); + + const mockRequest = httpServerMock.createRawRequest(); + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + + const cookie: SecuritySessionCookie = { + credentials: { + authHeaderValue: 'Bearer eyToken', + }, + }; + + const expectedHeaders = { + authorization: 'Bearer eyToken', + }; + + const headers = openIdAuthentication.buildAuthHeaderFromCookie(cookie, osRequest); + + expect(headers).toEqual(expectedHeaders); + }); + + test('get authHeaderValue from split cookies', async () => { + const openIdAuthentication = new OpenIdAuthentication( + config, + sessionStorageFactory, + router as IRouter, + esClient, + core, + logger + ); + + // The init method has a spyOn and is not executed, so we call createExtraStorage separately. + // This is not really needed for the test, but may help in spotting errors. + openIdAuthentication.createExtraStorage(); + + const testString = 'Bearer eyCombinedToken'; + const testStringBuffer: Buffer = deflateValue(testString); + const cookieValue = testStringBuffer.toString('base64'); + const cookiePrefix = config.openid!.extra_storage.cookie_prefix; + const splitValueAt = Math.ceil( + cookieValue.length / config.openid!.extra_storage.additional_cookies + ); + const mockRequest = httpServerMock.createRawRequest({ + state: { + [cookiePrefix + '1']: cookieValue.substring(0, splitValueAt), + [cookiePrefix + '2']: cookieValue.substring(splitValueAt), + }, + }); + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + + const cookie: SecuritySessionCookie = { + credentials: { + authHeaderValueExtra: true, + }, + }; + + const expectedHeaders = { + authorization: testString, + }; + + const headers = openIdAuthentication.buildAuthHeaderFromCookie(cookie, osRequest); + + expect(headers).toEqual(expectedHeaders); + }); +}); diff --git a/server/auth/types/openid/openid_auth.ts b/server/auth/types/openid/openid_auth.ts index b43d0bb0d..d8d61c2c3 100644 --- a/server/auth/types/openid/openid_auth.ts +++ b/server/auth/types/openid/openid_auth.ts @@ -29,12 +29,18 @@ import { import HTTP from 'http'; import HTTPS from 'https'; import { PeerCertificate } from 'tls'; +import { Server, ServerStateCookieOptions } from '@hapi/hapi'; import { SecurityPluginConfigType } from '../../..'; import { SecuritySessionCookie } from '../../../session/security_cookie'; import { OpenIdAuthRoutes } from './routes'; import { AuthenticationType } from '../authentication_type'; import { callTokenEndpoint } from './helper'; import { composeNextUrlQeuryParam } from '../../../utils/next_url'; +import { + ExtraAuthStorageOptions, + getExtraAuthStorageValue, + setExtraAuthStorage, +} from '../../../session/cookie_splitter'; export interface OpenIdAuthConfig { authorizationEndpoint?: string; @@ -93,6 +99,8 @@ export class OpenIdAuthentication extends AuthenticationType { this.openIdAuthConfig.tokenEndpoint = payload.token_endpoint; this.openIdAuthConfig.endSessionEndpoint = payload.end_session_endpoint || undefined; + this.createExtraStorage(); + const routes = new OpenIdAuthRoutes( this.router, this.config, @@ -135,6 +143,37 @@ export class OpenIdAuthentication extends AuthenticationType { } } + createExtraStorage() { + // @ts-ignore + const hapiServer: Server = this.sessionStorageFactory.asScoped({}).server; + + const extraCookiePrefix = this.config.openid!.extra_storage.cookie_prefix; + const extraCookieSettings: ServerStateCookieOptions = { + isSecure: this.config.cookie.secure, + isSameSite: this.config.cookie.isSameSite, + password: this.config.cookie.password, + domain: this.config.cookie.domain, + path: this.coreSetup.http.basePath.serverBasePath || '/', + clearInvalid: false, + isHttpOnly: true, + ignoreErrors: true, + encoding: 'iron', // Same as hapi auth cookie + }; + + for (let i = 1; i <= this.config.openid!.extra_storage.additional_cookies; i++) { + hapiServer.states.add(extraCookiePrefix + i, extraCookieSettings); + } + } + + private getExtraAuthStorageOptions(): ExtraAuthStorageOptions { + // If we're here, we will always have the openid configuration + return { + cookiePrefix: this.config.openid!.extra_storage.cookie_prefix, + additionalCookies: this.config.openid!.extra_storage.additional_cookies, + logger: this.logger, + }; + } + requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean { return request.headers.authorization ? true : false; } @@ -144,10 +183,16 @@ export class OpenIdAuthentication extends AuthenticationType { } getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie { + setExtraAuthStorage( + request, + request.headers.authorization as string, + this.getExtraAuthStorageOptions() + ); + return { username: authInfo.user_name, credentials: { - authHeaderValue: request.headers.authorization, + authHeaderValueExtra: true, }, authType: this.type, expiryTime: Date.now() + this.config.session.ttl, @@ -155,16 +200,20 @@ export class OpenIdAuthentication extends AuthenticationType { } // TODO: Add token expiration check here - async isValidCookie(cookie: SecuritySessionCookie): Promise { + async isValidCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): Promise { if ( cookie.authType !== this.type || !cookie.username || !cookie.expiryTime || - !cookie.credentials?.authHeaderValue || + (!cookie.credentials?.authHeaderValue && !this.getExtraAuthStorageValue(request, cookie)) || !cookie.credentials?.expires_at ) { return false; } + if (cookie.credentials?.expires_at > Date.now()) { return true; } @@ -187,10 +236,17 @@ export class OpenIdAuthentication extends AuthenticationType { // if no id_token from refresh token call, maybe the Idp doesn't allow refresh id_token if (refreshTokenResponse.idToken) { cookie.credentials = { - authHeaderValue: `Bearer ${refreshTokenResponse.idToken}`, + authHeaderValueExtra: true, refresh_token: refreshTokenResponse.refreshToken, expires_at: Date.now() + refreshTokenResponse.expiresIn! * 1000, // expiresIn is in second }; + + setExtraAuthStorage( + request, + `Bearer ${refreshTokenResponse.idToken}`, + this.getExtraAuthStorageOptions() + ); + return true; } else { return false; @@ -226,8 +282,37 @@ export class OpenIdAuthentication extends AuthenticationType { } } - buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any { + getExtraAuthStorageValue(request: OpenSearchDashboardsRequest, cookie: SecuritySessionCookie) { + let extraValue = ''; + if (!cookie.credentials?.authHeaderValueExtra) { + return extraValue; + } + + try { + extraValue = getExtraAuthStorageValue(request, this.getExtraAuthStorageOptions()); + } catch (error) { + this.logger.info(error); + } + + return extraValue; + } + + buildAuthHeaderFromCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): any { const header: any = {}; + if (cookie.credentials.authHeaderValueExtra) { + try { + const extraAuthStorageValue = this.getExtraAuthStorageValue(request, cookie); + header.authorization = extraAuthStorageValue; + return header; + } catch (error) { + this.logger.error(error); + // TODO Re-throw? + // throw error; + } + } const authHeaderValue = cookie.credentials?.authHeaderValue; if (authHeaderValue) { header.authorization = authHeaderValue; diff --git a/server/auth/types/openid/routes.ts b/server/auth/types/openid/routes.ts index 078f52e6d..47339d3ff 100644 --- a/server/auth/types/openid/routes.ts +++ b/server/auth/types/openid/routes.ts @@ -22,6 +22,7 @@ import { CoreSetup, OpenSearchDashboardsResponseFactory, OpenSearchDashboardsRequest, + Logger, } from '../../../../../../src/core/server'; import { SecuritySessionCookie } from '../../../session/security_cookie'; import { SecurityPluginConfigType } from '../../..'; @@ -30,6 +31,13 @@ import { SecurityClient } from '../../../backend/opensearch_security_client'; import { getBaseRedirectUrl, callTokenEndpoint, composeLogoutUrl } from './helper'; import { validateNextUrl } from '../../../utils/next_url'; +import { + clearSplitCookies, + ExtraAuthStorageOptions, + getExtraAuthStorageValue, + setExtraAuthStorage, +} from '../../../session/cookie_splitter'; + export class OpenIdAuthRoutes { private static readonly NONCE_LENGTH: number = 22; @@ -55,6 +63,15 @@ export class OpenIdAuthRoutes { }); } + private getExtraAuthStorageOptions(logger?: Logger): ExtraAuthStorageOptions { + // If we're here, we will always have the openid configuration + return { + cookiePrefix: this.config.openid!.extra_storage.cookie_prefix, + additionalCookies: this.config.openid!.extra_storage.additional_cookies, + logger, + }; + } + public setupRoutes() { this.router.get( { @@ -155,7 +172,7 @@ export class OpenIdAuthRoutes { const sessionStorage: SecuritySessionCookie = { username: user.username, credentials: { - authHeaderValue: `Bearer ${tokenResponse.idToken}`, + authHeaderValueExtra: true, expires_at: Date.now() + tokenResponse.expiresIn! * 1000, // expiresIn is in second }, authType: 'openid', @@ -166,6 +183,13 @@ export class OpenIdAuthRoutes { refresh_token: tokenResponse.refreshToken, }); } + + setExtraAuthStorage( + request, + `Bearer ${tokenResponse.idToken}`, + this.getExtraAuthStorageOptions(context.security_plugin.logger) + ); + this.sessionStorageFactory.asScoped(request).set(sessionStorage); return response.redirected({ headers: { @@ -187,10 +211,24 @@ export class OpenIdAuthRoutes { }, async (context, request, response) => { const cookie = await this.sessionStorageFactory.asScoped(request).get(); + let tokenFromExtraStorage = ''; + + const extraAuthStorageOptions: ExtraAuthStorageOptions = this.getExtraAuthStorageOptions( + context.security_plugin.logger + ); + + if (cookie?.credentials?.authHeaderValueExtra) { + tokenFromExtraStorage = getExtraAuthStorageValue(request, extraAuthStorageOptions); + } + + clearSplitCookies(request, extraAuthStorageOptions); this.sessionStorageFactory.asScoped(request).clear(); - // authHeaderValue is the bearer header, e.g. "Bearer " - const token = cookie?.credentials.authHeaderValue.split(' ')[1]; // get auth token + // tokenFromExtraStorage is the bearer header, e.g. "Bearer " + const token = tokenFromExtraStorage.length + ? tokenFromExtraStorage.split(' ')[1] + : cookie?.credentials.authHeaderValue.split(' ')[1]; // get auth token + const logoutQueryParams = { post_logout_redirect_uri: getBaseRedirectUrl(this.config, this.core), id_token_hint: token, diff --git a/server/auth/types/saml/routes.ts b/server/auth/types/saml/routes.ts index 79454272c..0333f9bda 100644 --- a/server/auth/types/saml/routes.ts +++ b/server/auth/types/saml/routes.ts @@ -14,17 +14,19 @@ */ import { schema } from '@osd/config-schema'; -import { - IRouter, - SessionStorageFactory, - OpenSearchDashboardsRequest, -} from '../../../../../../src/core/server'; +import { IRouter, SessionStorageFactory, Logger } from '../../../../../../src/core/server'; import { SecuritySessionCookie } from '../../../session/security_cookie'; import { SecurityPluginConfigType } from '../../..'; import { SecurityClient } from '../../../backend/opensearch_security_client'; import { CoreSetup } from '../../../../../../src/core/server'; import { validateNextUrl } from '../../../utils/next_url'; +import { + clearSplitCookies, + ExtraAuthStorageOptions, + setExtraAuthStorage, +} from '../../../session/cookie_splitter'; + export class SamlAuthRoutes { constructor( private readonly router: IRouter, @@ -35,6 +37,15 @@ export class SamlAuthRoutes { private readonly coreSetup: CoreSetup ) {} + private getExtraAuthStorageOptions(logger?: Logger): ExtraAuthStorageOptions { + // If we're here, we will always have the openid configuration + return { + cookiePrefix: this.config.saml.extra_storage.cookie_prefix, + additionalCookies: this.config.saml.extra_storage.additional_cookies, + logger, + }; + } + public setupRoutes() { this.router.get( { @@ -138,15 +149,24 @@ export class SamlAuthRoutes { if (tokenPayload.exp) { expiryTime = parseInt(tokenPayload.exp, 10) * 1000; } + const cookie: SecuritySessionCookie = { username: user.username, credentials: { - authHeaderValue: credentials.authorization, + authHeaderValueExtra: true, }, authType: 'saml', // TODO: create constant expiryTime, }; + + setExtraAuthStorage( + request, + credentials.authorization, + this.getExtraAuthStorageOptions(context.security_plugin.logger) + ); + this.sessionStorageFactory.asScoped(request).set(cookie); + if (redirectHash) { return response.redirected({ headers: { @@ -209,11 +229,18 @@ export class SamlAuthRoutes { const cookie: SecuritySessionCookie = { username: user.username, credentials: { - authHeaderValue: credentials.authorization, + authHeaderValueExtra: true, }, authType: 'saml', // TODO: create constant expiryTime, }; + + setExtraAuthStorage( + request, + credentials.authorization, + this.getExtraAuthStorageOptions(context.security_plugin.logger) + ); + this.sessionStorageFactory.asScoped(request).set(cookie); return response.redirected({ headers: { @@ -350,6 +377,10 @@ export class SamlAuthRoutes { async (context, request, response) => { try { const authInfo = await this.securityClient.authinfo(request); + await clearSplitCookies( + request, + this.getExtraAuthStorageOptions(context.security_plugin.logger) + ); this.sessionStorageFactory.asScoped(request).clear(); // TODO: need a default logout page const redirectUrl = diff --git a/server/auth/types/saml/saml_auth.test.ts b/server/auth/types/saml/saml_auth.test.ts new file mode 100644 index 000000000..4ee0e5a54 --- /dev/null +++ b/server/auth/types/saml/saml_auth.test.ts @@ -0,0 +1,145 @@ +/* + * 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/http/http_server.mocks'; + +import { OpenSearchDashboardsRequest } from '../../../../../../src/core/server/http/router'; + +import { SecurityPluginConfigType } from '../../../index'; +import { SecuritySessionCookie } from '../../../session/security_cookie'; +import { deflateValue } from '../../../utils/compression'; +import { + IRouter, + CoreSetup, + ILegacyClusterClient, + Logger, + SessionStorageFactory, +} from '../../../../../../src/core/server'; +import { SamlAuthentication } from './saml_auth'; + +describe('test SAML authHeaderValue', () => { + const router: Partial = { + get: jest.fn(), + post: jest.fn(), + }; + const core = ({ + http: { + basePath: { + serverBasePath: '/', + }, + resources: { + register: jest.fn(), + }, + }, + } as unknown) as CoreSetup; + + let esClient: ILegacyClusterClient; + let logger: Logger; + + const sessionStorageFactory: SessionStorageFactory = { + asScoped: jest.fn().mockImplementation(() => { + return { + server: { + states: { + add: jest.fn(), + }, + }, + }; + }), + }; + + // Consistent with auth_handler_factory.test.ts + beforeEach(() => {}); + + const config = ({ + cookie: { + secure: false, + }, + saml: { + extra_storage: { + cookie_prefix: 'testcookie', + additional_cookies: 5, + }, + }, + } as unknown) as SecurityPluginConfigType; + + test('make sure that cookies with authHeaderValue are still valid', async () => { + const samlAuthentication = new SamlAuthentication( + config, + sessionStorageFactory, + router as IRouter, + esClient, + core, + logger + ); + + const mockRequest = httpServerMock.createRawRequest(); + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + + const cookie: SecuritySessionCookie = { + credentials: { + authHeaderValue: 'Bearer eyToken', + }, + }; + + const expectedHeaders = { + authorization: 'Bearer eyToken', + }; + + const headers = samlAuthentication.buildAuthHeaderFromCookie(cookie, osRequest); + + expect(headers).toEqual(expectedHeaders); + }); + + test('get authHeaderValue from split cookies', async () => { + const samlAuthentication = new SamlAuthentication( + config, + sessionStorageFactory, + router as IRouter, + esClient, + core, + logger + ); + + const testString = 'Bearer eyCombinedToken'; + const testStringBuffer: Buffer = deflateValue(testString); + const cookieValue = testStringBuffer.toString('base64'); + const cookiePrefix = config.saml.extra_storage.cookie_prefix; + const splitValueAt = Math.ceil( + cookieValue.length / config.saml.extra_storage.additional_cookies + ); + const mockRequest = httpServerMock.createRawRequest({ + state: { + [cookiePrefix + '1']: cookieValue.substring(0, splitValueAt), + [cookiePrefix + '2']: cookieValue.substring(splitValueAt), + }, + }); + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + + const cookie: SecuritySessionCookie = { + credentials: { + authHeaderValueExtra: true, + }, + }; + + const expectedHeaders = { + authorization: testString, + }; + + const headers = samlAuthentication.buildAuthHeaderFromCookie(cookie, osRequest); + + expect(headers).toEqual(expectedHeaders); + }); +}); diff --git a/server/auth/types/saml/saml_auth.ts b/server/auth/types/saml/saml_auth.ts index 6b4ec03e5..a6a50f013 100644 --- a/server/auth/types/saml/saml_auth.ts +++ b/server/auth/types/saml/saml_auth.ts @@ -15,6 +15,7 @@ import { escape } from 'querystring'; import { CoreSetup } from 'opensearch-dashboards/server'; +import { Server, ServerStateCookieOptions } from '@hapi/hapi'; import { SecurityPluginConfigType } from '../../..'; import { SessionStorageFactory, @@ -34,6 +35,12 @@ import { import { SamlAuthRoutes } from './routes'; import { AuthenticationType } from '../authentication_type'; +import { + setExtraAuthStorage, + getExtraAuthStorageValue, + ExtraAuthStorageOptions, +} from '../../../session/cookie_splitter'; + export class SamlAuthentication extends AuthenticationType { public static readonly AUTH_HEADER_NAME = 'authorization'; @@ -48,6 +55,7 @@ export class SamlAuthentication extends AuthenticationType { logger: Logger ) { super(config, sessionStorageFactory, router, esClient, coreSetup, logger); + this.createExtraStorage(); this.setupRoutes(); } @@ -78,6 +86,37 @@ export class SamlAuthentication extends AuthenticationType { samlAuthRoutes.setupRoutes(); } + createExtraStorage() { + // @ts-ignore + const hapiServer: Server = this.sessionStorageFactory.asScoped({}).server; + + const extraCookiePrefix = this.config.saml.extra_storage.cookie_prefix; + const extraCookieSettings: ServerStateCookieOptions = { + isSecure: this.config.cookie.secure, + isSameSite: this.config.cookie.isSameSite, + password: this.config.cookie.password, + domain: this.config.cookie.domain, + path: this.coreSetup.http.basePath.serverBasePath || '/', + clearInvalid: false, + isHttpOnly: true, + ignoreErrors: true, + encoding: 'iron', // Same as hapi auth cookie + }; + + for (let i = 1; i <= this.config.saml.extra_storage.additional_cookies; i++) { + hapiServer.states.add(extraCookiePrefix + i, extraCookieSettings); + } + } + + private getExtraAuthStorageOptions(logger?: Logger): ExtraAuthStorageOptions { + // If we're here, we will always have the openid configuration + return { + cookiePrefix: this.config.saml.extra_storage.cookie_prefix, + additionalCookies: this.config.saml.extra_storage.additional_cookies, + logger, + }; + } + requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean { return request.headers[SamlAuthentication.AUTH_HEADER_NAME] ? true : false; } @@ -87,10 +126,20 @@ export class SamlAuthentication extends AuthenticationType { } getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie { + const authorizationHeaderValue: string = request.headers[ + SamlAuthentication.AUTH_HEADER_NAME + ] as string; + + setExtraAuthStorage( + request, + authorizationHeaderValue, + this.getExtraAuthStorageOptions(this.logger) + ); + return { username: authInfo.user_name, credentials: { - authHeaderValue: request.headers[SamlAuthentication.AUTH_HEADER_NAME], + authHeaderValueExtra: true, }, authType: this.type, expiryTime: Date.now() + this.config.session.ttl, @@ -98,12 +147,15 @@ export class SamlAuthentication extends AuthenticationType { } // Can be improved to check if the token is expiring. - async isValidCookie(cookie: SecuritySessionCookie): Promise { + async isValidCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): Promise { return ( cookie.authType === this.type && cookie.username && cookie.expiryTime && - cookie.credentials?.authHeaderValue + (cookie.credentials?.authHeaderValue || this.getExtraAuthStorageValue(request, cookie)) ); } @@ -119,9 +171,40 @@ export class SamlAuthentication extends AuthenticationType { } } - buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any { + getExtraAuthStorageValue(request: OpenSearchDashboardsRequest, cookie: SecuritySessionCookie) { + let extraValue = ''; + if (!cookie.credentials?.authHeaderValueExtra) { + return extraValue; + } + + try { + extraValue = getExtraAuthStorageValue(request, this.getExtraAuthStorageOptions(this.logger)); + } catch (error) { + this.logger.info(error); + } + + return extraValue; + } + + buildAuthHeaderFromCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): any { const headers: any = {}; - headers[SamlAuthentication.AUTH_HEADER_NAME] = cookie.credentials?.authHeaderValue; + + if (cookie.credentials?.authHeaderValueExtra) { + try { + const extraAuthStorageValue = this.getExtraAuthStorageValue(request, cookie); + headers[SamlAuthentication.AUTH_HEADER_NAME] = extraAuthStorageValue; + } catch (error) { + this.logger.error(error); + // @todo Re-throw? + // throw error; + } + } else { + headers[SamlAuthentication.AUTH_HEADER_NAME] = cookie.credentials?.authHeaderValue; + } + return headers; } } diff --git a/server/index.ts b/server/index.ts index a25bca467..f4d9dfeeb 100644 --- a/server/index.ts +++ b/server/index.ts @@ -135,8 +135,22 @@ export const configSchema = schema.object({ root_ca: schema.string({ defaultValue: '' }), verify_hostnames: schema.boolean({ defaultValue: true }), refresh_tokens: schema.boolean({ defaultValue: true }), + extra_storage: schema.object({ + cookie_prefix: schema.string({ + defaultValue: 'security_authentication_oidc', + minLength: 2, + }), + additional_cookies: schema.number({ min: 1, defaultValue: 5 }), + }), }) ), + saml: schema.object({ + extra_storage: schema.object({ + cookie_prefix: schema.string({ defaultValue: 'security_authentication_saml', minLength: 2 }), + additional_cookies: schema.number({ min: 0, defaultValue: 3 }), + }), + }), + proxycache: schema.maybe( schema.object({ // when auth.type is proxycache, user_header, roles_header and proxy_header_ip are required diff --git a/server/session/cookie_splitter.test.ts b/server/session/cookie_splitter.test.ts new file mode 100644 index 000000000..73745d59a --- /dev/null +++ b/server/session/cookie_splitter.test.ts @@ -0,0 +1,239 @@ +/* + * 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 { Request as HapiRequest, ResponseObject as HapiResponseObject } from '@hapi/hapi'; +import { httpServerMock } from '../../../../src/core/server/http/http_server.mocks'; +import { merge } from 'lodash'; +import { + clearSplitCookies, + getExtraAuthStorageValue, + setExtraAuthStorage, + splitValueIntoCookies, + unsplitCookiesIntoValue, +} from './cookie_splitter'; +import { OpenSearchDashboardsRequest } from '../../../../src/core/server/http/router'; +import { deflateValue } from '../utils/compression'; + +type CookieAuthWithResponseObject = Partial & { + h: Partial; +}; + +describe('Test extra auth storage', () => { + test('the cookie value is split up into multiple cookies', async () => { + const cookiePrefix = 'testcookie'; + const additionalCookies = 2; + + const mockRequest = httpServerMock.createRawRequest(); + (mockRequest.cookieAuth as CookieAuthWithResponseObject) = { + h: { + state: jest.fn(), + unstate: jest.fn(), + }, + }; + + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + + setExtraAuthStorage(osRequest, 'THIS IS MY VALUE', { + cookiePrefix, + additionalCookies, + }); + + const cookieAuth = mockRequest.cookieAuth as CookieAuthWithResponseObject; + expect(cookieAuth.h.state).toHaveBeenCalledTimes(1); + expect(cookieAuth.h.state).toHaveBeenCalledWith(cookiePrefix + '1', expect.anything()); + }); + + test('cookies are stitched together and inflated', async () => { + const cookiePrefix = 'testcookie'; + const additionalCookies = 2; + + const testString = 'abcdefghi'; + const testStringBuffer: Buffer = deflateValue(testString); + const cookieValue = testStringBuffer.toString('base64'); + + const splitValueAt = Math.ceil(cookieValue.length / additionalCookies); + const mockRequest = httpServerMock.createRawRequest({ + state: { + [cookiePrefix + '1']: cookieValue.substring(0, splitValueAt), + [cookiePrefix + '2']: cookieValue.substring(splitValueAt), + }, + }); + + (mockRequest.cookieAuth as CookieAuthWithResponseObject) = { + h: { + state: jest.fn(), + unstate: jest.fn(), + }, + }; + + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + + const extraStorageValue = getExtraAuthStorageValue(osRequest, { + cookiePrefix, + additionalCookies, + }); + + expect(extraStorageValue).toEqual(testString); + }); + + /** + * Should calculate the number of cookies correctly. + * Any cookies required should be unstated + */ + test('number of cookies used is correctly calculated', async () => { + const cookiePrefix = 'testcookie'; + const additionalCookies = 5; + + // 4000 bytes would require two cookies + const cookieValue = 'a'.repeat(4000); + + const mockRequest = httpServerMock.createRawRequest({ + state: { + [cookiePrefix + '1']: 'should be overridden', + [cookiePrefix + '2']: 'should be overridden', + [cookiePrefix + '3']: 'should be unstated', + [cookiePrefix + '4']: 'should be unstated', + [cookiePrefix + '5']: 'should be unstated', + }, + }); + + (mockRequest.cookieAuth as CookieAuthWithResponseObject) = { + h: { + state: jest.fn(), + unstate: jest.fn(), + }, + }; + + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + + splitValueIntoCookies(osRequest, cookiePrefix, cookieValue, additionalCookies); + + const cookieAuth = mockRequest.cookieAuth as CookieAuthWithResponseObject; + expect(cookieAuth.h.state).toHaveBeenCalledTimes(2); + expect(cookieAuth.h.unstate).toHaveBeenCalledTimes(3); + }); + + test('clear all cookies', async () => { + const cookiePrefix = 'testcookie'; + const additionalCookies = 5; + + const mockRequest = httpServerMock.createRawRequest({ + state: { + [cookiePrefix + '1']: 'should be unstated', + [cookiePrefix + '2']: 'should be unstated', + [cookiePrefix + '3']: 'should be unstated', + }, + }); + + (mockRequest.cookieAuth as CookieAuthWithResponseObject) = { + h: { + state: jest.fn(), + unstate: jest.fn(), + }, + }; + + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + + clearSplitCookies(osRequest, { + cookiePrefix, + additionalCookies, + }); + + const cookieAuth = mockRequest.cookieAuth as CookieAuthWithResponseObject; + // Only 3 out of 5 cookies set in the request + expect(cookieAuth.h.unstate).toHaveBeenCalledTimes(3); + }); + + test('should unsplit cookies', async () => { + const cookiePrefix = 'testcookie'; + const additionalCookies = 5; + + const mockRequest = httpServerMock.createRawRequest({ + state: { + [cookiePrefix + '1']: 'abc', + [cookiePrefix + '2']: 'def', + [cookiePrefix + '3']: 'ghi', + }, + }); + + const osRequest = OpenSearchDashboardsRequest.from(mockRequest); + const unsplitValue = unsplitCookiesIntoValue(osRequest, cookiePrefix, additionalCookies); + + expect(unsplitValue).toEqual('abcdefghi'); + }); + + test('should check for cookie values updated in the same request', async () => { + const cookiePrefix = 'testcookie'; + const additionalCookies = 5; + + const mockRequest = httpServerMock.createRawRequest(); + + const extendedMockRequest = merge(mockRequest, { + _states: { + [cookiePrefix + '1']: { + name: cookiePrefix + '1', + value: 'abc', + }, + [cookiePrefix + '2']: { + name: cookiePrefix + '2', + value: 'def', + }, + [cookiePrefix + '3']: { + name: cookiePrefix + '3', + value: 'ghi', + }, + }, + }) as HapiRequest; + + const osRequest = OpenSearchDashboardsRequest.from(extendedMockRequest); + const unsplitValue = unsplitCookiesIntoValue(osRequest, cookiePrefix, additionalCookies); + + expect(unsplitValue).toEqual('abcdefghi'); + }); + + test('should not mix cookie values updated in the same request with previous cookie values', async () => { + const cookiePrefix = 'testcookie'; + const additionalCookies = 5; + + const mockRequest = httpServerMock.createRawRequest({ + state: { + [cookiePrefix + '1']: 'abc', + [cookiePrefix + '2']: 'def', + [cookiePrefix + '3']: 'ghi', + }, + }); + + const extendedMockRequest = merge(mockRequest, { + _states: { + [cookiePrefix + '1']: { + name: cookiePrefix + '1', + value: 'jkl', + }, + [cookiePrefix + '2']: { + name: cookiePrefix + '2', + value: 'mno', + }, + [cookiePrefix + '3']: { + name: cookiePrefix + '3', + value: 'pqr', + }, + }, + }) as HapiRequest; + + const osRequest = OpenSearchDashboardsRequest.from(extendedMockRequest); + const unsplitValue = unsplitCookiesIntoValue(osRequest, cookiePrefix, additionalCookies); + + expect(unsplitValue).toEqual('jklmnopqr'); + }); +}); diff --git a/server/session/cookie_splitter.ts b/server/session/cookie_splitter.ts new file mode 100644 index 000000000..33b3ca12d --- /dev/null +++ b/server/session/cookie_splitter.ts @@ -0,0 +1,181 @@ +/* + * 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 { Request as HapiRequest, ResponseObject as HapiResponseObject } from '@hapi/hapi'; +import { Logger } from '@osd/logging'; +import { + ensureRawRequest, + OpenSearchDashboardsRequest, +} from '../../../../src/core/server/http/router'; +import { deflateValue, inflateValue } from '../utils/compression'; +import { ESTIMATED_IRON_COOKIE_OVERHEAD, MAX_LENGTH_OF_COOKIE_BYTES } from '../../common'; + +export interface ExtraAuthStorageOptions { + cookiePrefix: string; + additionalCookies: number; + logger?: Logger; +} + +type CookieAuthWithResponseObject = HapiRequest['cookieAuth'] & { h: HapiResponseObject }; + +interface HapiStates { + [cookieName: string]: { + name: string; + value: string; + }; +} + +export type HapiRequestWithStates = HapiRequest & { _states: HapiStates }; + +export function getExtraAuthStorageValue( + request: OpenSearchDashboardsRequest, + options: ExtraAuthStorageOptions +): string { + let compressedContent = ''; + let content = ''; + + if (options.additionalCookies > 0) { + compressedContent = unsplitCookiesIntoValue( + request, + options.cookiePrefix, + options.additionalCookies + ); + } + + try { + content = inflateValue(Buffer.from(compressedContent, 'base64')).toString(); + } catch (error) { + throw error; + } + + return content; +} + +/** + * Compress and split up the given value into multiple cookies + * @param request + * @param cookie + * @param options + */ +export function setExtraAuthStorage( + request: OpenSearchDashboardsRequest, + content: string, + options: ExtraAuthStorageOptions +): void { + const compressedAuthorizationHeaderValue: Buffer = deflateValue(content); + const compressedContent = compressedAuthorizationHeaderValue.toString('base64'); + + splitValueIntoCookies( + request, + options.cookiePrefix, + compressedContent, + options.additionalCookies, + options.logger + ); +} + +export function splitValueIntoCookies( + request: OpenSearchDashboardsRequest, // @todo Should be OpenSearchDashboardsRequest, I believe? + cookiePrefix: string, + value: string, + additionalCookies: number, + logger?: Logger +): void { + /** + * Assume that Iron adds around 50%. + * Remember that an empty cookie is around 30 bytes + */ + + const maxLengthPerCookie = Math.floor( + MAX_LENGTH_OF_COOKIE_BYTES / ESTIMATED_IRON_COOKIE_OVERHEAD + ); + const cookiesNeeded = value.length / maxLengthPerCookie; // Assume 1 bit per character since this value is encoded + // If the amount of additional cookies aren't enough for our logic, we try to write the value anyway + // TODO We could also consider throwing an error, since a failed cookie leads to weird redirects. + // But throwing would probably also lead to a weird redirect, since we'd get the token from the IdP again and again + let splitValueAt = maxLengthPerCookie; + if (cookiesNeeded > additionalCookies) { + splitValueAt = Math.ceil(value.length / additionalCookies); + if (logger) { + logger.warn( + 'The payload may be too large for the cookies. To be safe, we would need ' + + Math.ceil(cookiesNeeded) + + ' cookies in total, but we only have ' + + additionalCookies + + '. This can be changed with opensearch_security.openid.extra_storage.additional_cookies.' + ); + } + } + + const rawRequest: HapiRequest = ensureRawRequest(request); + + const values: string[] = []; + + for (let i = 1; i <= additionalCookies; i++) { + values.push(value.substring((i - 1) * splitValueAt, i * splitValueAt)); + } + + values.forEach(async (cookieSplitValue: string, index: number) => { + const cookieName: string = cookiePrefix + (index + 1); + + if (cookieSplitValue === '') { + // Make sure we clean up cookies that are not needed for the given value + (rawRequest.cookieAuth as CookieAuthWithResponseObject).h.unstate(cookieName); + } else { + (rawRequest.cookieAuth as CookieAuthWithResponseObject).h.state(cookieName, cookieSplitValue); + } + }); +} + +export function unsplitCookiesIntoValue( + request: OpenSearchDashboardsRequest, + cookiePrefix: string, + additionalCookies: number +): string { + const rawRequest: HapiRequestWithStates = ensureRawRequest(request) as HapiRequestWithStates; + let fullCookieValue = ''; + + // We don't want to mix and match between _states and .state. + // If we find the first additional cookie in _states, we + // use _states for all subsequent additional cookies + const requestHasNewerCookieState = rawRequest._states && rawRequest._states[cookiePrefix + 1]; + + for (let i = 1; i <= additionalCookies; i++) { + const cookieName = cookiePrefix + i; + if ( + requestHasNewerCookieState && + rawRequest._states[cookieName] && + rawRequest._states[cookieName].value + ) { + fullCookieValue = fullCookieValue + rawRequest._states[cookieName].value; + } else if (!requestHasNewerCookieState && rawRequest.state[cookieName]) { + fullCookieValue = fullCookieValue + rawRequest.state[cookieName]; + } + } + + return fullCookieValue; +} + +export function clearSplitCookies( + request: OpenSearchDashboardsRequest, + options: ExtraAuthStorageOptions +): void { + const rawRequest: HapiRequest = ensureRawRequest(request); + for (let i = 1; i <= options.additionalCookies; i++) { + const cookieName = options.cookiePrefix + i; + if (rawRequest.state[cookieName]) { + (rawRequest.cookieAuth as CookieAuthWithResponseObject).h.unstate(cookieName); + } + } +} diff --git a/server/utils/compression.test.ts b/server/utils/compression.test.ts new file mode 100644 index 000000000..63b807c51 --- /dev/null +++ b/server/utils/compression.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { deflateValue, inflateValue } from './compression'; + +describe('test compression', () => { + test('get original value from deflated value', () => { + const originalValue = 'This is the original value'; + const deflatedValue: Buffer = deflateValue(originalValue); + const inflatedValue: Buffer = inflateValue(deflatedValue); + + // Make sure deflateValue actually does something + expect(deflatedValue).not.toEqual(originalValue); + + expect(inflatedValue.toString()).toEqual(originalValue); + }); +}); diff --git a/server/utils/compression.ts b/server/utils/compression.ts new file mode 100644 index 000000000..8104efdc6 --- /dev/null +++ b/server/utils/compression.ts @@ -0,0 +1,28 @@ +/* + * 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 zlib, { ZlibOptions } from 'zlib'; + +export function deflateValue(value: string, options: ZlibOptions = {}): Buffer { + const compressedBuffer: Buffer = zlib.deflateSync(value, options); + + return compressedBuffer; +} + +export function inflateValue(value: Buffer, options: ZlibOptions = {}): Buffer { + const uncompressedBuffer: Buffer = zlib.inflateSync(value, options); + + return uncompressedBuffer; +} diff --git a/test/jest_integration/saml_auth.test.ts b/test/jest_integration/saml_auth.test.ts index 53d2a951e..eb6cb9720 100644 --- a/test/jest_integration/saml_auth.test.ts +++ b/test/jest_integration/saml_auth.test.ts @@ -33,7 +33,6 @@ describe('start OpenSearch Dashboards server', () => { // XPath Constants const userIconBtnXPath = '//button[@id="user-icon-btn"]'; const signInBtnXPath = '//*[@id="btn-sign-in"]'; - const skipWelcomeBtnXPath = '//button[@data-test-subj="skipWelcomeScreen"]'; const tenantNameLabelXPath = '//*[@id="tenantName"]'; const pageTitleXPath = '//*[@id="osdOverviewPageHeader__title"]'; // Browser Settings @@ -46,6 +45,7 @@ describe('start OpenSearch Dashboards server', () => { plugins: { scanDirs: [resolve(__dirname, '../..')], }, + home: { disableWelcomeScreen: true }, server: { host: 'localhost', port: 5601, @@ -243,7 +243,7 @@ describe('start OpenSearch Dashboards server', () => { await driver.wait(until.elementsLocated(By.xpath(pageTitleXPath)), 10000); const cookie = await driver.manage().getCookies(); - expect(cookie.length).toEqual(2); + expect(cookie.length).toEqual(3); await driver.manage().deleteAllCookies(); await driver.quit(); }); @@ -259,13 +259,13 @@ describe('start OpenSearch Dashboards server', () => { ); const cookie = await driver.manage().getCookies(); - expect(cookie.length).toEqual(2); + expect(cookie.length).toEqual(3); await driver.manage().deleteAllCookies(); await driver.quit(); }); it('Login to Dashboard with Hash', async () => { - const urlWithHash = `http://localhost:5601/app/dashboards#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=(filters:!(),refreshInterval:(pause:!f,value:900000),time:(from:now-24h,to:now))&_a=(description:'Analyze%20mock%20flight%20data%20for%20OpenSearch-Air,%20Logstash%20Airways,%20OpenSearch%20Dashboards%20Airlines%20and%20BeatsWest',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),query:(language:kuery,query:''),timeRestore:!t,title:'%5BFlights%5D%20Global%20Flight%20Dashboard',viewMode:view)`; + const urlWithHash = `http://localhost:5601/app/security-dashboards-plugin#/getstarted`; const driver = getDriver(browser, options).build(); await driver.manage().deleteAllCookies(); await driver.get(urlWithHash); @@ -278,7 +278,7 @@ describe('start OpenSearch Dashboards server', () => { const windowHash = await driver.getCurrentUrl(); expect(windowHash).toEqual(urlWithHash); const cookie = await driver.manage().getCookies(); - expect(cookie.length).toEqual(2); + expect(cookie.length).toEqual(3); await driver.manage().deleteAllCookies(); await driver.quit(); }); @@ -316,9 +316,7 @@ describe('start OpenSearch Dashboards server', () => { await driver.findElement(By.xpath(signInBtnXPath)).click(); - await driver.wait(until.elementsLocated(By.xpath(skipWelcomeBtnXPath)), 10000); - - await driver.findElement(By.xpath(skipWelcomeBtnXPath)).click(); + await driver.wait(until.elementsLocated(By.xpath(userIconBtnXPath)), 10000); await driver.findElement(By.xpath(userIconBtnXPath)).click();