From c45b931b7820dad5be83964cbc857cecaf5efa6f Mon Sep 17 00:00:00 2001 From: Stephen Crawford <65832608+stephen-crawford@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:49:53 -0400 Subject: [PATCH 1/3] Add Proxy Auth to Multi Auth Options (#2076) * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford * Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford --------- Signed-off-by: Stephen Crawford --- public/apps/login/login-page.tsx | 8 +- server/auth/types/multiple/multi_auth.ts | 22 +- test/constant.ts | 4 + test/jest_integration/proxy_multiauth.test.ts | 209 ++++++++++++++++++ 4 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 test/jest_integration/proxy_multiauth.test.ts diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index d53329cd3..4591c032c 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -237,7 +237,10 @@ export function LoginPage(props: LoginPageDeps) { ); } - if (authOpts.length > 1) { + if ( + authOpts.length > 1 && + !(authOpts.includes(AuthType.PROXY) && authOpts.length === 2) + ) { formBody.push(); formBody.push(); formBody.push(); @@ -258,6 +261,9 @@ export function LoginPage(props: LoginPageDeps) { formBodyOp.push(renderLoginButton(AuthType.SAML, samlAuthLoginUrl, samlConfig)); break; } + case AuthType.PROXY: { + break; + } default: { setloginFailed(true); setloginError( diff --git a/server/auth/types/multiple/multi_auth.ts b/server/auth/types/multiple/multi_auth.ts index b00b3d154..4b4f64834 100644 --- a/server/auth/types/multiple/multi_auth.ts +++ b/server/auth/types/multiple/multi_auth.ts @@ -29,7 +29,12 @@ import { AuthType, LOGIN_PAGE_URI } from '../../../../common'; import { composeNextUrlQueryParam } from '../../../utils/next_url'; import { MultiAuthRoutes } from './routes'; import { SecuritySessionCookie } from '../../../session/security_cookie'; -import { BasicAuthentication, OpenIdAuthentication, SamlAuthentication } from '../../types'; +import { + BasicAuthentication, + OpenIdAuthentication, + ProxyAuthentication, + SamlAuthentication, +} from '../../types'; export class MultipleAuthentication extends AuthenticationType { private authTypes: string | string[]; @@ -93,6 +98,19 @@ export class MultipleAuthentication extends AuthenticationType { this.authHandlers.set(AuthType.SAML, SamlAuth); break; } + case AuthType.PROXY: { + const ProxyAuth = new ProxyAuthentication( + this.config, + this.sessionStorageFactory, + this.router, + this.esClient, + this.coreSetup, + this.logger + ); + await ProxyAuth.init(); + this.authHandlers.set(AuthType.PROXY, ProxyAuth); + break; + } default: { throw new Error(`Unsupported authentication type: ${this.authTypes[i]}`); } @@ -115,7 +133,7 @@ export class MultipleAuthentication extends AuthenticationType { async getAdditionalAuthHeader( request: OpenSearchDashboardsRequest ): Promise { - // To Do: refactor this method to improve the effiency to get cookie, get cookie from input parameter + // To Do: refactor this method to improve the efficiency to get cookie, get cookie from input parameter const cookie = await this.sessionStorageFactory.asScoped(request).get(); const reqAuthType = cookie?.authType?.toLowerCase(); diff --git a/test/constant.ts b/test/constant.ts index 5dcb387e2..0f450e2b8 100644 --- a/test/constant.ts +++ b/test/constant.ts @@ -21,3 +21,7 @@ export const ADMIN_PASSWORD: string = process.env.ADMIN_PASSWORD || 'admin'; const ADMIN_USER_PASS: string = `${ADMIN_USER}:${ADMIN_PASSWORD}`; export const ADMIN_CREDENTIALS: string = `Basic ${Buffer.from(ADMIN_USER_PASS).toString('base64')}`; export const AUTHORIZATION_HEADER_NAME: string = 'Authorization'; + +export const PROXY_USER: string = 'x-proxy-user'; +export const PROXY_ROLE: string = 'x-proxy-roles'; +export const PROXY_ADMIN_ROLE: string = 'admin'; diff --git a/test/jest_integration/proxy_multiauth.test.ts b/test/jest_integration/proxy_multiauth.test.ts new file mode 100644 index 000000000..125055f31 --- /dev/null +++ b/test/jest_integration/proxy_multiauth.test.ts @@ -0,0 +1,209 @@ +/* + * 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 * as osdTestServer from '../../../../src/core/test_helpers/osd_server'; +import { Root } from '../../../../src/core/server/root'; +import { resolve } from 'path'; +import { describe, it, beforeAll, afterAll } from '@jest/globals'; +import { + ADMIN_CREDENTIALS, + OPENSEARCH_DASHBOARDS_SERVER_USER, + OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, + ADMIN_USER, + PROXY_ADMIN_ROLE, +} from '../constant'; +import wreck from '@hapi/wreck'; + +describe('start OpenSearch Dashboards server', () => { + let root: Root; + let config; + + beforeAll(async () => { + root = osdTestServer.createRootWithSettings( + { + plugins: { + scanDirs: [resolve(__dirname, '../..')], + }, + home: { disableWelcomeScreen: true }, + server: { + host: 'localhost', + port: 5601, + }, + logging: { + silent: true, + verbose: false, + }, + opensearch: { + hosts: ['https://localhost:9200'], + ignoreVersionMismatch: true, + ssl: { verificationMode: 'none' }, + username: OPENSEARCH_DASHBOARDS_SERVER_USER, + password: OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, + requestHeadersAllowlist: [ + 'securitytenant', + 'Authorization', + 'x-forwarded-for', + 'x-proxy-user', + 'x-proxy-roles', + ], + }, + opensearch_security: { + auth: { + anonymous_auth_enabled: false, + type: ['basicauth', 'proxy'], + multiple_auth_enabled: true, + }, + proxycache: { + user_header: 'x-proxy-user', + roles_header: 'x-proxy-roles', + }, + multitenancy: { + enabled: true, + tenants: { + enable_global: true, + enable_private: true, + preferred: ['Private', 'Global'], + }, + }, + }, + }, + { + // to make ignoreVersionMismatch setting work + // can be removed when we have corresponding ES version + dev: true, + } + ); + + console.log('Starting OpenSearchDashboards server..'); + await root.setup(); + await root.start(); + + console.log('Starting to Download Flights Sample Data'); + await wreck.post('http://localhost:5601/api/sample_data/flights', { + payload: {}, + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + security_tenant: 'global', + }, + }); + console.log('Downloaded Sample Data'); + const getConfigResponse = await wreck.get( + 'https://localhost:9200/_plugins/_security/api/securityconfig', + { + rejectUnauthorized: false, + headers: { + authorization: ADMIN_CREDENTIALS, + }, + } + ); + const responseBody = (getConfigResponse.payload as Buffer).toString(); + config = JSON.parse(responseBody).config; + const proxyConfig = { + http_enabled: true, + transport_enabled: true, + order: 0, + http_authenticator: { + challenge: false, + type: 'proxy', + config: { + user_header: 'x-proxy-user', + roles_header: 'x-proxy-roles', + }, + }, + authentication_backend: { + type: 'noop', + config: {}, + }, + }; + try { + config.dynamic!.authc!.proxy_auth_domain = proxyConfig; + config.dynamic!.authc!.basic_internal_auth_domain.http_authenticator.challenge = false; + config.dynamic!.http!.anonymous_auth_enabled = false; + await wreck.put('https://localhost:9200/_plugins/_security/api/securityconfig/config', { + payload: config, + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + }, + }); + } catch (error) { + console.log('Got an error while updating security config!!', error.stack); + fail(error); + } + }); + + afterAll(async () => { + console.log('Remove the Sample Data'); + await wreck + .delete('http://localhost:5601/api/sample_data/flights', { + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + }, + }) + .then((value) => { + Promise.resolve(value); + }) + .catch((value) => { + Promise.resolve(value); + }); + console.log('Remove the Security Config'); + await wreck + .patch('https://localhost:9200/_plugins/_security/api/securityconfig', { + payload: [ + { + op: 'remove', + path: '/config/dynamic/authc/proxy_auth_domain', + }, + ], + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + }, + }) + .then((value) => { + Promise.resolve(value); + }) + .catch((value) => { + Promise.resolve(value); + }); + // shutdown OpenSearchDashboards server + await root.shutdown(); + }); + + it('Verify Proxy access to dashboards', async () => { + console.log('Wreck access home page'); + await wreck + .get('http://localhost:5601/app/home#', { + rejectUnauthorized: true, + headers: { + 'Content-Type': 'application/json', + PROXY_USER: ADMIN_USER, + PROXY_ROLE: PROXY_ADMIN_ROLE, + }, + }) + .then((value) => { + Promise.resolve(value); + }) + .catch((value) => { + Promise.resolve(value); + }); + }); +}); From 487c571cd006fe5a8dfb7ccca063a62ab08ce0fe Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Wed, 21 Aug 2024 14:50:14 -0400 Subject: [PATCH 2/3] Moves Chang to emeritus (#2095) * Moves Chang to emeritus Signed-off-by: Derek Ho * Renames stephen Signed-off-by: Derek Ho * Fixes maintainers file Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho --- .github/CODEOWNERS | 2 +- MAINTAINERS.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5d2f2ead8..d3f54fa2b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @cliu123 @cwperks @DarshitChanpura @derek-ho @RyanL1997 @scrawfor99 +* @cwperks @DarshitChanpura @derek-ho @RyanL1997 @stephen-crawford diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 2d2ffb817..7d36fc6c0 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -6,11 +6,10 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Maintainer | GitHub ID | Affiliation | | ---------------- | ----------------------------------------------------- | ----------- | -| Chang Liu | [cliu123](https://github.com/cliu123) | Amazon | | Darshit Chanpura | [DarshitChanpura](https://github.com/DarshitChanpura) | Amazon | | Craig Perkins | [cwperks](https://github.com/cwperks) | Amazon | | Ryan Liang | [RyanL1997](https://github.com/RyanL1997) | Amazon | -| Stephen Crawford | [scrawfor99](https://github.com/scrawfor99) | Amazon | +| Stephen Crawford | [scrawfor99](https://github.com/stephen-crawford) | Amazon | | Derek Ho | [derek-ho](https://github.com/derek-ho) | Amazon | ## Emeritus @@ -22,3 +21,5 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Tianle Huang | [tianleh](https://github.com/tianleh) | Amazon | | Dave Lago | [davidlago](https://github.com/davidlago) | Contributor | | Peter Nied | [peternied](https://github.com/peternied) | Amazon | +| Chang Liu | [cliu123](https://github.com/cliu123) | Amazon | + From b1148fb74956f7a3a84d38ad3d2f2300a959b8a6 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Wed, 21 Aug 2024 16:35:10 -0400 Subject: [PATCH 3/3] Fix a bug where basepath nextUrl is invalid when it should be valid (#2096) * Fix a bug where basepath nextUrl is invalid when it should be valid Signed-off-by: Derek Ho * Udpate test Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho --- server/utils/next_url.test.ts | 5 +++++ server/utils/next_url.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/utils/next_url.test.ts b/server/utils/next_url.test.ts index 56f4b0741..c90e2cd7a 100644 --- a/server/utils/next_url.test.ts +++ b/server/utils/next_url.test.ts @@ -104,6 +104,11 @@ describe('test validateNextUrl', () => { expect(validateNextUrl(url, '')).toEqual(undefined); }); + test('allow basePath', () => { + const url = '/osd'; + expect(validateNextUrl(url, '/osd')).toEqual(undefined); + }); + test('allow dashboard url', () => { const url = '/_plugin/opensearch-dashboards/app/opensearch-dashboards#dashbard/dashboard-id?_g=(param=a&p=b)'; diff --git a/server/utils/next_url.ts b/server/utils/next_url.ts index 9cc47adbd..596aefd02 100644 --- a/server/utils/next_url.ts +++ b/server/utils/next_url.ts @@ -73,7 +73,7 @@ export function validateNextUrl( } const pathMinusBase = path.replace(bp, ''); if ( - !pathMinusBase.startsWith('/') || + (pathMinusBase && !pathMinusBase.startsWith('/')) || (pathMinusBase.length >= 2 && !/^\/[a-zA-Z_][\/a-zA-Z0-9-_]+$/.test(pathMinusBase)) ) { return INVALID_NEXT_URL_PARAMETER_MESSAGE;