Skip to content

Commit

Permalink
feat(repo): load system certs when custom cert specified
Browse files Browse the repository at this point in the history
fixes #139
  • Loading branch information
jwulf committed Apr 29, 2024
1 parent 49e9901 commit 43d64a6
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 119 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/pr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,39 @@ jobs:
CAMUNDA_CONSOLE_CLIENT_SECRET: ${{ secrets.CAMUNDA_CONSOLE_CLIENT_SECRET }}
CAMUNDA_CONSOLE_BASE_URL: ${{ secrets.CAMUNDA_CONSOLE_BASE_URL }}
CAMUNDA_CONSOLE_OAUTH_AUDIENCE: ${{ secrets.CAMUNDA_CONSOLE_OAUTH_AUDIENCE}}

saas_integration_windows:
needs: saas_integration
runs-on: windows-latest
environment: integration
steps:
- name: Check out the repo
uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "18" # Specify a Node.js version

- name: Install dependencies
run: npm install

- name: Run Integration Tests
run: |
npm run test:integration
env:
ZEEBE_ADDRESS: ${{ secrets.ZEEBE_ADDRESS }}
ZEEBE_CLIENT_ID: ${{ secrets.ZEEBE_CLIENT_ID }}
ZEEBE_AUTHORIZATION_SERVER_URL: ${{ secrets.ZEEBE_AUTHORIZATION_SERVER_URL }}
ZEEBE_CLIENT_SECRET: ${{ secrets.ZEEBE_CLIENT_SECRET }}
ZEEBE_TOKEN_AUDIENCE: ${{ secrets.ZEEBE_TOKEN_AUDIENCE }}
CAMUNDA_CREDENTIALS_SCOPES: ${{ secrets.CAMUNDA_CREDENTIALS_SCOPES }}
CAMUNDA_OAUTH_URL: ${{ secrets.CAMUNDA_OAUTH_URL }}
CAMUNDA_TASKLIST_BASE_URL: ${{ secrets.CAMUNDA_TASKLIST_BASE_URL }}
CAMUNDA_OPERATE_BASE_URL: ${{ secrets.CAMUNDA_OPERATE_BASE_URL }}
CAMUNDA_OPTIMIZE_BASE_URL: ${{ secrets.CAMUNDA_OPTIMIZE_BASE_URL }}
CAMUNDA_MODELER_BASE_URL: https://modeler.cloud.camunda.io/api
CAMUNDA_CONSOLE_CLIENT_ID: ${{ secrets.CAMUNDA_CONSOLE_CLIENT_ID }}
CAMUNDA_CONSOLE_CLIENT_SECRET: ${{ secrets.CAMUNDA_CONSOLE_CLIENT_SECRET }}
CAMUNDA_CONSOLE_BASE_URL: ${{ secrets.CAMUNDA_CONSOLE_BASE_URL }}
CAMUNDA_CONSOLE_OAUTH_AUDIENCE: ${{ secrets.CAMUNDA_CONSOLE_OAUTH_AUDIENCE}}
36 changes: 36 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,46 @@ jobs:
CAMUNDA_CONSOLE_BASE_URL: ${{ secrets.CAMUNDA_CONSOLE_BASE_URL }}
CAMUNDA_CONSOLE_OAUTH_AUDIENCE: ${{ secrets.CAMUNDA_CONSOLE_OAUTH_AUDIENCE}}

saas_integration_windows:
needs: saas_integration
runs-on: windows-latest
environment: integration
steps:
- name: Check out the repo
uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "18" # Specify a Node.js version

- name: Install dependencies
run: npm install

- name: Run Integration Tests
run: |
npm run test:integration
env:
ZEEBE_ADDRESS: ${{ secrets.ZEEBE_ADDRESS }}
ZEEBE_CLIENT_ID: ${{ secrets.ZEEBE_CLIENT_ID }}
ZEEBE_AUTHORIZATION_SERVER_URL: ${{ secrets.ZEEBE_AUTHORIZATION_SERVER_URL }}
ZEEBE_CLIENT_SECRET: ${{ secrets.ZEEBE_CLIENT_SECRET }}
ZEEBE_TOKEN_AUDIENCE: ${{ secrets.ZEEBE_TOKEN_AUDIENCE }}
CAMUNDA_CREDENTIALS_SCOPES: ${{ secrets.CAMUNDA_CREDENTIALS_SCOPES }}
CAMUNDA_OAUTH_URL: ${{ secrets.CAMUNDA_OAUTH_URL }}
CAMUNDA_TASKLIST_BASE_URL: ${{ secrets.CAMUNDA_TASKLIST_BASE_URL }}
CAMUNDA_OPERATE_BASE_URL: ${{ secrets.CAMUNDA_OPERATE_BASE_URL }}
CAMUNDA_OPTIMIZE_BASE_URL: ${{ secrets.CAMUNDA_OPTIMIZE_BASE_URL }}
CAMUNDA_MODELER_BASE_URL: https://modeler.cloud.camunda.io/api
CAMUNDA_CONSOLE_CLIENT_ID: ${{ secrets.CAMUNDA_CONSOLE_CLIENT_ID }}
CAMUNDA_CONSOLE_CLIENT_SECRET: ${{ secrets.CAMUNDA_CONSOLE_CLIENT_SECRET }}
CAMUNDA_CONSOLE_BASE_URL: ${{ secrets.CAMUNDA_CONSOLE_BASE_URL }}
CAMUNDA_CONSOLE_OAUTH_AUDIENCE: ${{ secrets.CAMUNDA_CONSOLE_OAUTH_AUDIENCE}}
tag-and-publish:
needs:
[
saas_integration,
saas_integration_windows,
local_multitenancy_integration,
local_integration,
unit-tests,
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@
"typedoc-plugin-missing-exports": "^2.2.0",
"typescript": "^5.3.3"
},
"optionalDependencies": {
"vscode-windows-ca-certs": "0.3.1"
},
"dependencies": {
"@grpc/grpc-js": "1.9.7",
"@grpc/proto-loader": "0.7.10",
Expand Down
3 changes: 1 addition & 2 deletions src/__tests__/operate/operate-integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ test('test error type', async () => {
// `string`
expect((e.response?.body as string).includes('404')).toBe(true)
if (e instanceof HTTPError) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((e as any).statusCode).toBe(404)
expect(e.statusCode).toBe(404)
}
expect(e instanceof HTTPError).toBe(true)
return false
Expand Down
34 changes: 17 additions & 17 deletions src/admin/lib/AdminApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
CamundaEnvironmentConfigurator,
CamundaPlatform8Configuration,
DeepPartial,
GetCertificateAuthority,
GetCustomCertificateBuffer,
GotRetryConfig,
RequireConfiguration,
constructOAuthProvider,
Expand All @@ -27,7 +27,7 @@ const debug = d('camunda:adminconsole')
export class AdminApiClient {
private userAgentString: string
private oAuthProvider: IOAuthProvider
private rest: typeof got
private rest!: typeof got

constructor(options?: {
config?: DeepPartial<CamundaPlatform8Configuration>
Expand All @@ -44,23 +44,23 @@ export class AdminApiClient {
this.oAuthProvider =
options?.oAuthProvider ?? constructOAuthProvider(config)

const certificateAuthority = GetCertificateAuthority(config)

this.userAgentString = createUserAgentString(config)
const prefixUrl = `${baseUrl}/clusters`
this.rest = got.extend({
prefixUrl,
retry: GotRetryConfig,
https: {
certificateAuthority,
},
handlers: [gotErrorHandler],
hooks: {
beforeRetry: [
makeBeforeRetryHandlerFor401TokenRetry(this.getHeaders.bind(this)),
],
beforeError: [gotBeforeErrorHook],
},
GetCustomCertificateBuffer(config).then((certificateAuthority) => {
this.rest = got.extend({
prefixUrl,
retry: GotRetryConfig,
https: {
certificateAuthority,
},
handlers: [gotErrorHandler],
hooks: {
beforeRetry: [
makeBeforeRetryHandlerFor401TokenRetry(this.getHeaders.bind(this)),
],
beforeError: [gotBeforeErrorHook],
},
})
})
debug('prefixUrl', `${baseUrl}/clusters`)
}
Expand Down
62 changes: 57 additions & 5 deletions src/lib/CertificateAuthority.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,64 @@
import { X509Certificate } from 'crypto'
import fs from 'fs'
import path from 'path'

import { CamundaPlatform8Configuration } from './Configuration'
import { getSystemCertificates } from './GetSystemCertificates'

export function GetCertificateAuthority(
export async function GetCustomCertificateBuffer(
config: CamundaPlatform8Configuration
): string | undefined {
): Promise<Buffer | undefined> {
const customRootCertPath = config.CAMUNDA_CUSTOM_ROOT_CERT_PATH
return customRootCertPath
? fs.readFileSync(customRootCertPath, 'utf-8')
: undefined

if (!customRootCertPath) {
return undefined
}
const rootCerts: string[] = []

if (customRootCertPath) {
const cert = readRootCertificate(customRootCertPath)

if (cert) {
rootCerts.push(cert)
}
}

// (2) use certificates from OS keychain
const systemCertificates = await getSystemCertificates()
rootCerts.push(...systemCertificates)

if (!rootCerts.length) {
return undefined
}

return Buffer.from(rootCerts.join('\n'))
}

function readRootCertificate(certPath) {
let cert

try {
const absolutePath = path.isAbsolute(certPath)
? certPath
: path.join(process.cwd(), certPath)

cert = fs.readFileSync(absolutePath)
} catch (err) {
console.error('Failed to read custom SSL certificate:', err)

return
}

let parsed
try {
parsed = new X509Certificate(cert)
} catch (err) {
console.warn('Failed to parse custom SSL certificate:', err)
}

if (parsed && parsed.issuer !== parsed.subject) {
console.warn('Custom SSL certificate appears to be not a root certificate')
}

return cert
}
159 changes: 159 additions & 0 deletions src/lib/GetSystemCertificates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership.
*
* Camunda licenses this file to you under the MIT; you may not use this file
* except in compliance with the MIT License.
*/

/**
* This file contains code adapted from https://github.com/microsoft/vscode-proxy-agent
*
* MIT License
*
* Copyright (c) 2014 Nathan Rajlich &lt;nathan@tootallnate.net&gt;
* Copyright (c) 2015 Félicien François &lt;felicien@tweakstyle.com&gt;
* Copyright (c) Microsoft Corporation.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* 'Software'), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import cp from 'child_process'
import fs from 'fs/promises'
import os from 'os'

let _cachedCertificates

module.exports = getSystemCertificates

/**
* Get certificates from the system keychain.
*
*/
export async function getSystemCertificates(): Promise<string[]> {
if (_cachedCertificates) {
return _cachedCertificates
}

_cachedCertificates = (await readCaCertificates()) || []

return _cachedCertificates
}

async function readCaCertificates() {
if (process.platform === 'win32') {
return readWindowsCaCertificates()
}
if (process.platform === 'darwin') {
return readMacCaCertificates()
}
if (process.platform === 'linux') {
return readLinuxCaCertificates()
}
throw new Error(`Unsupported platform: ${process.platform}`)
}

async function readWindowsCaCertificates() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const winCA = require('vscode-windows-ca-certs')
const ders: string[] = []
const store = new winCA.Crypt32()
try {
let der = store.next()
while (der) {
ders.push(der)
der = store.next()
}
} finally {
store.done()
}

const certs = new Set(ders.map(derToPem))

return Array.from(certs)
}

async function readMacCaCertificates() {
const args = ['find-certificate', '-a', '-p']
const systemRootCertsPath =
'/System/Library/Keychains/SystemRootCertificates.keychain'

const trusted = await spawnPromise('/usr/bin/security', args)
const systemTrusted = await spawnPromise('/usr/bin/security', [
...args,
systemRootCertsPath,
])

const certs = new Set(splitCerts(trusted).concat(splitCerts(systemTrusted)))

return Array.from(certs)
}

const linuxCaCertificatePaths = [
'/etc/ssl/certs/ca-certificates.crt',
'/etc/ssl/certs/ca-bundle.crt',
]

async function readLinuxCaCertificates() {
for (const certPath of linuxCaCertificatePaths) {
try {
const content = await fs.readFile(certPath, { encoding: 'utf8' })
const certs = new Set(splitCerts(content))
return Array.from(certs)
} catch (err) {
if ((err as { code?: string })?.code !== 'ENOENT') {
throw err
}
}
}
// Return an empty array if no certificates are found
return []
}

// helper /////////////////

function spawnPromise(command, args) {
return new Promise((resolve, reject) => {
const child = cp.spawn(command, args)
const stdout: string[] = []
child.stdout.setEncoding('utf8')
child.stdout.on('data', (str) => stdout.push(str))
child.on('error', reject)
child.on('exit', (code) => (code ? reject(code) : resolve(stdout.join(''))))
})
}

function derToPem(blob) {
const lines = ['-----BEGIN CERTIFICATE-----']
const der = blob.toString('base64')
for (let i = 0; i < der.length; i += 64) {
lines.push(der.substr(i, 64))
}
lines.push('-----END CERTIFICATE-----', '')
return lines.join(os.EOL)
}

function splitCerts(certs) {
return certs
.split(/(?=-----BEGIN CERTIFICATE-----)/g)
.filter((pem) => !!pem.length)
}
Loading

0 comments on commit 43d64a6

Please sign in to comment.