From 02558d0517487f6db81ce73f5fa8ea1d5567eb0a Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 10 May 2024 09:09:27 +1200 Subject: [PATCH 01/10] test(zeebe): fix set variables when throwing a BPMN error (#156) * test(zeebe): fix set variables when throwing a BPMN error * test(zeebe): turn on debugging info for throw error test * test(zeebe): update throw error test * test(repo): check different error message for Windows * test(repo): use string for custom certificates --- docker/.env | 8 +- package.json | 16 ++- .../GetCustomCertificateBuffer.unit.spec.ts | 12 +- .../testdata/Client-ThrowError-2.bpmn | 127 ++++++++++++++++++ src/__tests__/testdata/Client-ThrowError.bpmn | 61 +++++---- .../integration/Client-ThrowError.spec.ts | 24 ++-- src/lib/GetCustomCertificateBuffer.ts | 15 ++- src/operate/lib/OperateApiClient.ts | 5 + src/subscription/index.ts | 0 src/zeebe/lib/cancelProcesses.ts | 1 + src/zeebe/zb/ZeebeGrpcClient.ts | 2 +- 11 files changed, 221 insertions(+), 50 deletions(-) create mode 100644 src/__tests__/testdata/Client-ThrowError-2.bpmn create mode 100644 src/subscription/index.ts diff --git a/docker/.env b/docker/.env index 90c56fb3..c72a4440 100644 --- a/docker/.env +++ b/docker/.env @@ -1,9 +1,9 @@ ## Image versions ## # CAMUNDA_CONNECTORS_VERSION=0.23.2 -CAMUNDA_CONNECTORS_VERSION=8.5.0-alpha2 -CAMUNDA_OPTIMIZE_VERSION=8.5.0-alpha2 -CAMUNDA_PLATFORM_VERSION=8.5.0-alpha2 -CAMUNDA_WEB_MODELER_VERSION=8.4.1 +CAMUNDA_CONNECTORS_VERSION=8.5.0 +CAMUNDA_OPTIMIZE_VERSION=8.5.0 +CAMUNDA_PLATFORM_VERSION=8.5.0 +CAMUNDA_WEB_MODELER_VERSION=8.5.0 ELASTIC_VERSION=8.9.0 KEYCLOAK_SERVER_VERSION=22.0.3 MAILPIT_VERSION=v1.7.0 diff --git a/package.json b/package.json index 36b552dc..694aa404 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "compile": "tsc --project tsconfig.json", "docs": "rm -rf ./docs && typedoc", "generate:grpc": "grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:./src/generated --grpc_out=./src/generated --ts_out=./src/generated -I ./src/proto ./src/proto/*.proto", - "test": "cross-env CAMUNDA_UNIT_TEST=true jest '.*unit.*' -u --detectOpenHandles --testPathIgnorePatterns integration --testPathIgnorePatterns local-integration --testPathIgnorePatterns disconnection --testPathIgnorePatterns multitenancy --testPathIgnorePatterns __tests__/config", + "test": "cross-env CAMUNDA_UNIT_TEST=true jest '.*unit.*' -u --detectOpenHandles --runInBand --testPathIgnorePatterns integration --testPathIgnorePatterns local-integration --testPathIgnorePatterns disconnection --testPathIgnorePatterns multitenancy --testPathIgnorePatterns __tests__/config", "test:integration": "jest --runInBand --testPathIgnorePatterns disconnection --testPathIgnorePatterns '.unit.*' --testPathIgnorePatterns __tests__/config --testPathIgnorePatterns multitenancy --detectOpenHandles --verbose true -u", "test:multitenancy": "jest --runInBand --testPathIgnorePatterns disconnection --testPathIgnorePatterns admin --testPathIgnorePatterns '.unit.*' --testPathIgnorePatterns __tests__/config - --detectOpenHandles --verbose true -u", "test:local": "jest --runInBand --verbose true --detectOpenHandles local-integration -u", @@ -23,7 +23,19 @@ "lint": "eslint 'src/**/*.{ts,tsx}'", "format": "prettier --write 'src/**/*.ts'" }, - "keywords": [], + "keywords": [ + "zeebe", + "zeebe-node", + "camunda", + "automation", + "bpmn", + "camunda 8", + "operate", + "optimize", + "tasklist", + "web modeler", + "modeler" + ], "private": false, "publishConfig": { "access": "public" diff --git a/src/__tests__/lib/GetCustomCertificateBuffer.unit.spec.ts b/src/__tests__/lib/GetCustomCertificateBuffer.unit.spec.ts index 07507d21..84eaf0ee 100644 --- a/src/__tests__/lib/GetCustomCertificateBuffer.unit.spec.ts +++ b/src/__tests__/lib/GetCustomCertificateBuffer.unit.spec.ts @@ -53,8 +53,11 @@ test('Can use a custom root certificate to connect to a REST API', async () => { CAMUNDA_OPERATE_BASE_URL: 'https://localhost:3012', }, }) - + console.log('Trying to get process instance with certificate') const res = await c.getProcessInstance('1') + console.log( + `Got response from self-signed secured server: ${res.bpmnProcessId}` + ) expect(res.bpmnProcessId).toBe('test') const c1 = new OperateApiClient({ config: { @@ -65,10 +68,15 @@ test('Can use a custom root certificate to connect to a REST API', async () => { let threw = false try { + console.log('Trying to get process instance without certificate') await c1.getProcessInstance('1') } catch (e) { threw = true - expect((e as { code: string }).code).toBe('DEPTH_ZERO_SELF_SIGNED_CERT') + const correctErrorOnLinux = + (e as { code: string }).code === 'DEPTH_ZERO_SELF_SIGNED_CERT' + const correctErrorOnWindows = + (e as { code: string }).code === 'self-signed certificate' + expect(correctErrorOnLinux || correctErrorOnWindows).toBe(true) } expect(threw).toBe(true) server.close() diff --git a/src/__tests__/testdata/Client-ThrowError-2.bpmn b/src/__tests__/testdata/Client-ThrowError-2.bpmn new file mode 100644 index 00000000..3574bffa --- /dev/null +++ b/src/__tests__/testdata/Client-ThrowError-2.bpmn @@ -0,0 +1,127 @@ + + + + + SequenceFlow_0gyuvi7 + SequenceFlow_1o5o2aq + + SequenceFlow_199k4yb + + + SequenceFlow_07t3zxy + + + + + + + + SequenceFlow_199k4yb + SequenceFlow_07t3zxy + + + + + + + + + SequenceFlow_0uyuklc + + + + SequenceFlow_1o5o2aq + + + + + SequenceFlow_1o4en09 + + + + + + + SequenceFlow_0uyuklc + SequenceFlow_1o4en09 + + + SequenceFlow_0gyuvi7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/__tests__/testdata/Client-ThrowError.bpmn b/src/__tests__/testdata/Client-ThrowError.bpmn index 1d2eb238..c7ff7646 100644 --- a/src/__tests__/testdata/Client-ThrowError.bpmn +++ b/src/__tests__/testdata/Client-ThrowError.bpmn @@ -1,5 +1,5 @@ - + SequenceFlow_0gyuvi7 @@ -21,6 +21,11 @@ + + + + + SequenceFlow_0uyuklc @@ -63,47 +68,33 @@ - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + @@ -111,6 +102,22 @@ + + + + + + + + + + + + + + + + diff --git a/src/__tests__/zeebe/integration/Client-ThrowError.spec.ts b/src/__tests__/zeebe/integration/Client-ThrowError.spec.ts index fb89a4fb..e52be470 100644 --- a/src/__tests__/zeebe/integration/Client-ThrowError.spec.ts +++ b/src/__tests__/zeebe/integration/Client-ThrowError.spec.ts @@ -27,18 +27,21 @@ test('Throws a business error that is caught in the process', async () => { }) ).deployments[0].process) await cancelProcesses(processDefinitionKey) + zbc.createWorker({ - taskHandler: (job) => - job.error('BUSINESS_ERROR', 'Well, that did not work'), + taskHandler: (job) => { + return job.error('BUSINESS_ERROR', 'Well, that did not work') + }, taskType: 'throw-bpmn-error-task', timeout: Duration.seconds.of(30), }) zbc.createWorker({ taskType: 'sad-flow', - taskHandler: (job) => - job.complete({ + taskHandler: (job) => { + return job.complete({ bpmnErrorCaught: true, - }), + }) + }, }) const result = await zbc.createProcessInstanceWithResult({ bpmnProcessId, @@ -53,7 +56,7 @@ test('Can set variables when throwing a BPMN Error', async () => { const zbc = new ZeebeGrpcClient() ;({ bpmnProcessId, processDefinitionKey } = ( await zbc.deployResource({ - processFilename: './src/__tests__/testdata/Client-ThrowError.bpmn', + processFilename: './src/__tests__/testdata/Client-ThrowError-2.bpmn', }) ).deployments[0].process) await cancelProcesses(processDefinitionKey) @@ -65,11 +68,11 @@ test('Can set variables when throwing a BPMN Error', async () => { errorMessage: "Well, that didn't work", variables: { something: 'someValue' }, }), - taskType: 'throw-bpmn-error-task', + taskType: 'throw-bpmn-error-task-2', }) // This worker is on the business error throw path zbc.createWorker({ - taskType: 'sad-flow', + taskType: 'sad-flow-2', taskHandler: (job) => job.complete({ bpmnErrorCaught: true, @@ -81,8 +84,7 @@ test('Can set variables when throwing a BPMN Error', async () => { variables: {}, }) expect(result.variables.bpmnErrorCaught).toBe(true) - // This is not working, the variable is not being set on 8.5 - // this may be due to incremental implementation of the feature - // expect(result.variables.something).toBe('someValue') + // This requires output mapping on the error in the BPMN diagram + expect(result.variables.something).toBe('someValue') await zbc.close() }) diff --git a/src/lib/GetCustomCertificateBuffer.ts b/src/lib/GetCustomCertificateBuffer.ts index 8c6ca584..5197a910 100644 --- a/src/lib/GetCustomCertificateBuffer.ts +++ b/src/lib/GetCustomCertificateBuffer.ts @@ -2,27 +2,34 @@ import { X509Certificate } from 'crypto' import fs from 'fs' import path from 'path' +import { debug } from 'debug' + import { CamundaPlatform8Configuration } from './Configuration' import { getSystemCertificates } from './GetSystemCertificates' +const trace = debug('camunda:certificate') + export async function GetCustomCertificateBuffer( config: CamundaPlatform8Configuration -): Promise { +): Promise { const customRootCertPath = config.CAMUNDA_CUSTOM_ROOT_CERT_PATH const customRootCert = config.CAMUNDA_CUSTOM_ROOT_CERT_STRING if (!customRootCertPath && !customRootCert) { + trace(`No custom root certificate configured`) return undefined } const rootCerts: string[] = [] if (customRootCertPath) { + trace(`Using custom root certificate from file: ${customRootCertPath}`) const cert = readRootCertificate(customRootCertPath) if (cert) { rootCerts.push(cert) } } else if (customRootCert) { + trace(`Using custom root certificate from string`) rootCerts.push(customRootCert) } @@ -31,10 +38,12 @@ export async function GetCustomCertificateBuffer( rootCerts.push(...systemCertificates) if (!rootCerts.length) { + trace(`No custom root certificates found`) return undefined } - - return Buffer.from(rootCerts.join('\n')) + const output = rootCerts.join('\n') + trace(`Custom root certificates:\n${output}`) + return output } function readRootCertificate(certPath) { diff --git a/src/operate/lib/OperateApiClient.ts b/src/operate/lib/OperateApiClient.ts index d7baf528..6329a87e 100644 --- a/src/operate/lib/OperateApiClient.ts +++ b/src/operate/lib/OperateApiClient.ts @@ -1,3 +1,4 @@ +import { debug } from 'debug' import got from 'got' import { @@ -33,6 +34,8 @@ import { } from './OperateDto' import { parseSearchResults } from './parseSearchResults' +const trace = debug('camunda:operate') + const OPERATE_API_VERSION = 'v1' type JSONDoc = { [key: string]: string | boolean | number | JSONDoc } @@ -80,6 +83,8 @@ export class OperateApiClient { const config = CamundaEnvironmentConfigurator.mergeConfigWithEnvironment( options?.config ?? {} ) + trace('options.config', options?.config) + trace('config', config) this.oAuthProvider = options?.oAuthProvider ?? constructOAuthProvider(config) this.userAgentString = createUserAgentString(config) diff --git a/src/subscription/index.ts b/src/subscription/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/zeebe/lib/cancelProcesses.ts b/src/zeebe/lib/cancelProcesses.ts index cc74ad59..d45ec61c 100644 --- a/src/zeebe/lib/cancelProcesses.ts +++ b/src/zeebe/lib/cancelProcesses.ts @@ -10,6 +10,7 @@ export async function cancelProcesses(processDefinitionKey: string) { .searchProcessInstances({ filter: { processDefinitionKey, + state: 'ACTIVE', }, }) .catch((e) => { diff --git a/src/zeebe/zb/ZeebeGrpcClient.ts b/src/zeebe/zb/ZeebeGrpcClient.ts index 257551d8..72ae63ff 100644 --- a/src/zeebe/zb/ZeebeGrpcClient.ts +++ b/src/zeebe/zb/ZeebeGrpcClient.ts @@ -159,7 +159,7 @@ export class ZeebeGrpcClient extends TypedEmitter< const customSSL = { certChain: certChainPath ? readFileSync(certChainPath) : undefined, privateKey: privateKeyPath ? readFileSync(privateKeyPath) : undefined, - rootCerts, + rootCerts: rootCerts ? Buffer.from(rootCerts) : undefined, } this.customSSL = customSSL From f05aa8aa1670cbceb40a54b3bf4a8e40228ad2c3 Mon Sep 17 00:00:00 2001 From: Mario Hammer Date: Wed, 15 May 2024 23:42:56 +0200 Subject: [PATCH 02/10] feat(oauth): add conditional loading of client key and cert for getting a token (#161) Support mTLS for REST API clients with `CAMUNDA_CUSTOM_CERT_CHAIN_PATH` and `CAMUNDA_CUSTOM_PRIVATE_KEY_PATH`. --- src/oauth/lib/OAuthProvider.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/oauth/lib/OAuthProvider.ts b/src/oauth/lib/OAuthProvider.ts index ede08f59..ee7db305 100644 --- a/src/oauth/lib/OAuthProvider.ts +++ b/src/oauth/lib/OAuthProvider.ts @@ -30,6 +30,8 @@ export class OAuthProvider implements IOAuthProvider { private static readonly defaultTokenCache = `${homedir}/.camunda` private cacheDir: string private authServerUrl: string + private mTLSPrivateKey: string | undefined + private mTLSCertChain: string | undefined private clientId: string | undefined private clientSecret: string | undefined private useFileCache: boolean @@ -61,6 +63,12 @@ export class OAuthProvider implements IOAuthProvider { this.clientId = config.ZEEBE_CLIENT_ID this.clientSecret = config.ZEEBE_CLIENT_SECRET + this.mTLSPrivateKey = config.CAMUNDA_CUSTOM_PRIVATE_KEY_PATH + ? fs.readFileSync(config.CAMUNDA_CUSTOM_PRIVATE_KEY_PATH).toString() + : undefined + this.mTLSCertChain = config.CAMUNDA_CUSTOM_CERT_CHAIN_PATH + ? fs.readFileSync(config.CAMUNDA_CUSTOM_CERT_CHAIN_PATH).toString() + : undefined this.consoleClientId = config.CAMUNDA_CONSOLE_CLIENT_ID this.consoleClientSecret = config.CAMUNDA_CONSOLE_CLIENT_SECRET @@ -284,6 +292,8 @@ export class OAuthProvider implements IOAuthProvider { 'user-agent': this.userAgentString, accept: '*/*', }, + key: this.mTLSPrivateKey, + cert: this.mTLSCertChain, } trace(`Making token request to the token endpoint: `) From 9fe762af6fa3e9a77dbf650497003f86424ea399 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 15 May 2024 22:22:56 +0000 Subject: [PATCH 03/10] chore(release): 8.5.4-alpha.1 [skip ci] ## [8.5.4-alpha.1](https://github.com/camunda/camunda-8-js-sdk/compare/v8.5.3...v8.5.4-alpha.1) (2024-05-15) ### Features * **oauth:** add conditional loading of client key and cert for getting a token ([#161](https://github.com/camunda/camunda-8-js-sdk/issues/161)) ([f05aa8a](https://github.com/camunda/camunda-8-js-sdk/commit/f05aa8aa1670cbceb40a54b3bf4a8e40228ad2c3)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 694aa404..89d129d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@camunda8/sdk", - "version": "8.5.3", + "version": "8.5.4-alpha.1", "description": "", "main": "dist/index.js", "scripts": { From 992f7bdce3fbd57212e717da04434cbb23fb1d8c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 15 May 2024 22:22:57 +0000 Subject: [PATCH 04/10] chore(release): 8.5.4-alpha.1 [skip ci] ## [8.5.4-alpha.1](https://github.com/camunda/camunda-8-js-sdk/compare/v8.5.3...v8.5.4-alpha.1) (2024-05-15) ### Features * **oauth:** add conditional loading of client key and cert for getting a token ([#161](https://github.com/camunda/camunda-8-js-sdk/issues/161)) ([f05aa8a](https://github.com/camunda/camunda-8-js-sdk/commit/f05aa8aa1670cbceb40a54b3bf4a8e40228ad2c3)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fce2a09..5ab89562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [8.5.4-alpha.1](https://github.com/camunda/camunda-8-js-sdk/compare/v8.5.3...v8.5.4-alpha.1) (2024-05-15) + + +### Features + +* **oauth:** add conditional loading of client key and cert for getting a token ([#161](https://github.com/camunda/camunda-8-js-sdk/issues/161)) ([f05aa8a](https://github.com/camunda/camunda-8-js-sdk/commit/f05aa8aa1670cbceb40a54b3bf4a8e40228ad2c3)) + ## [8.5.3](https://github.com/camunda/camunda-8-js-sdk/compare/v8.5.2...v8.5.3) (2024-05-08) diff --git a/package-lock.json b/package-lock.json index dcb7ff3c..4ed63135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@camunda8/sdk", - "version": "8.5.3", + "version": "8.5.4-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@camunda8/sdk", - "version": "8.5.3", + "version": "8.5.4-alpha.1", "license": "Apache 2.0", "dependencies": { "@grpc/grpc-js": "1.10.7", From 41fdca0fcc9f7221c915dc82317e6609bb5106ee Mon Sep 17 00:00:00 2001 From: Hasan Alnatour Date: Sun, 19 May 2024 21:40:29 -0400 Subject: [PATCH 05/10] fix(issue137): support ZEEBE_REST_ADDRESS and ZEEBE_GRPC_ADDRESS environment variables (#159) --- CONTRIBUTING.md | 6 ++++-- QUICKSTART.md | 6 ++++-- README.md | 27 +++++++++++++++------------ smoke-test/smoke-test.js | 3 ++- src/lib/Configuration.ts | 13 +++++++++++++ src/zeebe/zb/ZeebeGrpcClient.ts | 4 ++-- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a8766ae..f0fb27d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,8 @@ Put the following credentials in the environment: ```bash # Self-Managed export ZEEBE_SECURE_CONNECTION=false -export ZEEBE_ADDRESS='localhost:26500' +export ZEEBE_GRPC_ADDRESS='localhost:26500' +export ZEEBE_REST_ADDRESS='localhost:8080/v1/' export ZEEBE_CLIENT_ID='zeebe' export ZEEBE_CLIENT_SECRET='zecret' export ZEEBE_AUTHORIZATION_SERVER_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' @@ -74,7 +75,8 @@ To run the multi-tenancy tests, use the following environment variables: ```bash # Self-Managed export ZEEBE_SECURE_CONNECTION=false -export ZEEBE_ADDRESS='localhost:26500' +export ZEEBE_GRPC_ADDRESS='localhost:26500' +export ZEEBE_REST_ADDRESS='localhost:8080/v1/' export ZEEBE_CLIENT_ID='zeebe' export ZEEBE_CLIENT_SECRET='zecret' export ZEEBE_AUTHORIZATION_SERVER_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' diff --git a/QUICKSTART.md b/QUICKSTART.md index 190edd7d..fd36241c 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -39,7 +39,8 @@ The environment variables that you need to set are the following (replace with y ```bash # Self-Managed -export ZEEBE_ADDRESS='localhost:26500' +export ZEEBE_GRPC_ADDRESS='localhost:26500' +export ZEEBE_REST_ADDRESS='localhost:8080/v1/' export ZEEBE_CLIENT_ID='zeebe' export ZEEBE_CLIENT_SECRET='zecret' export CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' @@ -64,7 +65,8 @@ export CAMUNDA_SECURE_CONNECTION=false ### Camunda SaaS configuration ```bash -export ZEEBE_ADDRESS='5c34c0a7-...-125615f7a9b9.syd-1.zeebe.camunda.io:443' +export ZEEBE_GRPC_ADDRESS='5c34c0a7-...-125615f7a9b9.syd-1.zeebe.camunda.io:443' +export ZEEBE_REST_ADDRESS='https://syd-1.zeebe.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9' export ZEEBE_CLIENT_ID='yvvURO...' export ZEEBE_CLIENT_SECRET='iJJu-SHg...' export CAMUNDA_TASKLIST_BASE_URL='https://syd-1.tasklist.camunda.io/5c34c0a7-...-125615f7a9b9' diff --git a/README.md b/README.md index 8905826f..da6413ee 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ With this environment variable set, the SDK will inject a `NullAuthProvider` tha To get a token for use with the application APIs, provide the following configuration fields at a minimum, either via the `Camunda8` constructor or in environment variables: ```bash -ZEEBE_ADDRESS +ZEEBE_GRPC_ADDRESS ZEEBE_CLIENT_ID ZEEBE_CLIENT_SECRET CAMUNDA_OAUTH_URL @@ -118,7 +118,8 @@ This is the complete environment configuration needed to run against the Dockeri ```bash # Self-Managed -export ZEEBE_ADDRESS='localhost:26500' +export ZEEBE_GRPC_ADDRESS='localhost:26500' +export ZEEBE_REST_ADDRESS='localhost:8080/v1/' export ZEEBE_CLIENT_ID='zeebe' export ZEEBE_CLIENT_SECRET='zecret' export CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' @@ -147,15 +148,16 @@ Here is an example of doing this via the constructor, rather than via the enviro import { Camunda8 } from '@camunda8/sdk' const c8 = new Camunda8({ - ZEEBE_ADDRESS: 'localhost:26500' - ZEEBE_CLIENT_ID: 'zeebe' - ZEEBE_CLIENT_SECRET: 'zecret' - CAMUNDA_OAUTH_URL: 'http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' - CAMUNDA_TASKLIST_BASE_URL: 'http://localhost:8082' - CAMUNDA_OPERATE_BASE_URL: 'http://localhost:8081' - CAMUNDA_OPTIMIZE_BASE_URL: 'http://localhost:8083' - CAMUNDA_MODELER_BASE_URL: 'http://localhost:8070/api' - CAMUNDA_TENANT_ID: '' // We can override values in the env by passing an empty string value + ZEEBE_GRPC_ADDRESS: 'localhost:26500', + ZEEBE_REST_ADDRESS: 'localhost:8080/v1/', + ZEEBE_CLIENT_ID: 'zeebe', + ZEEBE_CLIENT_SECRET: 'zecret', + CAMUNDA_OAUTH_URL: 'http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token', + CAMUNDA_TASKLIST_BASE_URL: 'http://localhost:8082', + CAMUNDA_OPERATE_BASE_URL: 'http://localhost:8081', + CAMUNDA_OPTIMIZE_BASE_URL: 'http://localhost:8083', + CAMUNDA_MODELER_BASE_URL: 'http://localhost:8070/api', + CAMUNDA_TENANT_ID: '', // We can override values in the env by passing an empty string value CAMUNDA_SECURE_CONNECTION: false }) ``` @@ -165,7 +167,8 @@ const c8 = new Camunda8({ Here is a complete configuration example for connection to Camunda SaaS: ```bash -export ZEEBE_ADDRESS='5c34c0a7-7f29-4424-8414-125615f7a9b9.syd-1.zeebe.camunda.io:443' +export ZEEBE_GRPC_ADDRESS='5c34c0a7-7f29-4424-8414-125615f7a9b9.syd-1.zeebe.camunda.io:443' +export ZEEBE_REST_ADDRESS='https://yd-1.zeebe.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9' export ZEEBE_CLIENT_ID='yvvURO9TmBnP3zx4Xd8Ho6apgeiZTjn6' export ZEEBE_CLIENT_SECRET='iJJu-SHgUtuJTTAMnMLdcb8WGF8s2mHfXhXutEwe8eSbLXn98vUpoxtuLk5uG0en' # export CAMUNDA_CREDENTIALS_SCOPES='Zeebe,Tasklist,Operate,Optimize' # What APIs these client creds are authorised for diff --git a/smoke-test/smoke-test.js b/smoke-test/smoke-test.js index 5f574706..e6d65084 100644 --- a/smoke-test/smoke-test.js +++ b/smoke-test/smoke-test.js @@ -6,7 +6,8 @@ console.log('Running smoke test...') const camunda = new Camunda8({ CAMUNDA_OAUTH_DISABLED: true, - ZEEBE_ADDRESS: 'localhost:26500', + ZEEBE_ADDRESS: 'localhost:26500', // to be deprecated + ZEEBE_GRPC_ADDRESS: 'localhost:26500', ZEEBE_CLIENT_ID: 'zeebeClientId', ZEEBE_CLIENT_SECRET: 'zeebeClientSecret', CAMUNDA_CONSOLE_BASE_URL: 'http://localhost:8080', diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index 0b8bcce6..14d42c19 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -20,6 +20,17 @@ const getMainEnv = () => optional: true, default: 1000, }, + /** The address for the Zeebe GRPC. */ + ZEEBE_GRPC_ADDRESS: { + type: 'string', + optional: true, + }, + /** The address for the Zeebe REST API. Defaults to localhost:8080 */ + ZEEBE_REST_ADDRESS: { + type: 'string', + optional: true, + default: 'http://localhost:8080', + }, /** The address for the Zeebe Gateway. Defaults to localhost:26500 */ ZEEBE_ADDRESS: { type: 'string', @@ -364,6 +375,8 @@ export const CamundaEnvironmentVariableDictionary = 'CAMUNDA_TOKEN_SCOPE', 'CAMUNDA_ZEEBE_OAUTH_AUDIENCE', 'GRPC_KEEPALIVE_TIME_MS', + 'ZEEBE_REST_ADDRESS', + 'ZEEBE_GRPC_ADDRESS', 'ZEEBE_ADDRESS', 'ZEEBE_CLIENT_ID', 'ZEEBE_CLIENT_SECRET', diff --git a/src/zeebe/zb/ZeebeGrpcClient.ts b/src/zeebe/zb/ZeebeGrpcClient.ts index 72ae63ff..68ffab87 100644 --- a/src/zeebe/zb/ZeebeGrpcClient.ts +++ b/src/zeebe/zb/ZeebeGrpcClient.ts @@ -131,8 +131,8 @@ export class ZeebeGrpcClient extends TypedEmitter< this.tenantId = this.options.tenantId this.gatewayAddress = RequireConfiguration( - config.ZEEBE_ADDRESS, - 'ZEEBE_ADDRESS' + config.ZEEBE_ADDRESS || config.ZEEBE_GRPC_ADDRESS, + 'ZEEBE_GRPC_ADDRESS' ) debug('Gateway address: ', this.gatewayAddress) From c86e550747f23205dac9fe199a38217b3a583f76 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 22 May 2024 19:23:52 +1200 Subject: [PATCH 06/10] fix(oauth): correctly expire cached token (#164) fixes #163 --- package-lock.json | 118 ++- package.json | 3 +- src/__tests__/admin/admin.integration.spec.ts | 5 +- .../oauth/OAuthProvider.unit.spec.ts | 944 +++++++++--------- src/oauth/lib/OAuthProvider.ts | 16 +- 5 files changed, 608 insertions(+), 478 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ed63135..da11da13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,13 +31,13 @@ "devDependencies": { "@commitlint/cli": "^18.4.3", "@commitlint/config-conventional": "^18.4.3", - "@mokuteki/jwt": "^1.0.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@sitapati/testcontainers": "^2.8.1", "@types/debug": "^4.1.12", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.6", "@types/lodash.mergewith": "^4.6.9", "@types/node": "^20.9.4", "@types/node-fetch": "^2.6.11", @@ -56,6 +56,7 @@ "grpc-tools": "^1.12.4", "husky": "^8.0.3", "jest": "^29.7.0", + "jsonwebtoken": "^9.0.2", "lint-staged": "^15.2.0", "prettier": "^3.1.1", "semantic-release": "^22.0.12", @@ -2209,12 +2210,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@mokuteki/jwt": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mokuteki/jwt/-/jwt-1.0.2.tgz", - "integrity": "sha512-KMMPes910EQGQWdj4DeKr683VTzHRgbar17xydzAIr2bYsDp9hln32hq9f/ddFF9ZNmnWfyozjaURBeshxhP7w==", - "dev": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -3732,6 +3727,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "license": "MIT", @@ -4897,6 +4901,12 @@ "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", "dev": true }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, "node_modules/buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", @@ -6471,6 +6481,15 @@ "readable-stream": "^2.0.2" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -10770,6 +10789,61 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -11141,11 +11215,35 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, "node_modules/lodash.isfunction": { "version": "3.0.9", "dev": true, "license": "MIT" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "dev": true, @@ -11182,6 +11280,12 @@ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", diff --git a/package.json b/package.json index 89d129d2..18a8a49e 100644 --- a/package.json +++ b/package.json @@ -93,13 +93,13 @@ "devDependencies": { "@commitlint/cli": "^18.4.3", "@commitlint/config-conventional": "^18.4.3", - "@mokuteki/jwt": "^1.0.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@sitapati/testcontainers": "^2.8.1", "@types/debug": "^4.1.12", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.6", "@types/lodash.mergewith": "^4.6.9", "@types/node": "^20.9.4", "@types/node-fetch": "^2.6.11", @@ -118,6 +118,7 @@ "grpc-tools": "^1.12.4", "husky": "^8.0.3", "jest": "^29.7.0", + "jsonwebtoken": "^9.0.2", "lint-staged": "^15.2.0", "prettier": "^3.1.1", "semantic-release": "^22.0.12", diff --git a/src/__tests__/admin/admin.integration.spec.ts b/src/__tests__/admin/admin.integration.spec.ts index cb5d9260..2072e04d 100644 --- a/src/__tests__/admin/admin.integration.spec.ts +++ b/src/__tests__/admin/admin.integration.spec.ts @@ -18,13 +18,14 @@ test('createClient', async () => { const c = new AdminApiClient() const clusters = await c.getClusters() const clusterUuid = clusters[0].uuid - c.getClient(clusterUuid, 'testors') + const clientName = 'test_generated-delete-me' + c.getClient(clusterUuid, clientName) .then((res) => c.deleteClient(clusterUuid, res.ZEEBE_CLIENT_ID)) .catch((e) => e) const res = await c.createClient({ clusterUuid, - clientName: 'testors', + clientName, permissions: ['Zeebe'], }) const client = await c.getClient(clusterUuid, res.clientId) diff --git a/src/__tests__/oauth/OAuthProvider.unit.spec.ts b/src/__tests__/oauth/OAuthProvider.unit.spec.ts index 798167fb..20625e3b 100644 --- a/src/__tests__/oauth/OAuthProvider.unit.spec.ts +++ b/src/__tests__/oauth/OAuthProvider.unit.spec.ts @@ -4,21 +4,11 @@ import http from 'http' import os from 'os' import path from 'path' -import { HS256Strategy, JSONWebToken } from '@mokuteki/jwt' +import jwt from 'jsonwebtoken' import { EnvironmentSetup } from '../../lib' import { OAuthProvider } from '../../oauth' -const strategy = new HS256Strategy({ - ttl: 30000, - secret: 'YOUR_SECRET', -}) - -const jwt = new JSONWebToken(strategy) -const payload = { id: 1 } - -const access_token = jwt.generate(payload) - jest.setTimeout(10000) let server: http.Server @@ -42,523 +32,545 @@ function removeCacheDir(dirpath: string) { } } -test('Throws in the constructor if there in no clientId credentials', () => { - let thrown = false - let message = '' - try { - new OAuthProvider({ - config: { CAMUNDA_OAUTH_URL: 'url' }, - }) - } catch (e) { - thrown = true - message = (e as Error).message - } - expect(thrown).toBe(true) - expect( - message.includes('ZEEBE_CLIENT_ID') && - message.includes('CAMUNDA_CONSOLE_CLIENT_ID') - ).toBe(true) -}) +describe('OAuthProvider', () => { + it('Throws in the constructor if there in no clientId credentials', () => { + let thrown = false + let message = '' + try { + new OAuthProvider({ + config: { CAMUNDA_OAUTH_URL: 'url' }, + }) + } catch (e) { + thrown = true + message = (e as Error).message + } + expect(thrown).toBe(true) + expect( + message.includes('ZEEBE_CLIENT_ID') && + message.includes('CAMUNDA_CONSOLE_CLIENT_ID') + ).toBe(true) + }) -test('Throws in the constructor if there in no clientSecret credentials', () => { - let thrown = false - let message = '' - try { - new OAuthProvider({ - config: { - CAMUNDA_CONSOLE_CLIENT_ID: 'clientId1', - CAMUNDA_OAUTH_URL: 'url', - }, - }) - } catch (e) { - thrown = true - message = (e as Error).message - } - expect(thrown).toBe(true) - expect( - message.includes('ZEEBE_CLIENT_SECRET') && - message.includes('CAMUNDA_CONSOLE_CLIENT_SECRET') - ).toBe(true) -}) + it('Throws in the constructor if there in no clientSecret credentials', () => { + let thrown = false + let message = '' + try { + new OAuthProvider({ + config: { + CAMUNDA_CONSOLE_CLIENT_ID: 'clientId1', + CAMUNDA_OAUTH_URL: 'url', + }, + }) + } catch (e) { + thrown = true + message = (e as Error).message + } + expect(thrown).toBe(true) + expect( + message.includes('ZEEBE_CLIENT_SECRET') && + message.includes('CAMUNDA_CONSOLE_CLIENT_SECRET') + ).toBe(true) + }) + + it('Throws in the constructor if there are insufficient credentials', () => { + let thrown = false + let message = '' + try { + new OAuthProvider({ + config: { + CAMUNDA_CONSOLE_CLIENT_ID: 'clientId2', + ZEEBE_CLIENT_SECRET: 'zeebe-secret', + CAMUNDA_OAUTH_URL: 'url', + }, + }) + } catch (e) { + thrown = true + message = (e as Error).message + } + expect(thrown).toBe(true) + expect( + message.includes('client ID') && message.includes('client secret') + ).toBe(true) + }) -test('Throws in the constructor if there are insufficient credentials', () => { - let thrown = false - let message = '' - try { - new OAuthProvider({ + it('Gets the token cache dir from the environment', () => { + const tokenCacheDir = path.join(__dirname, '.token-cache') + removeCacheDir(tokenCacheDir) + expect(fs.existsSync(tokenCacheDir)).toBe(false) + process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCacheDir + + const o = new OAuthProvider({ config: { - CAMUNDA_CONSOLE_CLIENT_ID: 'clientId2', - ZEEBE_CLIENT_SECRET: 'zeebe-secret', + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId3', + ZEEBE_CLIENT_SECRET: 'clientSecret', CAMUNDA_OAUTH_URL: 'url', }, }) - } catch (e) { - thrown = true - message = (e as Error).message - } - expect(thrown).toBe(true) - expect( - message.includes('client ID') && message.includes('client secret') - ).toBe(true) -}) + expect(o).toBeTruthy() + const exists = fs.existsSync(tokenCacheDir) + expect(exists).toBe(true) + removeCacheDir(tokenCacheDir) -test('Gets the token cache dir from the environment', () => { - const tokenCacheDir = path.join(__dirname, '.token-cache') - removeCacheDir(tokenCacheDir) - expect(fs.existsSync(tokenCacheDir)).toBe(false) - process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCacheDir - - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId3', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'url', - }, + expect(fs.existsSync(tokenCacheDir)).toBe(false) }) - expect(o).toBeTruthy() - const exists = fs.existsSync(tokenCacheDir) - expect(exists).toBe(true) - removeCacheDir(tokenCacheDir) - expect(fs.existsSync(tokenCacheDir)).toBe(false) -}) + it('Creates the token cache dir if it does not exist', () => { + const tokenCacheDir = path.join(__dirname, '.token-cache') + process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCacheDir + removeCacheDir(tokenCacheDir) + + expect(fs.existsSync(tokenCacheDir)).toBe(false) -test('Creates the token cache dir if it does not exist', () => { - const tokenCacheDir = path.join(__dirname, '.token-cache') - process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCacheDir - removeCacheDir(tokenCacheDir) + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId4', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: 'url', + }, + }) - expect(fs.existsSync(tokenCacheDir)).toBe(false) + expect(o).toBeTruthy() + expect(fs.existsSync(tokenCacheDir)).toBe(true) + removeCacheDir(tokenCacheDir) - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId4', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'url', - }, + expect(fs.existsSync(tokenCacheDir)).toBe(false) }) - expect(o).toBeTruthy() - expect(fs.existsSync(tokenCacheDir)).toBe(true) - removeCacheDir(tokenCacheDir) - - expect(fs.existsSync(tokenCacheDir)).toBe(false) -}) + it('Throws in the constructor if the token cache is not writable', () => { + const tokenCacheDir = path.join(__dirname, '.token-cache') + process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCacheDir + removeCacheDir(tokenCacheDir) + + expect(fs.existsSync(tokenCacheDir)).toBe(false) + if (os.platform() === 'win32') { + fs.mkdirSync(tokenCacheDir) + expect(fs.existsSync(tokenCacheDir)).toBe(true) + // Make the directory read-only on Windows + // Note that this requires administrative privileges + execSync( + `icacls ${tokenCacheDir} /deny Everyone:(OI)(CI)W /inheritance:r` + ) + } else { + // Make the directory read-only on Unix + fs.mkdirSync(tokenCacheDir, 0o400) + expect(fs.existsSync(tokenCacheDir)).toBe(true) + } -test('Throws in the constructor if the token cache is not writable', () => { - const tokenCacheDir = path.join(__dirname, '.token-cache') - process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCacheDir - removeCacheDir(tokenCacheDir) + let thrown = false + try { + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId5', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: 'url', + }, + }) + expect(o).toBeTruthy() + } catch { + thrown = true + } - expect(fs.existsSync(tokenCacheDir)).toBe(false) - if (os.platform() === 'win32') { - fs.mkdirSync(tokenCacheDir) - expect(fs.existsSync(tokenCacheDir)).toBe(true) - // Make the directory read-only on Windows + // Make the directory writeable on Windows, so it can be deleted // Note that this requires administrative privileges - execSync(`icacls ${tokenCacheDir} /deny Everyone:(OI)(CI)W /inheritance:r`) - } else { - // Make the directory read-only on Unix - fs.mkdirSync(tokenCacheDir, 0o400) - expect(fs.existsSync(tokenCacheDir)).toBe(true) - } + if (os.platform() === 'win32') { + execSync(`icacls ${tokenCacheDir} /grant Everyone:(OI)(CI)(F)`) + } + removeCacheDir(tokenCacheDir) + expect(thrown).toBe(true) + expect(fs.existsSync(tokenCacheDir)).toBe(false) + }) + + // Added test for https://github.com/camunda/camunda-saas-oauth-nodejs/issues/8 + // "Can not renew expired token" + // Updated test for https://github.com/camunda/camunda-8-js-sdk/issues/3 + // "Remove expiry timer from oAuth token implementation" + it('In-memory cache is populated and evicted after expiry', (done) => { + const delay = (timeout: number) => + new Promise((res) => setTimeout(() => res(null), timeout)) + const serverPort3002 = 3002 - let thrown = false - try { const o = new OAuthProvider({ config: { CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId5', + ZEEBE_CLIENT_ID: 'clientId6', ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'url', + CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3002}`, + CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true, + CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS: 0, }, }) - expect(o).toBeTruthy() - } catch { - thrown = true - } - - // Make the directory writeable on Windows, so it can be deleted - // Note that this requires administrative privileges - if (os.platform() === 'win32') { - execSync(`icacls ${tokenCacheDir} /grant Everyone:(OI)(CI)(F)`) - } - removeCacheDir(tokenCacheDir) - expect(thrown).toBe(true) - expect(fs.existsSync(tokenCacheDir)).toBe(false) -}) - -// Added test for https://github.com/camunda/camunda-saas-oauth-nodejs/issues/8 -// "Can not renew expired token" -// Updated test for https://github.com/camunda/camunda-8-js-sdk/issues/3 -// "Remove expiry timer from oAuth token implementation" -test('In-memory cache is populated and evicted after timeout', (done) => { - const delay = (timeout: number) => - new Promise((res) => setTimeout(() => res(null), timeout)) - const serverPort3002 = 3002 - - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId6', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3002}`, - CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true, - CAMUNDA_OAUTH_TOKEN_REFRESH_THRESHOLD_MS: 0, - }, - }) - const strategy = new HS256Strategy({ - ttl: 2000, - secret: 'YOUR_SECRET', + const secret = 'YOUR_SECRET' + const ttl = 2 // 2 seconds + const payload = { id: 1 } + const access_token = jwt.sign(payload, secret, { expiresIn: ttl }) + + // On subsequent requests, we will return a different token + let requested = false + + server = http + .createServer((req, res) => { + if (req.method === 'POST') { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + + req.on('end', () => { + console.log(body) + res.writeHead(200, { 'Content-Type': 'application/json' }) + const expiresIn = 2 // seconds + const token = requested + ? jwt.sign(payload, secret, { expiresIn: ttl }) + : access_token + requested = true + res.end(`{"access_token": "${token}", "expires_in": ${expiresIn}}`) + }) + } + }) + .listen(serverPort3002) + + o.getToken('ZEEBE').then(async (token) => { + const token1 = token + expect(token).toBe(token1) + await delay(500) + const token2 = await o.getToken('ZEEBE') + expect(token2).toBe(token1) + await delay(1600) + const token3 = await o.getToken('ZEEBE') + expect(token3).not.toBe(token1) + done() + }) }) - const jwt = new JSONWebToken(strategy) - const payload = { id: 1 } - - const access_token = jwt.generate(payload) - - let requestCount = 0 - server = http - .createServer((req, res) => { - if (req.method === 'POST') { - let body = '' - req.on('data', (chunk) => { - body += chunk - }) - - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - const expiresIn = 2 // seconds - const token = `${access_token}${requestCount}` - res.end(`{"access_token": "${token}", "expires_in": ${expiresIn}}`) - requestCount++ - expect(body).toEqual( - 'audience=token&client_id=clientId6&client_secret=clientSecret&grant_type=client_credentials' - ) - }) - } + it('Uses form encoding for request', (done) => { + const serverPort3001 = 3001 + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId8', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3001}`, + }, }) - .listen(serverPort3002) - - o.getToken('ZEEBE').then(async (token) => { - expect(token).toBe(`${access_token}0`) - await delay(500) - const token2 = await o.getToken('ZEEBE') - expect(token2).toBe(`${access_token}0`) - await delay(1600) - const token3 = await o.getToken('ZEEBE') - expect(token3).toBe(`${access_token}1`) - done() + const secret = 'YOUR_SECRET' + const ttl = 2 // 2 seconds + const payload = { id: 1 } + const access_token = jwt.sign(payload, secret, { expiresIn: ttl }) + server = http + .createServer((req, res) => { + if (req.method === 'POST') { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(`{"access_token": "${access_token}", "expires_in": "5"}`) + server.close() + expect(body).toEqual( + 'audience=operate.camunda.io&client_id=clientId8&client_secret=clientSecret&grant_type=client_credentials' + ) + done() + }) + } + }) + .listen(serverPort3001) + o.getToken('OPERATE') }) -}) -test('Uses form encoding for request', (done) => { - const serverPort3001 = 3001 - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId8', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3001}`, - }, - }) - server = http - .createServer((req, res) => { - if (req.method === 'POST') { - let body = '' - req.on('data', (chunk) => { - body += chunk - }) - - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(`{"access_token": "${access_token}", "expires_in": "5"}`) - server.close() - expect(body).toEqual( - 'audience=operate.camunda.io&client_id=clientId8&client_secret=clientSecret&grant_type=client_credentials' - ) - done() - }) - } + it('Uses a custom audience for an Operate token, if one is configured', (done) => { + const serverPort3003 = 3003 + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId9', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3003}`, + CAMUNDA_OPERATE_OAUTH_AUDIENCE: 'custom.operate.audience', + }, }) - .listen(serverPort3001) - o.getToken('OPERATE') -}) - -test('Uses a custom audience for an Operate token, if one is configured', (done) => { - const serverPort3003 = 3003 - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId9', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3003}`, - CAMUNDA_OPERATE_OAUTH_AUDIENCE: 'custom.operate.audience', - }, + const secret = 'YOUR_SECRET' + const ttl = 2 // 2 seconds + const payload = { id: 1 } + const access_token = jwt.sign(payload, secret, { expiresIn: ttl }) + server = http + .createServer((req, res) => { + if (req.method === 'POST') { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(`{"access_token": "${access_token}", "expires_in": "5"}`) + server.close() + expect(body).toEqual( + 'audience=custom.operate.audience&client_id=clientId9&client_secret=clientSecret&grant_type=client_credentials' + ) + done() + }) + } + }) + .listen(serverPort3003) + o.getToken('OPERATE') }) - server = http - .createServer((req, res) => { - if (req.method === 'POST') { - let body = '' - req.on('data', (chunk) => { - body += chunk - }) - - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(`{"access_token": "${access_token}", "expires_in": "5"}`) - server.close() - expect(body).toEqual( - 'audience=custom.operate.audience&client_id=clientId9&client_secret=clientSecret&grant_type=client_credentials' - ) - done() - }) - } - }) - .listen(serverPort3003) - o.getToken('OPERATE') -}) -test('Passes scope, if provided', () => { - const serverPort3004 = 3004 - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - CAMUNDA_TOKEN_SCOPE: 'scope', - ZEEBE_CLIENT_ID: 'clientId10', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3004}`, - }, - }) - server = http - .createServer((req, res) => { - if (req.method === 'POST') { - let body = '' - req.on('data', (chunk) => { - body += chunk - }) - - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(`{"access_token": "${access_token}", "expires_in": "5"}`) - - expect(body).toEqual( - 'audience=token&client_id=clientId10&client_secret=clientSecret&grant_type=client_credentials&scope=scope' - ) - }) - } + it('Passes scope, if provided', () => { + const serverPort3004 = 3004 + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + CAMUNDA_TOKEN_SCOPE: 'scope', + ZEEBE_CLIENT_ID: 'clientId10', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3004}`, + }, }) - .listen(serverPort3004) - - return o.getToken('ZEEBE') -}) - -test('Can get scope from environment', () => { - const serverPort3005 = 3005 - process.env.CAMUNDA_TOKEN_SCOPE = 'scope2' - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId11', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3005}`, - }, + const secret = 'YOUR_SECRET' + const ttl = 5 // 5 seconds + const payload = { id: 1 } + const access_token = jwt.sign(payload, secret, { expiresIn: ttl }) + server = http + .createServer((req, res) => { + if (req.method === 'POST') { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(`{"access_token": "${access_token}", "expires_in": "5"}`) + + expect(body).toEqual( + 'audience=token&client_id=clientId10&client_secret=clientSecret&grant_type=client_credentials&scope=scope' + ) + }) + } + }) + .listen(serverPort3004) + + return o.getToken('ZEEBE') }) - server = http - .createServer((req, res) => { - if (req.method === 'POST') { - let body = '' - req.on('data', (chunk) => { - body += chunk - }) - - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(`{"access_token": "${access_token}", "expires_in": "5"}`) - - expect(body).toEqual( - 'audience=token&client_id=clientId11&client_secret=clientSecret&grant_type=client_credentials&scope=scope2' - ) - }) - } - }) - .listen(serverPort3005) - return o.getToken('ZEEBE') -}) - -test('Creates the token cache dir if it does not exist', () => { - const tokenCache = path.join(__dirname, '.token-cache') - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) - - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - CAMUNDA_TOKEN_CACHE_DIR: tokenCache, - ZEEBE_CLIENT_ID: 'clientId12', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'url', - }, + it('Can get scope from environment', () => { + const serverPort3005 = 3005 + process.env.CAMUNDA_TOKEN_SCOPE = 'scope2' + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId11', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3005}`, + }, + }) + const secret = 'YOUR_SECRET' + const ttl = 5 // 5 seconds + const payload = { id: 1 } + const access_token = jwt.sign(payload, secret, { expiresIn: ttl }) + server = http + .createServer((req, res) => { + if (req.method === 'POST') { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(`{"access_token": "${access_token}", "expires_in": "5"}`) + + expect(body).toEqual( + 'audience=token&client_id=clientId11&client_secret=clientSecret&grant_type=client_credentials&scope=scope2' + ) + }) + } + }) + .listen(serverPort3005) + + return o.getToken('ZEEBE') }) - expect(o).toBeTruthy() - expect(fs.existsSync(tokenCache)).toBe(true) - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) -}) - -test('Gets the token cache dir from the environment', () => { - const tokenCache = path.join(__dirname, '.token-cache') - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) - process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCache - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId13', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'url', - }, - }) + it('Creates the token cache dir if it does not exist', () => { + const tokenCache = path.join(__dirname, '.token-cache') + if (fs.existsSync(tokenCache)) { + fs.rmdirSync(tokenCache) + } + expect(fs.existsSync(tokenCache)).toBe(false) - expect(o).toBeTruthy() - expect(fs.existsSync(tokenCache)).toBe(true) - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) -}) + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + CAMUNDA_TOKEN_CACHE_DIR: tokenCache, + ZEEBE_CLIENT_ID: 'clientId12', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: 'url', + }, + }) -test('Uses an explicit token cache over the environment', () => { - const tokenCache1 = path.join(__dirname, '.token-cache1') - const tokenCache2 = path.join(__dirname, '.token-cache2') - ;[tokenCache1, tokenCache2].forEach((tokenCache) => { + expect(o).toBeTruthy() + expect(fs.existsSync(tokenCache)).toBe(true) if (fs.existsSync(tokenCache)) { fs.rmdirSync(tokenCache) } expect(fs.existsSync(tokenCache)).toBe(false) }) - process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCache1 - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - CAMUNDA_TOKEN_CACHE_DIR: tokenCache2, - ZEEBE_CLIENT_ID: 'clientId14', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'url', - }, - }) - expect(o).toBeTruthy() - expect(fs.existsSync(tokenCache2)).toBe(true) - expect(fs.existsSync(tokenCache1)).toBe(false) - ;[tokenCache1, tokenCache2].forEach((tokenCache) => { + it('Gets the token cache dir from the environment', () => { + const tokenCache = path.join(__dirname, '.token-cache') if (fs.existsSync(tokenCache)) { fs.rmdirSync(tokenCache) } expect(fs.existsSync(tokenCache)).toBe(false) - }) -}) + process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCache + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId13', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: 'url', + }, + }) -test('Can set a custom user agent', () => { - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId16', - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'http://127.0.0.1:3005', - CAMUNDA_CUSTOM_USER_AGENT_STRING: 'modeler', - }, + expect(o).toBeTruthy() + expect(fs.existsSync(tokenCache)).toBe(true) + if (fs.existsSync(tokenCache)) { + fs.rmdirSync(tokenCache) + } + expect(fs.existsSync(tokenCache)).toBe(false) }) - expect(o.userAgentString.includes(' modeler')).toBe(true) -}) + it('Uses an explicit token cache over the environment', () => { + const tokenCache1 = path.join(__dirname, '.token-cache1') + const tokenCache2 = path.join(__dirname, '.token-cache2') + ;[tokenCache1, tokenCache2].forEach((tokenCache) => { + if (fs.existsSync(tokenCache)) { + fs.rmdirSync(tokenCache) + } + expect(fs.existsSync(tokenCache)).toBe(false) + }) + process.env.CAMUNDA_TOKEN_CACHE_DIR = tokenCache1 + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + CAMUNDA_TOKEN_CACHE_DIR: tokenCache2, + ZEEBE_CLIENT_ID: 'clientId14', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: 'url', + }, + }) -// See: https://github.com/camunda/camunda-8-js-sdk/issues/60 -test('Passes no audience for Modeler API when self-hosted', (done) => { - const serverPort3006 = 3006 - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId17', - CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true, - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3006}`, - }, - }) - server = http - .createServer((req, res) => { - if (req.method === 'POST') { - let body = '' - req.on('data', (chunk) => { - body += chunk - }) - - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(`{"access_token": "${access_token}"}`) - expect(body).toEqual( - 'client_id=clientId17&client_secret=clientSecret&grant_type=client_credentials' - ) - done() - }) + expect(o).toBeTruthy() + expect(fs.existsSync(tokenCache2)).toBe(true) + expect(fs.existsSync(tokenCache1)).toBe(false) + ;[tokenCache1, tokenCache2].forEach((tokenCache) => { + if (fs.existsSync(tokenCache)) { + fs.rmdirSync(tokenCache) } + expect(fs.existsSync(tokenCache)).toBe(false) + }) + }) + + it('Can set a custom user agent', () => { + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId16', + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: 'http://127.0.0.1:3005', + CAMUNDA_CUSTOM_USER_AGENT_STRING: 'modeler', + }, }) - .listen(serverPort3006) - o.getToken('MODELER') -}) -// See: https://github.com/camunda/camunda-8-js-sdk/issues/60 -test('Throws if you try to get a Modeler token from SaaS without console creds', async () => { - let thrown = false - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - ZEEBE_CLIENT_ID: 'clientId18', - CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true, - ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'https://login.cloud.camunda.io/oauth/token', - }, + expect(o.userAgentString.includes(' modeler')).toBe(true) }) - await o - .getToken('MODELER') - .catch(() => { - thrown = true + // See: https://github.com/camunda/camunda-8-js-sdk/issues/60 + it('Passes no audience for Modeler API when self-hosted', (done) => { + const serverPort3006 = 3006 + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId17', + CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true, + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3006}`, + }, }) - .then(() => { - expect(thrown).toBe(true) + const secret = 'YOUR_SECRET' + const ttl = 5 // 5 seconds + const payload = { id: 1 } + const access_token = jwt.sign(payload, secret, { expiresIn: ttl }) + server = http + .createServer((req, res) => { + if (req.method === 'POST') { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(`{"access_token": "${access_token}"}`) + expect(body).toEqual( + 'client_id=clientId17&client_secret=clientSecret&grant_type=client_credentials' + ) + done() + }) + } + }) + .listen(serverPort3006) + o.getToken('MODELER') + }) + + // See: https://github.com/camunda/camunda-8-js-sdk/issues/60 + it('Throws if you try to get a Modeler token from SaaS without console creds', async () => { + let thrown = false + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + ZEEBE_CLIENT_ID: 'clientId18', + CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true, + ZEEBE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: 'https://login.cloud.camunda.io/oauth/token', + }, }) -}) -// See: https://github.com/camunda/camunda-8-js-sdk/issues/60 -test('Throws if you try to get a Modeler token from Self-hosted without application creds', async () => { - let thrown = false - const o = new OAuthProvider({ - config: { - CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', - CAMUNDA_CONSOLE_CLIENT_ID: 'clientId19', - CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true, - CAMUNDA_CONSOLE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: 'https://localhost', - }, + await o + .getToken('MODELER') + .catch(() => { + thrown = true + }) + .then(() => { + expect(thrown).toBe(true) + }) }) - await o - .getToken('MODELER') - .catch(() => { - thrown = true - }) - .then(() => { - expect(thrown).toBe(true) + + // See: https://github.com/camunda/camunda-8-js-sdk/issues/60 + it('Throws if you try to get a Modeler token from Self-hosted without application creds', async () => { + let thrown = false + const o = new OAuthProvider({ + config: { + CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', + CAMUNDA_CONSOLE_CLIENT_ID: 'clientId19', + CAMUNDA_TOKEN_DISK_CACHE_DISABLE: true, + CAMUNDA_CONSOLE_CLIENT_SECRET: 'clientSecret', + CAMUNDA_OAUTH_URL: 'https://localhost', + }, }) + await o + .getToken('MODELER') + .catch(() => { + thrown = true + }) + .then(() => { + expect(thrown).toBe(true) + }) + }) }) diff --git a/src/oauth/lib/OAuthProvider.ts b/src/oauth/lib/OAuthProvider.ts index ee7db305..e180bf64 100644 --- a/src/oauth/lib/OAuthProvider.ts +++ b/src/oauth/lib/OAuthProvider.ts @@ -353,7 +353,8 @@ export class OAuthProvider implements IOAuthProvider { const key = this.getCacheKey(audience) try { const decoded = jwtDecode(token.access_token) - + trace(`Caching token: ${JSON.stringify(decoded, null, 2)}`) + trace(`Caching token for ${audience} in memory. Expiry: ${decoded.exp}`) token.expiry = decoded.exp ?? 0 this.tokenCache[key] = token } catch (e) { @@ -371,9 +372,11 @@ export class OAuthProvider implements IOAuthProvider { const tokenFileName = this.getCachedTokenFileName(clientId, audience) const tokenCachedInFile = fs.existsSync(tokenFileName) if (!tokenCachedInFile) { + trace(`No file cached token for ${audience} found`) return null } try { + trace(`Reading file cached token for ${audience}`) token = JSON.parse( fs.readFileSync(this.getCachedTokenFileName(clientId, audience), 'utf8') ) @@ -422,10 +425,19 @@ export class OAuthProvider implements IOAuthProvider { private isExpired(token: Token) { const d = new Date() const currentTime = d.setSeconds(d.getSeconds()) + + // token.expiry is seconds since Unix Epoch + // The Date constructor expects milliseconds since Unix Epoch + const tokenExpiryMs = token.expiry * 1000 + + trace(`Checking token expiry for ${token.audience}`) + trace(` Current time: ${currentTime}`) + trace(` Token expiry: ${tokenExpiryMs}`) + // If the token has 10 seconds (by default) or less left, renew it. // The Identity server token cache is cleared 30 seconds before the token expires, allowing us to renew it // See: https://github.com/camunda/camunda-8-js-sdk/issues/125 - const tokenIsExpired = currentTime >= token.expiry - this.refreshWindow + const tokenIsExpired = currentTime >= tokenExpiryMs - this.refreshWindow return tokenIsExpired } From 022607bf77077fdacffdce7f26ce580360d54bf3 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 22 May 2024 21:51:37 +1200 Subject: [PATCH 07/10] feat(zeebe): support Zeebe User Task REST API * feat(zeebe): support Zeebe User Task REST API Implement ZeebeRESTClient fixes #34 * docs(zeebe): document ZeebeRestClient * refactor(repo): rename docker-compose files to *.yaml * test(zeebe): fix Topology Rest Client test * ci(repo): fix GitHub workflow docker-compose * ci(repo): rename docker-compose.yaml * ci(repo): enable Zeebe REST tests in CI * refactor: improve error messages * test(zeebe): improve test structure * refactor(zeebe): add headers to REST requests * fix(zeebe): request correct token for Zeebe * ci(repo): run windows tests on windows 2016 * ci(repo): use cross-platform regex for unit test * ci(repo): use windows-2019 runner for tests * ci(repo): disable windows tests --- .github/workflows/commitlint.yml | 2 +- .github/workflows/pr-test.yml | 122 ++++++------ .github/workflows/publish.yml | 97 +++++----- .github/workflows/unit.yml | 38 ++-- README.md | 34 ++-- ...y.yml => docker-compose-multitenancy.yaml} | 1 + ...docker-compose.yml => docker-compose.yaml} | 1 + package.json | 10 +- smoke-test/smoke-test.js | 4 + .../zeebe/integration-rest/Topology.spec.ts | 9 + src/c8/index.ts | 14 +- src/lib/GotErrors.ts | 16 +- src/lib/GotHooks.ts | 8 +- src/oauth/lib/OAuthProvider.ts | 6 +- src/zeebe/index.ts | 1 + src/zeebe/zb/ZeebeRESTClient.ts | 176 ++++++++++++++++++ 16 files changed, 388 insertions(+), 151 deletions(-) rename docker/{docker-compose-multitenancy.yml => docker-compose-multitenancy.yaml} (99%) rename docker/{docker-compose.yml => docker-compose.yaml} (99%) create mode 100644 src/__tests__/zeebe/integration-rest/Topology.spec.ts create mode 100644 src/zeebe/zb/ZeebeRESTClient.ts diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index c888d052..2d9f2b56 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -13,7 +13,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 552ccbc0..20f9084d 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -12,7 +12,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install @@ -24,26 +24,26 @@ jobs: run: | npm run test - unit-tests-windows: - runs-on: windows-latest - steps: - - name: Check out the repo - uses: actions/checkout@v4 + # unit-tests-windows: + # runs-on: windows-2019 + # 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: Use Node.js + # uses: actions/setup-node@v4 + # with: + # node-version: "20" # Specify a Node.js version - - name: Install dependencies - run: npm install + # - name: Install dependencies + # run: npm install - - name: Build - run: npm run build + # - name: Build + # run: npm run build - - name: Run Unit Tests - run: | - npm run test + # - name: Run Unit Tests + # run: | + # npm run test local_integration: runs-on: ubuntu-latest @@ -56,7 +56,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install @@ -71,7 +71,7 @@ jobs: - name: Set up Docker Compose run: | - docker-compose -f docker/docker-compose.yml -f docker/docker-compose-modeler.yaml up -d + docker-compose -f docker/docker-compose.yaml -f docker/docker-compose-modeler.yaml up -d - name: Run Integration Tests run: | @@ -89,7 +89,7 @@ jobs: - name: Cleanup if: always() - run: docker-compose -f docker/docker-compose.yml -f docker/docker-compose-modeler.yaml down + run: docker-compose -f docker/docker-compose.yaml -f docker/docker-compose-modeler.yaml down local_multitenancy_integration: runs-on: ubuntu-latest @@ -102,7 +102,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install @@ -117,7 +117,7 @@ jobs: - name: Set up Docker Compose run: | - docker-compose -f docker/docker-compose-multitenancy.yml -f docker/docker-compose-modeler.yaml up -d + docker-compose -f docker/docker-compose-multitenancy.yaml -f docker/docker-compose-modeler.yaml up -d - name: Run Integration Tests run: | @@ -137,7 +137,7 @@ jobs: - name: Cleanup if: always() - run: docker-compose -f docker/docker-compose-multitenancy.yml -f docker/docker-compose-modeler.yaml down + run: docker-compose -f docker/docker-compose-multitenancy.yaml -f docker/docker-compose-modeler.yaml down saas_integration: runs-on: ubuntu-latest @@ -149,7 +149,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install @@ -159,6 +159,8 @@ jobs: npm run test:integration env: ZEEBE_ADDRESS: ${{ secrets.ZEEBE_ADDRESS }} + ZEEBE_REST_ADDRESS: ${{ secrets.ZEEBE_REST_ADDRESS }} + ZEEBE_GRPC_ADDRESS: ${{ secrets.ZEEBE_GRPC_ADDRESS }} ZEEBE_CLIENT_ID: ${{ secrets.ZEEBE_CLIENT_ID }} ZEEBE_AUTHORIZATION_SERVER_URL: ${{ secrets.ZEEBE_AUTHORIZATION_SERVER_URL }} ZEEBE_CLIENT_SECRET: ${{ secrets.ZEEBE_CLIENT_SECRET }} @@ -174,38 +176,40 @@ 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}} + # saas_integration_windows: + # needs: saas_integration + # runs-on: windows-2019 + # environment: integration + # steps: + # - name: Check out the repo + # uses: actions/checkout@v4 + + # - name: Use Node.js + # uses: actions/setup-node@v4 + # with: + # node-version: "20" # 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_REST_ADDRESS: ${{ secrets.ZEEBE_REST_ADDRESS }} + # ZEEBE_GRPC_ADDRESS: ${{ secrets.ZEEBE_GRPC_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}} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a39ce237..02016bb3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,7 +16,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm i @@ -39,7 +39,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install @@ -54,7 +54,7 @@ jobs: - name: Set up Docker Compose run: | - docker-compose -f docker/docker-compose.yml -f docker/docker-compose-modeler.yaml up -d + docker-compose -f docker/docker-compose.yaml -f docker/docker-compose-modeler.yaml up -d - name: Run Integration Tests run: | @@ -72,7 +72,7 @@ jobs: - name: Cleanup if: always() - run: docker-compose -f docker/docker-compose.yml -f docker/docker-compose-modeler.yaml down + run: docker-compose -f docker/docker-compose.yaml -f docker/docker-compose-modeler.yaml down local_multitenancy_integration: runs-on: ubuntu-latest @@ -85,7 +85,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install @@ -100,7 +100,7 @@ jobs: - name: Set up Docker Compose run: | - docker-compose -f docker/docker-compose-multitenancy.yml -f docker/docker-compose-modeler.yaml up -d + docker-compose -f docker/docker-compose-multitenancy.yaml -f docker/docker-compose-modeler.yaml up -d - name: Run Integration Tests run: | @@ -120,7 +120,7 @@ jobs: - name: Cleanup if: always() - run: docker-compose -f docker/docker-compose-multitenancy.yml -f docker/docker-compose-modeler.yaml down + run: docker-compose -f docker/docker-compose-multitenancy.yaml -f docker/docker-compose-modeler.yaml down saas_integration: runs-on: ubuntu-latest @@ -132,7 +132,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install @@ -142,6 +142,8 @@ jobs: npm run test:integration env: ZEEBE_ADDRESS: ${{ secrets.ZEEBE_ADDRESS }} + ZEEBE_REST_ADDRESS: ${{ secrets.ZEEBE_REST_ADDRESS }} + ZEEBE_GRPC_ADDRESS: ${{ secrets.ZEEBE_GRPC_ADDRESS }} ZEEBE_CLIENT_ID: ${{ secrets.ZEEBE_CLIENT_ID }} ZEEBE_AUTHORIZATION_SERVER_URL: ${{ secrets.ZEEBE_AUTHORIZATION_SERVER_URL }} ZEEBE_CLIENT_SECRET: ${{ secrets.ZEEBE_CLIENT_SECRET }} @@ -157,46 +159,47 @@ 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}} + # saas_integration_windows: + # needs: saas_integration + # runs-on: windows-2019 + # environment: integration + # steps: + # - name: Check out the repo + # uses: actions/checkout@v4 + + # - name: Use Node.js + # uses: actions/setup-node@v4 + # with: + # node-version: "20" # 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_REST_ADDRESS: ${{ secrets.ZEEBE_REST_ADDRESS }} + # ZEEBE_GRPC_ADDRESS: ${{ secrets.ZEEBE_GRPC_ADDRESS }} + # 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: - [ + needs: [ saas_integration, - saas_integration_windows, + # saas_integration_windows, local_multitenancy_integration, local_integration, unit-tests, @@ -212,7 +215,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "18" + node-version: "20" cache: "npm" - name: Install run: npm i diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index f6dedcc9..000ce1e6 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -12,7 +12,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "18" # Specify a Node.js version + node-version: "20" # Specify a Node.js version - name: Install dependencies run: npm install @@ -23,26 +23,26 @@ jobs: - name: Run Unit Tests run: | npm run test - unit-tests-windows: - runs-on: windows-latest - steps: - - name: Check out the repo - uses: actions/checkout@v4 + # unit-tests-windows: + # runs-on: windows-2019 + # 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: Use Node.js + # uses: actions/setup-node@v4 + # with: + # node-version: "20" # Specify a Node.js version - - name: Install dependencies - run: npm install + # - name: Install dependencies + # run: npm install - - name: Build - run: npm run build + # - name: Build + # run: npm run build - - name: Build and Smoke Test - run: npm run test:smoketest + # - name: Build and Smoke Test + # run: npm run test:smoketest - - name: Run Unit Tests - run: | - npm run test + # - name: Run Unit Tests + # run: | + # npm run test diff --git a/README.md b/README.md index da6413ee..416f941f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ import { Camunda8 } from '@camunda8/sdk' const c8 = new Camunda8() const zeebe = c8.getZeebeGrpcApiClient() +const zeebeRest = c8.getZeebeRestClient() const operate = c8.getOperateApiClient() const optimize = c8.getOptimizeApiClient() const tasklist = c8.getTasklistApiClient() @@ -119,7 +120,7 @@ This is the complete environment configuration needed to run against the Dockeri ```bash # Self-Managed export ZEEBE_GRPC_ADDRESS='localhost:26500' -export ZEEBE_REST_ADDRESS='localhost:8080/v1/' +export ZEEBE_REST_ADDRESS='http://localhost:8080' export ZEEBE_CLIENT_ID='zeebe' export ZEEBE_CLIENT_SECRET='zecret' export CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' @@ -148,17 +149,18 @@ Here is an example of doing this via the constructor, rather than via the enviro import { Camunda8 } from '@camunda8/sdk' const c8 = new Camunda8({ - ZEEBE_GRPC_ADDRESS: 'localhost:26500', - ZEEBE_REST_ADDRESS: 'localhost:8080/v1/', - ZEEBE_CLIENT_ID: 'zeebe', - ZEEBE_CLIENT_SECRET: 'zecret', - CAMUNDA_OAUTH_URL: 'http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token', - CAMUNDA_TASKLIST_BASE_URL: 'http://localhost:8082', - CAMUNDA_OPERATE_BASE_URL: 'http://localhost:8081', - CAMUNDA_OPTIMIZE_BASE_URL: 'http://localhost:8083', - CAMUNDA_MODELER_BASE_URL: 'http://localhost:8070/api', - CAMUNDA_TENANT_ID: '', // We can override values in the env by passing an empty string value - CAMUNDA_SECURE_CONNECTION: false + ZEEBE_GRPC_ADDRESS: 'localhost:26500', + ZEEBE_REST_ADDRESS: 'http://localhost:8080', + ZEEBE_CLIENT_ID: 'zeebe', + ZEEBE_CLIENT_SECRET: 'zecret', + CAMUNDA_OAUTH_URL: + 'http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token', + CAMUNDA_TASKLIST_BASE_URL: 'http://localhost:8082', + CAMUNDA_OPERATE_BASE_URL: 'http://localhost:8081', + CAMUNDA_OPTIMIZE_BASE_URL: 'http://localhost:8083', + CAMUNDA_MODELER_BASE_URL: 'http://localhost:8070/api', + CAMUNDA_TENANT_ID: '', // We can override values in the env by passing an empty string value + CAMUNDA_SECURE_CONNECTION: false, }) ``` @@ -168,7 +170,7 @@ Here is a complete configuration example for connection to Camunda SaaS: ```bash export ZEEBE_GRPC_ADDRESS='5c34c0a7-7f29-4424-8414-125615f7a9b9.syd-1.zeebe.camunda.io:443' -export ZEEBE_REST_ADDRESS='https://yd-1.zeebe.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9' +export ZEEBE_REST_ADDRESS='https://syd-1.zeebe.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9' export ZEEBE_CLIENT_ID='yvvURO9TmBnP3zx4Xd8Ho6apgeiZTjn6' export ZEEBE_CLIENT_SECRET='iJJu-SHgUtuJTTAMnMLdcb8WGF8s2mHfXhXutEwe8eSbLXn98vUpoxtuLk5uG0en' # export CAMUNDA_CREDENTIALS_SCOPES='Zeebe,Tasklist,Operate,Optimize' # What APIs these client creds are authorised for @@ -244,3 +246,9 @@ class MyLargerDto extends LosslessDto { The Zeebe worker receives custom headers as `job.customHeaders`. The `ZBClient.createWorker()` method accepts a `customHeadersDto` to control the behavior of custom header parsing of number values and provide design-time type information. This follows the same strategy as the job variables, as previously described. + +## Zeebe User Tasks + +From 8.5, you can use Zeebe user tasks. See the documentation on [how to migrate to Zeebe user tasks](https://docs.camunda.io/docs/apis-tools/tasklist-api-rest/migrate-to-zeebe-user-tasks/). + +The SDK supports the Zeebe REST API. Be sure to set the `ZEEBE_REST_ADDRESS` either via environment variable or configuration field. diff --git a/docker/docker-compose-multitenancy.yml b/docker/docker-compose-multitenancy.yaml similarity index 99% rename from docker/docker-compose-multitenancy.yml rename to docker/docker-compose-multitenancy.yaml index da332e31..366b1f95 100644 --- a/docker/docker-compose-multitenancy.yml +++ b/docker/docker-compose-multitenancy.yaml @@ -16,6 +16,7 @@ services: ports: - "26500:26500" - "9600:9600" + - "8080:8080" environment: # https://docs.camunda.io/docs/self-managed/zeebe-deployment/configuration/environment-variables/ - ZEEBE_BROKER_GATEWAY_SECURITY_AUTHENTICATION_MODE=${ZEEBE_AUTHENTICATION_MODE} - ZEEBE_BROKER_GATEWAY_SECURITY_AUTHENTICATION_IDENTITY_ISSUERBACKENDURL=http://keycloak:8080/auth/realms/camunda-platform diff --git a/docker/docker-compose.yml b/docker/docker-compose.yaml similarity index 99% rename from docker/docker-compose.yml rename to docker/docker-compose.yaml index 23db838c..9f93b71f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yaml @@ -16,6 +16,7 @@ services: ports: - "26500:26500" - "9600:9600" + - "8080:8080" environment: # https://docs.camunda.io/docs/self-managed/zeebe-deployment/configuration/environment-variables/ - ZEEBE_BROKER_GATEWAY_SECURITY_AUTHENTICATION_MODE=${ZEEBE_AUTHENTICATION_MODE} - ZEEBE_BROKER_GATEWAY_SECURITY_AUTHENTICATION_IDENTITY_ISSUERBACKENDURL=http://keycloak:8080/auth/realms/camunda-platform diff --git a/package.json b/package.json index 18a8a49e..68106518 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,13 @@ "compile": "tsc --project tsconfig.json", "docs": "rm -rf ./docs && typedoc", "generate:grpc": "grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:./src/generated --grpc_out=./src/generated --ts_out=./src/generated -I ./src/proto ./src/proto/*.proto", - "test": "cross-env CAMUNDA_UNIT_TEST=true jest '.*unit.*' -u --detectOpenHandles --runInBand --testPathIgnorePatterns integration --testPathIgnorePatterns local-integration --testPathIgnorePatterns disconnection --testPathIgnorePatterns multitenancy --testPathIgnorePatterns __tests__/config", - "test:integration": "jest --runInBand --testPathIgnorePatterns disconnection --testPathIgnorePatterns '.unit.*' --testPathIgnorePatterns __tests__/config --testPathIgnorePatterns multitenancy --detectOpenHandles --verbose true -u", - "test:multitenancy": "jest --runInBand --testPathIgnorePatterns disconnection --testPathIgnorePatterns admin --testPathIgnorePatterns '.unit.*' --testPathIgnorePatterns __tests__/config - --detectOpenHandles --verbose true -u", + "sm:start": "docker compose -f docker/docker-compose-multitenancy.yaml -f docker/docker-compose-modeler.yaml up -d", + "sm:stop": "docker compose -f docker/docker-compose-multitenancy.yaml -f docker/docker-compose-modeler.yaml down", + "test": "cross-env CAMUNDA_UNIT_TEST=true jest '\\.unit\\.' -u --detectOpenHandles --runInBand --testPathIgnorePatterns integration --testPathIgnorePatterns local-integration --testPathIgnorePatterns disconnection --testPathIgnorePatterns multitenancy --testPathIgnorePatterns __tests__/config", + "test:integration": "jest --runInBand --testPathIgnorePatterns disconnection --testPathIgnorePatterns '\\.unit\\.' --testPathIgnorePatterns __tests__/config --testPathIgnorePatterns multitenancy --detectOpenHandles --verbose true -u", + "test:multitenancy": "jest --runInBand --testPathIgnorePatterns disconnection --testPathIgnorePatterns admin --testPathIgnorePatterns '\\.unit\\.' --testPathIgnorePatterns __tests__/config - --detectOpenHandles --verbose true -u", "test:local": "jest --runInBand --verbose true --detectOpenHandles local-integration -u", - "test:local-integration": "jest --runInBand --detectOpenHandles --verbose --testPathIgnorePatterns disconnection --testPathIgnorePatterns '.unit.*' --testPathIgnorePatterns admin --testPathIgnorePatterns multitenancy --testPathIgnorePatterns __tests__/config -u", + "test:local-integration": "jest --runInBand --detectOpenHandles --verbose --testPathIgnorePatterns disconnection --testPathIgnorePatterns '\\.unit\\.' --testPathIgnorePatterns admin --testPathIgnorePatterns multitenancy --testPathIgnorePatterns __tests__/config -u", "test:docker": "jest --runInBand --testPathIgnorePatterns disconnection --testPathIgnorePatterns __tests__/config local-integration --detectOpenHandles --verbose true", "test:disconnect": "jest --runInBand --verbose true --detectOpenHandles --testPathIgnorePatterns __tests__/config disconnection", "test:smoketest": "npm run build && node smoke-test/smoke-test.js && npx tsd --typings dist/", diff --git a/smoke-test/smoke-test.js b/smoke-test/smoke-test.js index e6d65084..3a061b2b 100644 --- a/smoke-test/smoke-test.js +++ b/smoke-test/smoke-test.js @@ -43,4 +43,8 @@ const operate = camunda.getOperateApiClient() // console.log(operate) +const zeebeRest = camunda.getZeebeRestClient() + +// console.log(zeebeRest) + console.log('Smoke test passed!') diff --git a/src/__tests__/zeebe/integration-rest/Topology.spec.ts b/src/__tests__/zeebe/integration-rest/Topology.spec.ts new file mode 100644 index 00000000..da595ee3 --- /dev/null +++ b/src/__tests__/zeebe/integration-rest/Topology.spec.ts @@ -0,0 +1,9 @@ +import { ZeebeRestClient } from '../../../zeebe' + +describe('ZeebeRestClient', () => { + it('can get the topology', async () => { + const zbc = new ZeebeRestClient() + const topology = await zbc.getTopology() + expect(topology).toHaveProperty('brokers') + }) +}) diff --git a/src/c8/index.ts b/src/c8/index.ts index bb8e3d46..d542a63b 100644 --- a/src/c8/index.ts +++ b/src/c8/index.ts @@ -9,7 +9,7 @@ import { OAuthProvider } from '../oauth' import { OperateApiClient } from '../operate' import { OptimizeApiClient } from '../optimize' import { TasklistApiClient } from '../tasklist' -import { ZeebeGrpcClient } from '../zeebe' +import { ZeebeGrpcClient, ZeebeRestClient } from '../zeebe' /** * A single point of configuration for all Camunda Platform 8 clients. @@ -22,6 +22,7 @@ import { ZeebeGrpcClient } from '../zeebe' * * const c8 = new Camunda8() * const zeebe = c8.getZeebeGrpcApiClient() + * const zeebeRest = c8.getZeebeRestClient() * const operate = c8.getOperateApiClient() * const optimize = c8.getOptimizeApiClient() * const tasklist = c8.getTasklistApiClient() @@ -36,6 +37,7 @@ export class Camunda8 { private optimizeApiClient?: OptimizeApiClient private tasklistApiClient?: TasklistApiClient private zeebeGrpcApiClient?: ZeebeGrpcClient + private zeebeRestClient?: ZeebeRestClient private configuration: CamundaPlatform8Configuration private oAuthProvider?: OAuthProvider @@ -107,4 +109,14 @@ export class Camunda8 { } return this.zeebeGrpcApiClient } + + public getZeebeRestClient(): ZeebeRestClient { + if (!this.zeebeRestClient) { + this.zeebeRestClient = new ZeebeRestClient({ + config: this.configuration, + oAuthProvider: this.oAuthProvider, + }) + } + return this.zeebeRestClient + } } diff --git a/src/lib/GotErrors.ts b/src/lib/GotErrors.ts index 9055a846..94d7dd47 100644 --- a/src/lib/GotErrors.ts +++ b/src/lib/GotErrors.ts @@ -4,9 +4,19 @@ export class HTTPError extends Got.HTTPError { statusCode: number constructor(response: Got.Response) { super(response) - const details = JSON.parse((response?.body as string) || '{}') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.statusCode = details.status + try { + const details = JSON.parse((response?.body as string) || '{}') + this.statusCode = details.status + } catch (e) { + this.statusCode = 0 + // Sometimes APIs return errors data in plain text (not JSON) + // and sometimes we get back an HTML error page (for example: 502 Bad Gateway) + // We want to extract and surface plain text errors + // and ignore the HTML strings + if (!((response.body as string) ?? '<').startsWith('<')) { + this.message += ` - ${response.body}` + } + } } } diff --git a/src/lib/GotHooks.ts b/src/lib/GotHooks.ts index 0bf87163..2f3ca71d 100644 --- a/src/lib/GotHooks.ts +++ b/src/lib/GotHooks.ts @@ -24,8 +24,12 @@ export const gotBeforeErrorHook = (error) => { const { request } = error if (error instanceof GotHTTPError) { error = new HTTPError(error.response) - const details = JSON.parse((error.response?.body as string) || '{}') - error.statusCode = details.status + try { + const details = JSON.parse((error.response?.body as string) || '{}') + error.statusCode = details.status + } catch (e) { + error.statusCode = 0 + } } // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(error as any).source = (error as any).options.context.stack.split('\n') diff --git a/src/oauth/lib/OAuthProvider.ts b/src/oauth/lib/OAuthProvider.ts index e180bf64..dea642f6 100644 --- a/src/oauth/lib/OAuthProvider.ts +++ b/src/oauth/lib/OAuthProvider.ts @@ -292,8 +292,10 @@ export class OAuthProvider implements IOAuthProvider { 'user-agent': this.userAgentString, accept: '*/*', }, - key: this.mTLSPrivateKey, - cert: this.mTLSCertChain, + https: { + key: this.mTLSPrivateKey, + cert: this.mTLSCertChain, + }, } trace(`Making token request to the token endpoint: `) diff --git a/src/zeebe/index.ts b/src/zeebe/index.ts index 6f7b9fc3..3898da30 100644 --- a/src/zeebe/index.ts +++ b/src/zeebe/index.ts @@ -9,6 +9,7 @@ import * as PublishedContract from './lib/interfaces-published-contract' export * from './zb/ZBWorker' export * from './zb/ZeebeGrpcClient' +export * from './zb/ZeebeRESTClient' /** @namespace */ export const Types = { ...Interfaces, diff --git a/src/zeebe/zb/ZeebeRESTClient.ts b/src/zeebe/zb/ZeebeRESTClient.ts new file mode 100644 index 00000000..2141aef2 --- /dev/null +++ b/src/zeebe/zb/ZeebeRESTClient.ts @@ -0,0 +1,176 @@ +import { debug } from 'debug' +import got from 'got' + +import { + CamundaEnvironmentConfigurator, + CamundaPlatform8Configuration, + DeepPartial, + GetCustomCertificateBuffer, + GotRetryConfig, + RequireConfiguration, + constructOAuthProvider, + createUserAgentString, + gotBeforeErrorHook, + gotErrorHandler, + makeBeforeRetryHandlerFor401TokenRetry, +} from '../../lib' +import { IOAuthProvider } from '../../oauth' +import { TopologyResponse } from '../types' + +const trace = debug('camunda:zeebe') + +const ZEEBE_REST_API_VERSION = 'v1' + +/** + * JSON object with changed task attribute values. + */ +interface TaskChangeSet { + /* The due date of the task. Reset by providing an empty String. */ + dueDate: Date | string + /* The follow-up date of the task. Reset by providing an empty String. */ + followUpDate: Date | string + /* The list of candidate users of the task. Reset by providing an empty list. */ + candidateUsers: string[] + /* The list of candidate groups of the task. Reset by providing an empty list. */ + candidateGroups: string[] +} + +export class ZeebeRestClient { + private userAgentString: string + private oAuthProvider: IOAuthProvider + private rest: Promise + // private tenantId: string | undefined + + constructor(options?: { + config?: DeepPartial + oAuthProvider?: IOAuthProvider + }) { + const config = CamundaEnvironmentConfigurator.mergeConfigWithEnvironment( + options?.config ?? {} + ) + trace('options.config', options?.config) + trace('config', config) + this.oAuthProvider = + options?.oAuthProvider ?? constructOAuthProvider(config) + this.userAgentString = createUserAgentString(config) + const baseUrl = RequireConfiguration( + config.ZEEBE_REST_ADDRESS, + 'ZEEBE_REST_ADDRESS' + ) + + const prefixUrl = `${baseUrl}/${ZEEBE_REST_API_VERSION}` + + this.rest = GetCustomCertificateBuffer(config).then( + (certificateAuthority) => + got.extend({ + prefixUrl, + retry: GotRetryConfig, + https: { + certificateAuthority, + }, + handlers: [gotErrorHandler], + hooks: { + beforeRetry: [ + makeBeforeRetryHandlerFor401TokenRetry( + this.getHeaders.bind(this) + ), + ], + beforeError: [gotBeforeErrorHook], + }, + }) + ) + + // this.tenantId = config.CAMUNDA_TENANT_ID + } + + private async getHeaders() { + const token = await this.oAuthProvider.getToken('ZEEBE') + + const headers = { + 'content-type': 'application/json', + authorization: `Bearer ${token}`, + 'user-agent': this.userAgentString, + accept: '*/*', + } + trace('headers', headers) + return headers + } + + /* Get the topology of the Zeebe cluster. */ + public async getTopology(): Promise { + const headers = await this.getHeaders() + return this.rest.then((rest) => + rest + .get('topology', { headers }) + .json() + .catch((error) => { + trace('error', error) + throw error + }) + ) as Promise + } + + /* Completes a user task with the given key. */ + public completeUserTask({ + userTaskKey, + variables, + action = 'complete', + }: { + userTaskKey: string + variables: Record + action: string + }) { + return this.rest.then((rest) => + rest.post(`user-tasks/${userTaskKey}/completion`, { + body: JSON.stringify({ + variables, + action, + }), + }) + ) + } + + /* Assigns a user task with the given key to the given assignee. */ + public assignTask({ + userTaskKey, + assignee, + allowOverride = true, + action = 'assign', + }: { + userTaskKey: string + assignee: string + allowOverride?: boolean + action: string + }) { + return this.rest.then((rest) => + rest.post(`user-tasks/${userTaskKey}/assignment`, { + body: JSON.stringify({ + allowOverride, + action, + assignee, + }), + }) + ) + } + + /** Update a user task with the given key. */ + public updateTask({ + userTaskKey, + changeset, + }: { + userTaskKey: string + changeset: TaskChangeSet + }) { + return this.rest.then((rest) => + rest.post(`user-tasks/${userTaskKey}/update`, { + body: JSON.stringify(changeset), + }) + ) + } + /* Removes the assignee of a task with the given key. */ + public removeAssignee({ userTaskKey }: { userTaskKey: string }) { + return this.rest.then((rest) => + rest.delete(`user-tasks/${userTaskKey}/assignee`) + ) + } +} From bd49a8e411c33316044a82b65bb5e47e131c6ed6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 22 May 2024 10:04:37 +0000 Subject: [PATCH 08/10] chore(release): 8.5.4-alpha.2 [skip ci] ## [8.5.4-alpha.2](https://github.com/camunda/camunda-8-js-sdk/compare/v8.5.4-alpha.1...v8.5.4-alpha.2) (2024-05-22) ### Bug Fixes * **issue137:** support ZEEBE_REST_ADDRESS and ZEEBE_GRPC_ADDRESS environment variables ([#159](https://github.com/camunda/camunda-8-js-sdk/issues/159)) ([41fdca0](https://github.com/camunda/camunda-8-js-sdk/commit/41fdca0fcc9f7221c915dc82317e6609bb5106ee)) * **oauth:** correctly expire cached token ([#164](https://github.com/camunda/camunda-8-js-sdk/issues/164)) ([c86e550](https://github.com/camunda/camunda-8-js-sdk/commit/c86e550747f23205dac9fe199a38217b3a583f76)), closes [#163](https://github.com/camunda/camunda-8-js-sdk/issues/163) ### Features * **zeebe:** support Zeebe User Task REST API ([022607b](https://github.com/camunda/camunda-8-js-sdk/commit/022607bf77077fdacffdce7f26ce580360d54bf3)), closes [#34](https://github.com/camunda/camunda-8-js-sdk/issues/34) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68106518..9abde404 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@camunda8/sdk", - "version": "8.5.4-alpha.1", + "version": "8.5.4-alpha.2", "description": "", "main": "dist/index.js", "scripts": { From 44320bf90b59232ca370a4471728ce919b32d9a3 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 22 May 2024 10:04:38 +0000 Subject: [PATCH 09/10] chore(release): 8.5.4-alpha.2 [skip ci] ## [8.5.4-alpha.2](https://github.com/camunda/camunda-8-js-sdk/compare/v8.5.4-alpha.1...v8.5.4-alpha.2) (2024-05-22) ### Bug Fixes * **issue137:** support ZEEBE_REST_ADDRESS and ZEEBE_GRPC_ADDRESS environment variables ([#159](https://github.com/camunda/camunda-8-js-sdk/issues/159)) ([41fdca0](https://github.com/camunda/camunda-8-js-sdk/commit/41fdca0fcc9f7221c915dc82317e6609bb5106ee)) * **oauth:** correctly expire cached token ([#164](https://github.com/camunda/camunda-8-js-sdk/issues/164)) ([c86e550](https://github.com/camunda/camunda-8-js-sdk/commit/c86e550747f23205dac9fe199a38217b3a583f76)), closes [#163](https://github.com/camunda/camunda-8-js-sdk/issues/163) ### Features * **zeebe:** support Zeebe User Task REST API ([022607b](https://github.com/camunda/camunda-8-js-sdk/commit/022607bf77077fdacffdce7f26ce580360d54bf3)), closes [#34](https://github.com/camunda/camunda-8-js-sdk/issues/34) --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab89562..2ce80b7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [8.5.4-alpha.2](https://github.com/camunda/camunda-8-js-sdk/compare/v8.5.4-alpha.1...v8.5.4-alpha.2) (2024-05-22) + + +### Bug Fixes + +* **issue137:** support ZEEBE_REST_ADDRESS and ZEEBE_GRPC_ADDRESS environment variables ([#159](https://github.com/camunda/camunda-8-js-sdk/issues/159)) ([41fdca0](https://github.com/camunda/camunda-8-js-sdk/commit/41fdca0fcc9f7221c915dc82317e6609bb5106ee)) +* **oauth:** correctly expire cached token ([#164](https://github.com/camunda/camunda-8-js-sdk/issues/164)) ([c86e550](https://github.com/camunda/camunda-8-js-sdk/commit/c86e550747f23205dac9fe199a38217b3a583f76)), closes [#163](https://github.com/camunda/camunda-8-js-sdk/issues/163) + + +### Features + +* **zeebe:** support Zeebe User Task REST API ([022607b](https://github.com/camunda/camunda-8-js-sdk/commit/022607bf77077fdacffdce7f26ce580360d54bf3)), closes [#34](https://github.com/camunda/camunda-8-js-sdk/issues/34) + ## [8.5.4-alpha.1](https://github.com/camunda/camunda-8-js-sdk/compare/v8.5.3...v8.5.4-alpha.1) (2024-05-15) diff --git a/package-lock.json b/package-lock.json index da11da13..26400939 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@camunda8/sdk", - "version": "8.5.4-alpha.1", + "version": "8.5.4-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@camunda8/sdk", - "version": "8.5.4-alpha.1", + "version": "8.5.4-alpha.2", "license": "Apache 2.0", "dependencies": { "@grpc/grpc-js": "1.10.7", From d6acdfddcef8413226e3366932df5b6bda234e47 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 24 May 2024 13:28:11 +1200 Subject: [PATCH 10/10] feat(camunda8): support Basic Auth * feat(camunda8): support Basic Auth Implement Basic Auth provider for all clients fixes #165 * refactor: remove trace output of auth creds --- README.md | 39 ++++++++++------ package-lock.json | 23 ++++++++++ package.json | 2 + .../oauth/OAuthProvider.unit.spec.ts | 44 ++++++++++++++++++- src/admin/lib/AdminApiClient.ts | 1 - src/lib/Configuration.ts | 17 +++++++ src/lib/ConstructOAuthProvider.ts | 21 ++++++++- src/modeler/lib/ModelerAPIClient.ts | 5 +-- src/oauth/lib/BasicAuthProvider.ts | 39 ++++++++++++++++ 9 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 src/oauth/lib/BasicAuthProvider.ts diff --git a/README.md b/README.md index 416f941f..14ead87c 100644 --- a/README.md +++ b/README.md @@ -61,34 +61,42 @@ Some number values - for example: "_total returned results_ " - may be specified For `int64` values whose type is not known ahead of time, such as job variables, you can pass an annotated data transfer object (DTO) to decode them reliably. If no DTO is specified, the default behavior of the SDK is to serialise all numbers to JavaScript `number`, and if a number value is detected at a runtime that cannot be accurately stored as `number`, to throw an exception. -## OAuth +## Authorization -Calls to APIs are authorized using a token that is obtained via a client id/secret pair exchange, and then passes as an authorization header on API calls. The SDK handles this for you. +Calls to APIs can be authorized using basic auth or via OAuth - a token that is obtained via a client id/secret pair exchange. -If your Camunda 8 platform is secured using token exchange, provide the client id and secret to the SDK. +### Disable Auth -### Disable OAuth +To disable OAuth, set the environment variable `CAMUNDA_OAUTH_STRATEGY=NONE`. You can use this when running against a minimal Zeebe broker in a development environment, for example. -To disable OAuth, set the environment variable `CAMUNDA_OAUTH_DISABLED`. You can use this when running against a minimal Zeebe broker in a development environment, for example. +### Basic Auth -With this environment variable set, the SDK will inject a `NullAuthProvider` that does nothing. +To use basic auth, set the following values either via the environment or explicitly in code via the constructor: -### Configuring OAuth +```bash +CAMUNDA_AUTH_STRATEGY=BASIC +CAMUNDA_BASIC_AUTH_USERNAME=.... +CAMUNDA_BASIC_AUTH_PASSWORD=... +``` + +### OAuth -To get a token for use with the application APIs, provide the following configuration fields at a minimum, either via the `Camunda8` constructor or in environment variables: +If your platform is secured with OAuth token exchange (Camunda SaaS or Self-Managed with Identity), provide the following configuration fields at a minimum, either via the `Camunda8` constructor or in environment variables: ```bash -ZEEBE_GRPC_ADDRESS -ZEEBE_CLIENT_ID -ZEEBE_CLIENT_SECRET -CAMUNDA_OAUTH_URL +CAMUNDA_AUTH_STRATEGY=OAUTH +ZEEBE_GRPC_ADDRESS=... +ZEEBE_CLIENT_ID=... +ZEEBE_CLIENT_SECRET=... +CAMUNDA_OAUTH_URL=... ``` To get a token for the Camunda SaaS Administration API or the Camunda SaaS Modeler API, set the following: ```bash -CAMUNDA_CONSOLE_CLIENT_ID -CAMUNDA_CONSOLE_CLIENT_SECRET +CAMUNDA_AUTH_STRATEGY=OAUTH +CAMUNDA_CONSOLE_CLIENT_ID=... +CAMUNDA_CONSOLE_CLIENT_SECRET=... ``` ### Token caching @@ -123,6 +131,7 @@ export ZEEBE_GRPC_ADDRESS='localhost:26500' export ZEEBE_REST_ADDRESS='http://localhost:8080' export ZEEBE_CLIENT_ID='zeebe' export ZEEBE_CLIENT_SECRET='zecret' +export CAMUNDA_OAUTH_STRATEGY='OAUTH' export CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' export CAMUNDA_TASKLIST_BASE_URL='http://localhost:8082' export CAMUNDA_OPERATE_BASE_URL='http://localhost:8081' @@ -153,6 +162,7 @@ const c8 = new Camunda8({ ZEEBE_REST_ADDRESS: 'http://localhost:8080', ZEEBE_CLIENT_ID: 'zeebe', ZEEBE_CLIENT_SECRET: 'zecret', + CAMUNDA_OAUTH_STRATEGY: 'OAUTH', CAMUNDA_OAUTH_URL: 'http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token', CAMUNDA_TASKLIST_BASE_URL: 'http://localhost:8082', @@ -178,6 +188,7 @@ export CAMUNDA_TASKLIST_BASE_URL='https://syd-1.tasklist.camunda.io/5c34c0a7-7f2 export CAMUNDA_OPTIMIZE_BASE_URL='https://syd-1.optimize.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9' export CAMUNDA_OPERATE_BASE_URL='https://syd-1.operate.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9' export CAMUNDA_OAUTH_URL='https://login.cloud.camunda.io/oauth/token' +export CAMUNDA_AUTH_STRATEGY='OAUTH' # This is on by default, but we include it in case it got turned off for local tests export CAMUNDA_SECURE_CONNECTION=true diff --git a/package-lock.json b/package-lock.json index 26400939..d2f16e93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@sitapati/testcontainers": "^2.8.1", + "@types/basic-auth": "^1.1.8", "@types/debug": "^4.1.12", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", @@ -45,6 +46,7 @@ "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", + "basic-auth": "^2.0.1", "commitizen": "^4.3.0", "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", @@ -3589,6 +3591,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/basic-auth": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/basic-auth/-/basic-auth-1.1.8.tgz", + "integrity": "sha512-dKcUeixGuZn8pBjcUrf1N7x5K6lWuKuwHHitM2IZ4vwZUDWEhhNtwCWiba8jTA9zn0GQQ+fTFkWpKx8pOU/enw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -4701,6 +4712,18 @@ ], "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/before-after-hook": { "version": "2.2.3", "dev": true, diff --git a/package.json b/package.json index 9abde404..38cd59f6 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@sitapati/testcontainers": "^2.8.1", + "@types/basic-auth": "^1.1.8", "@types/debug": "^4.1.12", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", @@ -109,6 +110,7 @@ "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", + "basic-auth": "^2.0.1", "commitizen": "^4.3.0", "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", diff --git a/src/__tests__/oauth/OAuthProvider.unit.spec.ts b/src/__tests__/oauth/OAuthProvider.unit.spec.ts index 20625e3b..9d5cff29 100644 --- a/src/__tests__/oauth/OAuthProvider.unit.spec.ts +++ b/src/__tests__/oauth/OAuthProvider.unit.spec.ts @@ -4,9 +4,11 @@ import http from 'http' import os from 'os' import path from 'path' +import auth from 'basic-auth' +import got from 'got' import jwt from 'jsonwebtoken' -import { EnvironmentSetup } from '../../lib' +import { EnvironmentSetup, constructOAuthProvider } from '../../lib' import { OAuthProvider } from '../../oauth' jest.setTimeout(10000) @@ -573,4 +575,44 @@ describe('OAuthProvider', () => { expect(thrown).toBe(true) }) }) + + it('Can use Basic Auth as a strategy', async () => { + const server = http.createServer((req, res) => { + const credentials = auth(req) + + if ( + !credentials || + credentials.name !== 'admin' || + credentials.pass !== 'supersecret' + ) { + res.statusCode = 401 + res.setHeader('WWW-Authenticate', 'Basic realm="example"') + res.end('Access denied') + } else { + res.end('Access granted') + } + }) + + server.listen(3033, () => { + console.log('Server running on port 3033') + }) + + const oAuthProvider = constructOAuthProvider({ + CAMUNDA_AUTH_STRATEGY: 'BASIC', + CAMUNDA_BASIC_AUTH_PASSWORD: 'supersecret', + CAMUNDA_BASIC_AUTH_USERNAME: 'admin', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any) + const token = await oAuthProvider.getToken('ZEEBE') + await got + .get('http://localhost:3033', { + headers: { + Authorization: 'Basic ' + token, + }, + }) + .then((res) => { + server.close() + expect(res).toBeTruthy() + }) + }) }) diff --git a/src/admin/lib/AdminApiClient.ts b/src/admin/lib/AdminApiClient.ts index 675bbddf..b262c105 100644 --- a/src/admin/lib/AdminApiClient.ts +++ b/src/admin/lib/AdminApiClient.ts @@ -176,7 +176,6 @@ export class AdminApiClient { body: JSON.stringify(createClusterRequest), headers, } - debug(req) const rest = await this.rest return rest.post('', req).json() } diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index 14d42c19..73476b42 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -173,6 +173,21 @@ const getMainEnv = () => type: 'string', optional: true, }, + /** Username for Basic Auth */ + CAMUNDA_BASIC_AUTH_USERNAME: { + type: 'string', + optional: true, + }, + /** Username for Basic Auth */ + CAMUNDA_BASIC_AUTH_PASSWORD: { + type: 'string', + optional: true, + }, + CAMUNDA_AUTH_STRATEGY: { + type: 'string', + choices: ['BASIC', 'OAUTH', 'NONE'], + default: 'OAUTH', + }, }) const getZeebeEnv = () => @@ -377,6 +392,8 @@ export const CamundaEnvironmentVariableDictionary = 'GRPC_KEEPALIVE_TIME_MS', 'ZEEBE_REST_ADDRESS', 'ZEEBE_GRPC_ADDRESS', + 'CAMUNDA_BASIC_AUTH_USERNAME', + 'CAMUNDA_BASIC_AUTH_PASSWORD', 'ZEEBE_ADDRESS', 'ZEEBE_CLIENT_ID', 'ZEEBE_CLIENT_SECRET', diff --git a/src/lib/ConstructOAuthProvider.ts b/src/lib/ConstructOAuthProvider.ts index 32ef884b..23a23004 100644 --- a/src/lib/ConstructOAuthProvider.ts +++ b/src/lib/ConstructOAuthProvider.ts @@ -1,11 +1,28 @@ +import debug from 'debug' + import { NullAuthProvider, OAuthProvider } from '../oauth' +import { BasicAuthProvider } from '../oauth/lib/BasicAuthProvider' import { CamundaPlatform8Configuration } from './Configuration' +const trace = debug('camunda:oauth') + export function constructOAuthProvider(config: CamundaPlatform8Configuration) { - if (config.CAMUNDA_OAUTH_DISABLED) { + trace(`Auth strategy is ${config.CAMUNDA_AUTH_STRATEGY}`) + trace(`OAuth disabled is ${config.CAMUNDA_OAUTH_DISABLED}`) + if ( + config.CAMUNDA_OAUTH_DISABLED || + config.CAMUNDA_AUTH_STRATEGY === 'NONE' + ) { + trace(`Disabling Auth`) return new NullAuthProvider() } else { - return new OAuthProvider({ config }) + if (config.CAMUNDA_AUTH_STRATEGY === 'BASIC') { + trace(`Using Basic Auth`) + return new BasicAuthProvider({ config }) + } else { + trace(`Using OAuth`) + return new OAuthProvider({ config }) + } } } diff --git a/src/modeler/lib/ModelerAPIClient.ts b/src/modeler/lib/ModelerAPIClient.ts index c57dbfed..d052a22f 100644 --- a/src/modeler/lib/ModelerAPIClient.ts +++ b/src/modeler/lib/ModelerAPIClient.ts @@ -64,14 +64,13 @@ export class ModelerApiClient { private async getHeaders() { const token = await this.oAuthProvider.getToken('MODELER') - const auth = `Bearer ${token}` + const authorization = `Bearer ${token}` const headers = { 'content-type': 'application/json', - authorization: auth, + authorization, 'user-agent': this.userAgentString, accept: '*/*', } - debug(auth) return headers } diff --git a/src/oauth/lib/BasicAuthProvider.ts b/src/oauth/lib/BasicAuthProvider.ts new file mode 100644 index 00000000..925c90a2 --- /dev/null +++ b/src/oauth/lib/BasicAuthProvider.ts @@ -0,0 +1,39 @@ +import debug from 'debug' + +import { + CamundaEnvironmentConfigurator, + CamundaPlatform8Configuration, + DeepPartial, + RequireConfiguration, +} from '../../lib' + +import { IOAuthProvider, TokenGrantAudienceType } from './IOAuthProvider' + +const trace = debug('camunda:oauth') + +export class BasicAuthProvider implements IOAuthProvider { + private username: string | undefined + private password: string | undefined + constructor(options?: { + config?: DeepPartial + }) { + const config = CamundaEnvironmentConfigurator.mergeConfigWithEnvironment( + options?.config ?? {} + ) + this.username = RequireConfiguration( + config.CAMUNDA_BASIC_AUTH_USERNAME, + 'CAMUNDA_BASIC_AUTH_USERNAME' + ) + this.password = RequireConfiguration( + config.CAMUNDA_BASIC_AUTH_PASSWORD, + 'CAMUNDA_BASIC_AUTH_PASSWORD' + ) + } + getToken(audience: TokenGrantAudienceType): Promise { + trace(`Requesting token for audience ${audience}`) + const token = Buffer.from(`${this.username}:${this.password}`).toString( + 'base64' + ) + return Promise.resolve(token) + } +}