Skip to content
This repository has been archived by the owner on May 26, 2023. It is now read-only.

Commit

Permalink
Support oauth to remote decoder via web oidc token. (#445)
Browse files Browse the repository at this point in the history
* Support oauth to remote decoder via web oidc token.
* Improve terminology and refer to Codec not Remote Encoder.
  • Loading branch information
robholland committed Mar 29, 2022
1 parent f04ba51 commit 550b487
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 47 deletions.
42 changes: 22 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |


<details>
<summary>
Expand Down Expand Up @@ -60,21 +62,21 @@ Setting `TEMPORAL_TLS_REFRESH_INTERVAL` will make the TLS certs reload every N s

</details>

### 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)

Expand Down
10 changes: 8 additions & 2 deletions client/features/data-conversion.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import WebSocketAsPromised from 'websocket-as-promised';

export const convertEventPayloadsWithRemoteEncoder = async (namespace, events, endpointTemplate) => {
const headers = { 'Content-Type': 'application/json', 'X-Namespace': namespace };
export const convertEventPayloadsWithCodec = async (namespace, events, endpointTemplate, accessToken) => {
let headers = {
'Content-Type': 'application/json',
'X-Namespace': namespace,
};
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
const requests = [];
const endpoint = endpointTemplate.replaceAll('{namespace}', namespace);

Expand Down
10 changes: 6 additions & 4 deletions client/routes/workflow/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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}`;
Expand Down Expand Up @@ -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 => {
Expand All @@ -221,7 +221,9 @@ export default {
}
if (endpoint !== undefined) {
return convertEventPayloadsWithRemoteEncoder(this.namespace, events, endpoint).catch(error => {
const accessToken = this.webSettings.codec.accessToken;
return convertEventPayloadsWithCodec(this.namespace, events, endpoint, accessToken).catch(error => {
console.error(error);
this.$emit('onNotification', {
Expand Down
7 changes: 4 additions & 3 deletions server/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +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
# codec:
# Codec Server Endpoint
# endpoint: https://codec.myorg.com
# pass_access_token: false
18 changes: 9 additions & 9 deletions server/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +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 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, {
Expand All @@ -28,16 +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
const codecConfig = {
endpoint: codecEndpointFromEnv || codec?.endpoint,
passAccessToken: codecPassAccessTokenFromEnv || !!codec?.pass_access_token,
}

return dataEncoderConfig;
return codecConfig;
}

const getRoutingConfig = async () => {
Expand Down Expand Up @@ -84,7 +84,7 @@ logger.log(

module.exports = {
getAuthConfig,
getDataEncoderConfig,
getCodecConfig,
getRoutingConfig,
getTlsConfig,
};
22 changes: 13 additions & 9 deletions server/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -350,22 +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 dataEncoder = 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.
if (ctx.session.dataEncoder?.endpoint) {
dataEncoder.endpoint = ctx.session.dataEncoder.endpoint;
let codec = {
endpoint: ctx.session.codec?.endpoint || codecConfig.endpoint
};

if (codecConfig.passAccessToken && !!ctx.state.user) {
codec.accessToken = ctx.state.user.accessToken;
}

const auth = { enabled }; // only include non-sensitive data
Expand All @@ -375,7 +379,7 @@ router.get('/api/web-settings', async (ctx) => {
auth,
permitWriteApi,
dataConverter,
dataEncoder,
codec,
};
});

Expand Down

0 comments on commit 550b487

Please sign in to comment.