From b6fde28231e3175319e23c5c2d8dc9e2ec72a651 Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Mon, 21 Mar 2022 15:27:29 +0000 Subject: [PATCH 1/3] Support oauth to remote decoder via web oidc token. --- client/features/data-conversion.js | 7 +++++-- client/routes/workflow/index.vue | 4 +++- server/config.yml | 3 ++- server/config/index.js | 4 +++- server/routes.js | 3 +++ 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/client/features/data-conversion.js b/client/features/data-conversion.js index 6c778c2b..11f0eba4 100644 --- a/client/features/data-conversion.js +++ b/client/features/data-conversion.js @@ -1,7 +1,10 @@ import WebSocketAsPromised from 'websocket-as-promised'; -export const convertEventPayloadsWithRemoteEncoder = async (events, endpoint) => { - const headers = { 'Content-Type': 'application/json' }; +export const convertEventPayloadsWithRemoteEncoder = async (events, endpoint, accessToken) => { + let headers = { 'Content-Type': 'application/json' }; + if (accessToken) { + headers['Authorization'] = `Bearer ${accessToken}`; + } const requests = []; events.forEach(event => { diff --git a/client/routes/workflow/index.vue b/client/routes/workflow/index.vue index 8e2150d2..bccc774d 100644 --- a/client/routes/workflow/index.vue +++ b/client/routes/workflow/index.vue @@ -221,7 +221,9 @@ export default { } if (endpoint !== undefined) { - return convertEventPayloadsWithRemoteEncoder(events, endpoint).catch(error => { + const accessToken = this.webSettings.dataEncoder.accessToken; + + return convertEventPayloadsWithRemoteEncoder(events, endpoint, accessToken).catch(error => { console.error(error); this.$emit('onNotification', { diff --git a/server/config.yml b/server/config.yml index a5cd56f7..4b466d40 100644 --- a/server/config.yml +++ b/server/config.yml @@ -19,4 +19,5 @@ routing: # data_encoder: # Remote Data Encoder Endpoint - # endpoint: https://remote_encoder.myorg.com \ No newline at end of file + # endpoint: https://remote_encoder.myorg.com + # pass_auth_token: false \ No newline at end of file diff --git a/server/config/index.js b/server/config/index.js index f30ba468..db5f0096 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -5,6 +5,7 @@ const logger = require('../logger'); const configPath = process.env.TEMPORAL_CONFIG_PATH || './server/config.yml'; const dataEncoderEndpoint = process.env.TEMPORAL_DATA_ENCODER_ENDPOINT; +const dataEncoderPassAccessToken = process.env.TEMPORAL_DATA_ENCODER_PASS_ACCESS_TOKEN ? ![false, 'false'].includes(process.env.TEMPORAL_DATA_ENCODER_PASS_ACCESS_TOKEN) : undefined; const readConfigSync = () => { const cfgContents = readFileSync(configPath, { @@ -34,7 +35,8 @@ const getDataEncoderConfig = async () => { // Data encoder endpoint from the environment takes precedence over // configuration file value. const dataEncoderConfig = { - endpoint: dataEncoderEndpoint || data_encoder?.endpoint + endpoint: dataEncoderEndpoint || data_encoder?.endpoint, + passAccessToken: dataEncoderPassAccessToken || !!data_encoder?.pass_auth_token, } return dataEncoderConfig; diff --git a/server/routes.js b/server/routes.js index 32aa80df..e10e8ea0 100644 --- a/server/routes.js +++ b/server/routes.js @@ -367,6 +367,9 @@ router.get('/api/web-settings', async (ctx) => { if (ctx.session.dataEncoder?.endpoint) { dataEncoder.endpoint = ctx.session.dataEncoder.endpoint; } + if (dataEncoder.passAccessToken && !!ctx.state.user) { + dataEncoder.accessToken = ctx.state.user.accessToken; + } const auth = { enabled }; // only include non-sensitive data From 59e608fba3fb6b66be4433a80916a8015df084ac Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Fri, 25 Mar 2022 08:09:33 +0000 Subject: [PATCH 2/3] Clean up. --- server/routes.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/routes.js b/server/routes.js index e10e8ea0..513317c0 100644 --- a/server/routes.js +++ b/server/routes.js @@ -358,16 +358,17 @@ router.post('/api/web-settings/remote-data-encoder/:endpoint', async (ctx) => { router.get('/api/web-settings', async (ctx) => { const routing = await getRoutingConfig(); const { enabled } = await getAuthConfig(); - const dataEncoder = await getDataEncoderConfig(); + const dataEncoderConfig = await getDataEncoderConfig(); const permitWriteApi = isWriteApiPermitted(); const dataConverter = ctx.session.dataConverter; - + // Encoder endpoint from the session has higher priority than global config. // This is to allow for testing of new remote encoder endpoints. - if (ctx.session.dataEncoder?.endpoint) { - dataEncoder.endpoint = ctx.session.dataEncoder.endpoint; - } - if (dataEncoder.passAccessToken && !!ctx.state.user) { + let dataEncoder = { + endpoint: ctx.session.dataEncoder?.endpoint || dataEncoderConfig.endpoint + }; + + if (dataEncoderConfig.passAccessToken && !!ctx.state.user) { dataEncoder.accessToken = ctx.state.user.accessToken; } From 16d6ca56294e0951aa241d88318bf46fbd330529 Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Fri, 25 Mar 2022 08:28:07 +0000 Subject: [PATCH 3/3] Improve terminology and refer to Codec not Remote Encoder. --- README.md | 42 ++++++++++++++++-------------- client/features/data-conversion.js | 2 +- client/routes/workflow/index.vue | 10 +++---- server/config.yml | 8 +++--- server/config/index.js | 20 +++++++------- server/routes.js | 20 +++++++------- 6 files changed, 51 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 671ad0f1..5a7e7a47 100644 --- a/README.md +++ b/README.md @@ -20,19 +20,21 @@ For a **video demo** of how this looks, you can [check our docs](https://docs.te Set these environment variables if you need to change their defaults -| Variable | Description | Default | -| ----------------------------- | ----------------------------------------------------------------- | ----------------------------- | -| TEMPORAL_GRPC_ENDPOINT | String representing server gRPC endpoint | 127.0.0.1:7233 | -| TEMPORAL_WEB_PORT | HTTP port to serve on | 8088 | -| TEMPORAL_CONFIG_PATH | Path to config file, see [configurations](#configuring-authentication-optional) | ./server/config.yml | -| TEMPORAL_PERMIT_WRITE_API | Boolean to permit write API methods such as Terminating Workflows | true | -| TEMPORAL_WEB_ROOT_PATH | The root path to serve the app under. Ex. "/test/" | / | -| TEMPORAL_HOT_RELOAD_PORT | HTTP port used by hot reloading in development | 8081 | -| TEMPORAL_HOT_RELOAD_TEST_PORT | HTTP port used by hot reloading in tests | 8082 | -| TEMPORAL_SESSION_SECRET | Secret used to hash the session with HMAC | "ensure secret in production" | -| TEMPORAL_EXTERNAL_SCRIPTS | Additional JavaScript tags to serve in the UI | | -| TEMPORAL_GRPC_MAX_MESSAGE_LENGTH | gRPC max message length (bytes) | 4194304 (4mb) | -| TEMPORAL_DATA_ENCODER_ENDPOINT | Remote Data Encoder Endpoint, explained below | | +| Variable | Description | Default | +| -------------------------------- | ----------------------------------------------------------------- | ----------------------------- | +| TEMPORAL_GRPC_ENDPOINT | String representing server gRPC endpoint | 127.0.0.1:7233 | +| TEMPORAL_WEB_PORT | HTTP port to serve on | 8088 | +| TEMPORAL_CONFIG_PATH | Path to config file, see [configurations](#configuring-authentication-optional) | ./server/config.yml | +| TEMPORAL_PERMIT_WRITE_API | Boolean to permit write API methods such as Terminating Workflows | true | +| TEMPORAL_WEB_ROOT_PATH | The root path to serve the app under. Ex. "/test/" | / | +| TEMPORAL_HOT_RELOAD_PORT | HTTP port used by hot reloading in development | 8081 | +| TEMPORAL_HOT_RELOAD_TEST_PORT | HTTP port used by hot reloading in tests | 8082 | +| TEMPORAL_SESSION_SECRET | Secret used to hash the session with HMAC | "ensure secret in production" | +| TEMPORAL_EXTERNAL_SCRIPTS | Additional JavaScript tags to serve in the UI | | +| TEMPORAL_GRPC_MAX_MESSAGE_LENGTH | gRPC max message length (bytes) | 4194304 (4mb) | +| TEMPORAL_CODEC_ENDPOINT | Codec Endpoint, explained below | | +| TEMPORAL_CODEC_PASS_ACCESS_TOKEN | Send OIDC access token to Codec Server | false | +
@@ -60,21 +62,21 @@ Setting `TEMPORAL_TLS_REFRESH_INTERVAL` will make the TLS certs reload every N s
-### Configuring Remote Data Encoder (optional) +### Configuring a Codec Endpoint (optional) -If you are using a data converter on your workers to encrypt Temporal Payloads you may wish to deploy a remote data encoder so that your users can see the unencrypted Payloads while using Temporal Web. The documentation for the Temporal SDK you are using for your application should include documentation on how to build a remote data encoder. Please let us know if this is not the case. Once you have a remote data encoder running you can configure Temporal Web to use it to decode Payloads for a user in 2 ways: +If you are using a codec on your workers to encrypt or compress Temporal Payloads you may wish to deploy a codec server so that your users can see the decoded Payloads while using Temporal Web. The samples for the Temporal SDK you are using for your application should include examples of how to build a codec server. Please let us know if this is not the case. Once you have a codec server running you can configure Temporal Web to use it to decode Payloads for a user in 2 ways: 1. Edit the `server/config.yml` file: ```yaml - data_encoder: - endpoint: https://remote_encoder.myorg.com + codec: + endpoint: https://codec.myorg.com ``` -2. Set the environment variable TEMPORAL_DATA_ENCODER_ENDPOINT to the URL for your remote data encoder. This is often a more convenient option when running Temporal Web in a docker container. +2. Set the environment variable TEMPORAL_CODEC_ENDPOINT to the URL for your remote codec server. This is often a more convenient option when running Temporal Web in a docker container. -Temporal Web will then configure it's UI to decode Payloads as appropriate via the remote data encoder. +Temporal Web will then configure it's UI to decode Payloads as appropriate via the codec. -Please note that requests to the remote data encoder will be made from the user's browser directly, not via Temporal Web's server. This means that the Temporal Web server will never see the decoded Payloads and does not need to be able to connect to the remote data encoder. This allows using remote data encoders on internal and secure networks while using an externally hosted Temporal Web instance, such that provided by Temporal Cloud. +Please note that requests to the codec server will be made from the user's browser directly, not via Temporal Web's server. This means that the Temporal Web server will never see the decoded Payloads and does not need to be able to connect to the codec server. This allows using codec servers on internal and secure networks while using an externally hosted Temporal Web instance, such that provided by Temporal Cloud. ### Configuring Authentication (optional) diff --git a/client/features/data-conversion.js b/client/features/data-conversion.js index 11f0eba4..d70c196f 100644 --- a/client/features/data-conversion.js +++ b/client/features/data-conversion.js @@ -1,6 +1,6 @@ import WebSocketAsPromised from 'websocket-as-promised'; -export const convertEventPayloadsWithRemoteEncoder = async (events, endpoint, accessToken) => { +export const convertEventPayloadsWithCodec = async (events, endpoint, accessToken) => { let headers = { 'Content-Type': 'application/json' }; if (accessToken) { headers['Authorization'] = `Bearer ${accessToken}`; diff --git a/client/routes/workflow/index.vue b/client/routes/workflow/index.vue index bccc774d..ff5187fe 100644 --- a/client/routes/workflow/index.vue +++ b/client/routes/workflow/index.vue @@ -80,7 +80,7 @@ import { import { NOTIFICATION_TYPE_ERROR } from '~constants'; import { getErrorMessage } from '~helpers'; import { NavigationBar, NavigationLink } from '~components'; -import { convertEventPayloadsWithWebsocket, convertEventPayloadsWithRemoteEncoder } from '~features/data-conversion'; +import { convertEventPayloadsWithWebsocket, convertEventPayloadsWithCodec } from '~features/data-conversion'; export default { data() { @@ -139,7 +139,7 @@ export default { )}/${encodeURIComponent(runId)}`; }, historyUrl() { - const rawPayloads = (this.webSettings?.dataConverter?.port || this.webSettings?.dataEncoder?.endpoint) + const rawPayloads = (this.webSettings?.dataConverter?.port || this.webSettings?.codec?.endpoint) ? '&rawPayloads=true' : ''; const historyUrl = `${this.baseAPIURL}/history?waitForNewEvent=true${rawPayloads}`; @@ -205,7 +205,7 @@ export default { }) .then(events => { const port = this.webSettings?.dataConverter?.port; - const endpoint = this.webSettings?.dataEncoder?.endpoint; + const endpoint = this.webSettings?.codec?.endpoint; if (port !== undefined) { return convertEventPayloadsWithWebsocket(events, port).catch(error => { @@ -221,9 +221,9 @@ export default { } if (endpoint !== undefined) { - const accessToken = this.webSettings.dataEncoder.accessToken; + const accessToken = this.webSettings.codec.accessToken; - return convertEventPayloadsWithRemoteEncoder(events, endpoint, accessToken).catch(error => { + return convertEventPayloadsWithCodec(events, endpoint, accessToken).catch(error => { console.error(error); this.$emit('onNotification', { diff --git a/server/config.yml b/server/config.yml index 4b466d40..5a452daf 100644 --- a/server/config.yml +++ b/server/config.yml @@ -17,7 +17,7 @@ routing: default_to_namespace: # internal use only issue_report_link: https://github.com/temporalio/web/issues/new/choose # set this field if you need to direct people to internal support forums -# data_encoder: - # Remote Data Encoder Endpoint - # endpoint: https://remote_encoder.myorg.com - # pass_auth_token: false \ No newline at end of file +# codec: + # Codec Server Endpoint + # endpoint: https://codec.myorg.com + # pass_access_token: false \ No newline at end of file diff --git a/server/config/index.js b/server/config/index.js index db5f0096..5ff4947e 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -4,8 +4,8 @@ const yaml = require('js-yaml'); const logger = require('../logger'); const configPath = process.env.TEMPORAL_CONFIG_PATH || './server/config.yml'; -const dataEncoderEndpoint = process.env.TEMPORAL_DATA_ENCODER_ENDPOINT; -const dataEncoderPassAccessToken = process.env.TEMPORAL_DATA_ENCODER_PASS_ACCESS_TOKEN ? ![false, 'false'].includes(process.env.TEMPORAL_DATA_ENCODER_PASS_ACCESS_TOKEN) : undefined; +const codecEndpointFromEnv = process.env.TEMPORAL_CODEC_ENDPOINT; +const codecPassAccessTokenFromEnv = process.env.TEMPORAL_CODEC_PASS_ACCESS_TOKEN ? ![false, 'false'].includes(process.env.TEMPORAL_CODEC_PASS_ACCESS_TOKEN) : undefined; const readConfigSync = () => { const cfgContents = readFileSync(configPath, { @@ -29,17 +29,15 @@ const getAuthConfig = async () => { return auth; }; -const getDataEncoderConfig = async () => { - let { data_encoder } = await readConfig(); +const getCodecConfig = async () => { + let { codec } = await readConfig(); - // Data encoder endpoint from the environment takes precedence over - // configuration file value. - const dataEncoderConfig = { - endpoint: dataEncoderEndpoint || data_encoder?.endpoint, - passAccessToken: dataEncoderPassAccessToken || !!data_encoder?.pass_auth_token, + const codecConfig = { + endpoint: codecEndpointFromEnv || codec?.endpoint, + passAccessToken: codecPassAccessTokenFromEnv || !!codec?.pass_access_token, } - return dataEncoderConfig; + return codecConfig; } const getRoutingConfig = async () => { @@ -86,7 +84,7 @@ logger.log( module.exports = { getAuthConfig, - getDataEncoderConfig, + getCodecConfig, getRoutingConfig, getTlsConfig, }; diff --git a/server/routes.js b/server/routes.js index 513317c0..0c1e21e5 100644 --- a/server/routes.js +++ b/server/routes.js @@ -3,7 +3,7 @@ const Router = require('koa-router'), moment = require('moment'), losslessJSON = require('lossless-json'), { isWriteApiPermitted } = require('./utils'), - { getAuthConfig, getRoutingConfig, getDataEncoderConfig } = require('./config'), + { getAuthConfig, getRoutingConfig, getCodecConfig } = require('./config'), authRoutes = require('./routes-auth'), { getTemporalClient: tClient } = require('./temporal-client-provider'); @@ -350,26 +350,26 @@ router.post('/api/web-settings/data-converter/:port', async (ctx) => { ctx.status = 200; }); -router.post('/api/web-settings/remote-data-encoder/:endpoint', async (ctx) => { - ctx.session.dataEncoder = { endpoint: ctx.params.endpoint }; +router.post('/api/web-settings/codec/:endpoint', async (ctx) => { + ctx.session.codec = { endpoint: ctx.params.endpoint }; ctx.status = 200; }); router.get('/api/web-settings', async (ctx) => { const routing = await getRoutingConfig(); const { enabled } = await getAuthConfig(); - const dataEncoderConfig = await getDataEncoderConfig(); + const codecConfig = await getCodecConfig(); const permitWriteApi = isWriteApiPermitted(); const dataConverter = ctx.session.dataConverter; - // Encoder endpoint from the session has higher priority than global config. + // Codec endpoint from the session has higher priority than global config. // This is to allow for testing of new remote encoder endpoints. - let dataEncoder = { - endpoint: ctx.session.dataEncoder?.endpoint || dataEncoderConfig.endpoint + let codec = { + endpoint: ctx.session.codec?.endpoint || codecConfig.endpoint }; - if (dataEncoderConfig.passAccessToken && !!ctx.state.user) { - dataEncoder.accessToken = ctx.state.user.accessToken; + if (codecConfig.passAccessToken && !!ctx.state.user) { + codec.accessToken = ctx.state.user.accessToken; } const auth = { enabled }; // only include non-sensitive data @@ -379,7 +379,7 @@ router.get('/api/web-settings', async (ctx) => { auth, permitWriteApi, dataConverter, - dataEncoder, + codec, }; });