From cce2844943614fb434255e6ed9ef066ecdea2462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Alvarez=20Pi=C3=B1eiro?= <95703246+emilioalvap@users.noreply.github.com> Date: Fri, 13 Sep 2024 20:18:17 +0200 Subject: [PATCH] feat: add support for in-memory client certificates (#952) * feat: add support for in-memory client certificates * Apply suggestions from code review Co-authored-by: Vignesh Shanmugam * Update snapshots --------- Co-authored-by: Vignesh Shanmugam --- __tests__/options.test.ts | 34 +++++++++++- .../push/__snapshots__/index.test.ts.snap | 27 ++++++++++ __tests__/push/index.test.ts | 53 +++++++++++++------ package-lock.json | 28 +++++----- package.json | 6 +-- src/config.ts | 1 + src/formatter/javascript.ts | 4 +- src/options.ts | 44 ++++++++++++++- src/push/index.ts | 12 ++++- 9 files changed, 169 insertions(+), 40 deletions(-) diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts index 283a7556..0d681407 100644 --- a/__tests__/options.test.ts +++ b/__tests__/options.test.ts @@ -24,7 +24,7 @@ */ import { CliArgs } from '../src/common_types'; -import { normalizeOptions } from '../src/options'; +import { normalizeOptions, parsePlaywrightOptions } from '../src/options'; import { join } from 'path'; describe('options', () => { @@ -66,7 +66,7 @@ describe('options', () => { ignoreHTTPSErrors: undefined, isMobile: true, userAgent: - 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36', viewport: { height: 658, width: 320, @@ -148,4 +148,34 @@ describe('options', () => { }, }); }); + + it('parses cli playwrightOptions.clientCertificates', async () => { + const test = { + clientCertificates: [ + { + key: Buffer.from('This should be revived'), + cert: Buffer.from('This should be revived'), + pfx: Buffer.from('This should be revived'), + origin: Buffer.from('This should not be revived'), + passphrase: Buffer.from('This should not be revived'), + }, + { + key: 'This should be revived', + cert: 'This should be revived', + pfx: 'This should be revived', + origin: 'This should not be revived', + passphrase: 'This should not be revived', + }, + ], + }; + const result = parsePlaywrightOptions(JSON.stringify(test)); + + result.clientCertificates.forEach(t => { + expect(Buffer.isBuffer(t.cert)).toBeTruthy(); + expect(Buffer.isBuffer(t.key)).toBeTruthy(); + expect(Buffer.isBuffer(t.pfx)).toBeTruthy(); + expect(Buffer.isBuffer(t.origin)).toBeFalsy(); + expect(Buffer.isBuffer(t.passphrase)).toBeFalsy(); + }); + }); }); diff --git a/__tests__/push/__snapshots__/index.test.ts.snap b/__tests__/push/__snapshots__/index.test.ts.snap index 82e93d04..08751272 100644 --- a/__tests__/push/__snapshots__/index.test.ts.snap +++ b/__tests__/push/__snapshots__/index.test.ts.snap @@ -1,5 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Push abort on push with clientCertificate.certPath used in cloud 1`] = ` +"Aborted. Invalid synthetics project settings. + +Certificate path options (certPath, keyPath, pfxPath) are not supported on globally managed locations, use in-memory alternatives (cert, key, pfx) when running on cloud. + +Run 'npx @elastic/synthetics init' to create project with default settings. +" +`; + +exports[`Push abort on push with clientCertificate.keyPath used in cloud 1`] = ` +"Aborted. Invalid synthetics project settings. + +Certificate path options (certPath, keyPath, pfxPath) are not supported on globally managed locations, use in-memory alternatives (cert, key, pfx) when running on cloud. + +Run 'npx @elastic/synthetics init' to create project with default settings. +" +`; + +exports[`Push abort on push with clientCertificate.pfxPath used in cloud 1`] = ` +"Aborted. Invalid synthetics project settings. + +Certificate path options (certPath, keyPath, pfxPath) are not supported on globally managed locations, use in-memory alternatives (cert, key, pfx) when running on cloud. + +Run 'npx @elastic/synthetics init' to create project with default settings. +" +`; + exports[`Push error on empty project id 1`] = ` "Aborted. Invalid synthetics project settings. diff --git a/__tests__/push/index.test.ts b/__tests__/push/index.test.ts index 151762ac..3c8e6ecf 100644 --- a/__tests__/push/index.test.ts +++ b/__tests__/push/index.test.ts @@ -53,9 +53,7 @@ describe('Push', () => { ) { await writeFile( join(PROJECT_DIR, filename), - `export default { monitor: ${JSON.stringify( - monitor - )}, project: ${JSON.stringify(settings)} }` + `export default ${JSON.stringify({ ...settings, monitor })}` ); } @@ -89,20 +87,23 @@ describe('Push', () => { }); it('error on invalid location', async () => { - await fakeProjectSetup({ id: 'test-project' }, {}); + await fakeProjectSetup({ project: { id: 'test-project' } }, {}); const output = await runPush(); expect(output).toMatchSnapshot(); }); it('error when schedule is not present', async () => { - await fakeProjectSetup({ id: 'test-project' }, { locations: ['test-loc'] }); + await fakeProjectSetup( + { project: { id: 'test-project' } }, + { locations: ['test-loc'] } + ); const output = await runPush(); expect(output).toMatchSnapshot(); }); it('error on invalid schedule', async () => { await fakeProjectSetup( - { id: 'test-project' }, + { project: { id: 'test-project' } }, { locations: ['test-loc'], schedule: 12 } ); const output = await runPush(); @@ -111,7 +112,7 @@ describe('Push', () => { it('abort on push with different project id', async () => { await fakeProjectSetup( - { id: 'test-project' }, + { project: { id: 'test-project' } }, { locations: ['test-loc'], schedule: 3 } ); const output = await runPush( @@ -125,7 +126,13 @@ describe('Push', () => { it('error on invalid schedule in monitor DSL', async () => { await fakeProjectSetup( - { id: 'test-project', space: 'dummy', url: 'http://localhost:8080' }, + { + project: { + id: 'test-project', + space: 'dummy', + url: 'http://localhost:8080', + }, + }, { locations: ['test-loc'], schedule: 3 } ); const testJourney = join(PROJECT_DIR, 'test.journey.ts'); @@ -141,7 +148,7 @@ journey('journey 1', () => monitor.use({ id: 'j1', schedule: 8 }));` it('errors on duplicate browser monitors', async () => { await fakeProjectSetup( - { id: 'test-project', space: 'dummy', url: server.PREFIX }, + { project: { id: 'test-project', space: 'dummy', url: server.PREFIX } }, { locations: ['test-loc'], schedule: 3 } ); @@ -164,7 +171,7 @@ journey('duplicate name', () => monitor.use({ schedule: 15 }));` it('warn if throttling config is set', async () => { await fakeProjectSetup( - { id: 'test-project' }, + { project: { id: 'test-project' } }, { locations: ['test-loc'], schedule: 3 } ); const testJourney = join(PROJECT_DIR, 'test.journey.ts'); @@ -180,7 +187,7 @@ journey('duplicate name', () => monitor.use({ schedule: 15 }));` it('errors on duplicate lightweight monitors', async () => { await fakeProjectSetup( - { id: 'test-project', space: 'dummy', url: server.PREFIX }, + { project: { id: 'test-project', space: 'dummy', url: server.PREFIX } }, { locations: ['test-loc'], schedule: 3 } ); @@ -220,7 +227,7 @@ heartbeat.monitors: it('error on invalid CHUNK SIZE', async () => { await fakeProjectSetup( - { id: 'test-project', space: 'dummy', url: server.PREFIX }, + { project: { id: 'test-project', space: 'dummy', url: server.PREFIX } }, { locations: ['test-loc'], schedule: 3 } ); const output = await runPush(undefined, { CHUNK_SIZE: '251' }); @@ -231,7 +238,7 @@ heartbeat.monitors: it('respects valid CHUNK SIZE', async () => { await fakeProjectSetup( - { id: 'test-project', space: 'dummy', url: server.PREFIX }, + { project: { id: 'test-project', space: 'dummy', url: server.PREFIX } }, { locations: ['test-loc'], schedule: 3 } ); const testJourney = join(PROJECT_DIR, 'chunk.journey.ts'); @@ -260,7 +267,9 @@ heartbeat.monitors: beforeAll(async () => { server = await createKibanaTestServer(version); await fakeProjectSetup( - { id: 'test-project', space: 'dummy', url: server.PREFIX }, + { + project: { id: 'test-project', space: 'dummy', url: server.PREFIX }, + }, { locations: ['test-loc'], schedule: 3 } ); }); @@ -314,7 +323,7 @@ heartbeat.monitors: journey('journey 1', () => monitor.use({ id: 'j1' }));` ); await fakeProjectSetup( - { id: 'bar', space: 'dummy', url: server.PREFIX }, + { project: { id: 'bar', space: 'dummy', url: server.PREFIX } }, { locations: ['test-loc'], schedule: 3 }, 'synthetics.config.test.ts' ); @@ -330,4 +339,18 @@ heartbeat.monitors: }); }); }); + + ['certPath', 'keyPath', 'pfxPath'].forEach(key => { + it(`abort on push with clientCertificate.${key} used in cloud`, async () => { + await fakeProjectSetup( + { + project: { id: 'test-project', space: 'dummy', url: server.PREFIX }, + playwrightOptions: { clientCertificates: [{ [key]: 'test.file' }] }, + }, + { locations: ['test-loc'], schedule: 3 } + ); + const output = await runPush(); + expect(output).toMatchSnapshot(); + }); + }); }); diff --git a/package-lock.json b/package-lock.json index 0bdc0390..d052b432 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,9 @@ "kleur": "^4.1.5", "micromatch": "^4.0.7", "pirates": "^4.0.5", - "playwright": "=1.45.1", - "playwright-chromium": "=1.45.1", - "playwright-core": "=1.45.1", + "playwright": "=1.47.0", + "playwright-chromium": "=1.47.0", + "playwright-core": "=1.47.0", "semver": "^7.5.4", "sharp": "^0.33.5", "snakecase-keys": "^4.0.1", @@ -13825,11 +13825,11 @@ } }, "node_modules/playwright": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", - "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.0.tgz", + "integrity": "sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==", "dependencies": { - "playwright-core": "1.45.1" + "playwright-core": "1.47.0" }, "bin": { "playwright": "cli.js" @@ -13842,12 +13842,12 @@ } }, "node_modules/playwright-chromium": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-1.45.1.tgz", - "integrity": "sha512-BlYo+kuMg4Jo40Nems2GGVMWdKI2GeHL85D7pkwEW3aq6iEDW3XL7udmoNLOIfluSCKzVRJMB0ta1mt67B3tGA==", + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-1.47.0.tgz", + "integrity": "sha512-S/9ShSLRK6gZZCuon2K0OcEi/t7vmUmx7vqqcpI9/zzKPMWm/+XKKuOHahKXsZLp3DfmRLv7h/PflC19nXZVhA==", "hasInstallScript": true, "dependencies": { - "playwright-core": "1.45.1" + "playwright-core": "1.47.0" }, "bin": { "playwright": "cli.js" @@ -13857,9 +13857,9 @@ } }, "node_modules/playwright-core": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", - "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.0.tgz", + "integrity": "sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==", "bin": { "playwright-core": "cli.js" }, diff --git a/package.json b/package.json index 4440d13c..aa3a2dda 100644 --- a/package.json +++ b/package.json @@ -61,9 +61,9 @@ "kleur": "^4.1.5", "micromatch": "^4.0.7", "pirates": "^4.0.5", - "playwright": "=1.45.1", - "playwright-chromium": "=1.45.1", - "playwright-core": "=1.45.1", + "playwright": "=1.47.0", + "playwright-chromium": "=1.47.0", + "playwright-core": "=1.47.0", "semver": "^7.5.4", "sharp": "^0.33.5", "snakecase-keys": "^4.0.1", diff --git a/src/config.ts b/src/config.ts index 64c8b0e8..c9982eab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -59,6 +59,7 @@ export async function readConfig( options = await optionsPromise; } } + return options; } diff --git a/src/formatter/javascript.ts b/src/formatter/javascript.ts index 2b8af6a3..6a9d8bf5 100644 --- a/src/formatter/javascript.ts +++ b/src/formatter/javascript.ts @@ -26,7 +26,7 @@ import { JavaScriptLanguageGenerator, JavaScriptFormatter, -} from 'playwright-core/lib/server/recorder/javascript'; +} from 'playwright-core/lib/server/codegen/javascript'; export type Step = { actions: ActionInContext[]; @@ -272,7 +272,7 @@ export class SyntheticsGenerator extends JavaScriptLanguageGenerator { if (isAssert && action.command) { formatter.add(toAssertCall(pageAlias, action)); } else { - formatter.add(super._generateActionCall(subject, action)); + formatter.add(super._generateActionCall(subject, actionInContext)); } if (signals.popup) diff --git a/src/options.ts b/src/options.ts index d2be3737..78ed7846 100644 --- a/src/options.ts +++ b/src/options.ts @@ -77,7 +77,15 @@ export async function normalizeOptions( */ const playwrightOpts = merge( config.playwrightOptions, - cliArgs.playwrightOptions || {} + cliArgs.playwrightOptions || {}, + { + arrayMerge(target, source) { + if (source && source.length > 0) { + return [...new Set(source)]; + } + return target; + }, + } ); options.playwrightOptions = { ...playwrightOpts, @@ -195,7 +203,7 @@ export function getCommonCommandOpts() { const playwrightOpts = createOption( '--playwright-options ', 'JSON object to pass in custom Playwright options for the agent. Options passed will be merged with Playwright options defined in your synthetics.config.js file.' - ).argParser(JSON.parse); + ).argParser(parsePlaywrightOptions); const pattern = createOption( '--pattern ', @@ -237,3 +245,35 @@ export function getCommonCommandOpts() { match, }; } + +export function parsePlaywrightOptions(playwrightOpts: string) { + return JSON.parse(playwrightOpts, (key, value) => { + if (key !== 'clientCertificates') { + return value; + } + + // Revive serialized clientCertificates buffer objects + return (value ?? []).map(item => { + const revived = { ...item }; + if (item.cert && !Buffer.isBuffer(item.cert)) { + revived.cert = parseAsBuffer(item.cert); + } + if (item.key && !Buffer.isBuffer(item.key)) { + revived.key = parseAsBuffer(item.key); + } + if (item.pfx && !Buffer.isBuffer(item.pfx)) { + revived.pfx = parseAsBuffer(item.pfx); + } + + return revived; + }); + }); +} + +function parseAsBuffer(value: any): Buffer { + try { + return Buffer.from(value); + } catch (e) { + return value; + } +} diff --git a/src/push/index.ts b/src/push/index.ts index fe4f9dbb..2a123145 100644 --- a/src/push/index.ts +++ b/src/push/index.ts @@ -223,8 +223,16 @@ export function validateSettings(opts: PushOptions) { - CLI '--schedule ' - Config file 'monitors.schedule' field`; } else if (opts.schedule && !ALLOWED_SCHEDULES.includes(opts.schedule)) { - reason = `Set default schedule(${opts.schedule - }) to one of the allowed values - ${ALLOWED_SCHEDULES.join(',')}`; + reason = `Set default schedule(${ + opts.schedule + }) to one of the allowed values - ${ALLOWED_SCHEDULES.join(',')}`; + } else if ( + opts.locations.length > 0 && + (opts?.playwrightOptions?.clientCertificates ?? []).filter(cert => { + return cert.certPath || cert.keyPath || cert.pfxPath; + }).length > 0 + ) { + reason = `Certificate path options (certPath, keyPath, pfxPath) are not supported on globally managed locations, use in-memory alternatives (cert, key, pfx) when running on cloud.`; } if (!reason) return;