From ab5cad2781e4e078ee9276faafe947bf0378b23d Mon Sep 17 00:00:00 2001 From: Stephen Crawford Date: Tue, 30 Jul 2024 15:29:22 -0400 Subject: [PATCH] Add Proxy Auth to Multi Auth Options Signed-off-by: Stephen Crawford --- public/apps/login/login-page.tsx | 5 +- server/auth/types/multiple/multi_auth.ts | 17 +- test/constant.ts | 4 + test/jest_integration/proxy_auth.test.ts | 101 +++++++ test/jest_integration/proxy_multiauth.test.ts | 271 ++++++++++++++++++ 5 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 test/jest_integration/proxy_auth.test.ts 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 66915dd9f..5a93afbe4 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -237,7 +237,7 @@ 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 +258,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..2851fa24c 100644 --- a/server/auth/types/multiple/multi_auth.ts +++ b/server/auth/types/multiple/multi_auth.ts @@ -29,7 +29,7 @@ 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 +93,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 +128,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_auth.test.ts b/test/jest_integration/proxy_auth.test.ts new file mode 100644 index 000000000..8772bac99 --- /dev/null +++ b/test/jest_integration/proxy_auth.test.ts @@ -0,0 +1,101 @@ +/* + * 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, expect, it, beforeAll, afterAll } from '@jest/globals'; +import { + OPENSEARCH_DASHBOARDS_SERVER_USER, + OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, + ADMIN_USER, + PROXY_USER, + PROXY_ROLE, + PROXY_ADMIN_ROLE, +} from '../constant'; + +describe('start OpenSearch Dashboards server', () => { + let root: Root; + + beforeAll(async () => { + root = osdTestServer.createRootWithSettings( + { + plugins: { + scanDirs: [resolve(__dirname, '../..')], + }, + 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: { + type: 'proxy', + }, + proxycache: { + user_header: 'x-proxy-user', + roles_header: 'x-proxy-roles', + }, + }, + }, + { + // to make ignoreVersionMismatch setting work + // can be removed when we have corresponding ES version + dev: true, + } + ); + }); + + afterAll(async () => { + // shutdown OpenSearchDashboards server + await root.shutdown(); + }); + + it('can access home page with proxy header', async () => { + const response = await osdTestServer.request + .get(root, 'app/home#/') + .set(PROXY_USER, ADMIN_USER) + .set(PROXY_ROLE, PROXY_ADMIN_ROLE); + expect(response.status).toEqual(200); + }); + + it('cannot access home page without proxy header', async () => { + const response = await osdTestServer.request.get(root, 'app/home#/'); + expect(response.status).toEqual(401); + }); + + it('cannot access home page with partial proxy header', async () => { + const response = await osdTestServer.request + .get(root, 'app/home#/') + .set(PROXY_USER, ADMIN_USER); + expect(response.status).toEqual(401); + }); + + it('cannot access home page with partial proxy header2', async () => { + const response = await osdTestServer.request + .get(root, 'app/home#/') + .set(PROXY_ROLE, PROXY_ADMIN_ROLE); + expect(response.status).toEqual(401); + }); +}); diff --git a/test/jest_integration/proxy_multiauth.test.ts b/test/jest_integration/proxy_multiauth.test.ts new file mode 100644 index 000000000..97e9f8bb5 --- /dev/null +++ b/test/jest_integration/proxy_multiauth.test.ts @@ -0,0 +1,271 @@ +/* + * 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, expect, 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'; +import { Builder, By, until } from 'selenium-webdriver'; +import { Options } from 'selenium-webdriver/firefox'; + +describe('start OpenSearch Dashboards server', () => { + let root: Root; + let config; + + // XPath Constants + const signInBtnXPath = '//*[@id="btn-sign-in"]'; + // Browser Settings + const browser = 'firefox'; + const options = new Options().headless(); + + 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); + }); + }); + it('Login to Dashboards and resume from nextUrl', async () => { + const urlWithHash = `http://localhost:5601/app/security-dashboards-plugin#/getstarted`; + const loginUrlWithNextUrl = `http://localhost:5601/app/login?nextUrl=%2Fapp%2Fsecurity-dashboards-plugin#/getstarted`; + const driver = getDriver(browser, options).build(); + await driver.manage().deleteAllCookies(); + await driver.get(loginUrlWithNextUrl); + await driver.wait(until.elementsLocated(By.xpath(signInBtnXPath)), 20000); + await driver.findElement(By.xpath(signInBtnXPath)).click(); + // TODO Use a better XPath. + await driver.wait( + until.elementsLocated(By.xpath('/html/body/div[1]/div/header/div/div[2]')), + 20000 + ); + const windowHash = await driver.getCurrentUrl(); + console.log('windowHash: ' + windowHash); + expect(windowHash).toEqual(urlWithHash); + const cookie = await driver.manage().getCookies(); + expect(cookie.length).toEqual(3); + await driver.manage().deleteAllCookies(); + await driver.quit(); + }); + + it('Login to Dashboards without nextUrl', async () => { + const urlWithoutHash = `http://localhost:5601/app/home#/`; + const loginUrl = `http://localhost:5601/app/login`; + const driver = getDriver(browser, options).build(); + await driver.manage().deleteAllCookies(); + await driver.get(loginUrl); + await driver.wait(until.elementsLocated(By.xpath(signInBtnXPath)), 20000); + await driver.findElement(By.xpath(signInBtnXPath)).click(); + // TODO Use a better XPath. + await driver.wait( + until.elementsLocated(By.xpath('/html/body/div[1]/div/header/div/div[2]')), + 20000 + ); + await driver.wait(until.elementsLocated(By.css('img[data-test-subj="defaultLogo"]')), 20000); + await driver.wait( + until.elementsLocated(By.css('section[aria-labelledby="homDataAdd__title"]')), + 20000 + ); + await driver.wait( + until.elementsLocated(By.css('section[aria-labelledby="homDataManage__title"]')), + 20000 + ); + const windowHash = await driver.getCurrentUrl(); + console.log('windowHash: ' + windowHash); + expect(windowHash).toEqual(urlWithoutHash); + const cookie = await driver.manage().getCookies(); + expect(cookie.length).toEqual(3); + await driver.manage().deleteAllCookies(); + await driver.quit(); + }); +}); + +function getDriver(browser: string, options: Options) { + return new Builder().forBrowser(browser).setFirefoxOptions(options); +}