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

Data encoder config #429

Merged
merged 7 commits into from
Feb 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Set these environment variables if you need to change their defaults
| 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 | |

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

</details>

### Configuring Remote Data Encoder (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:

1. Edit the `server/config.yml` file:

```yaml
data_encoder:
endpoint: https://remote_encoder.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.

Temporal Web will then configure it's UI to decode Payloads as appropriate via the remote data encoder.

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.

### Configuring Authentication (optional)

**Note** For proper security, your server needs to be secured as well and validate the JWT tokens that Temporal Web will be sending to server once users are authenticated. See [security docs](https://docs.temporal.io/docs/server/security/#authorization) for details
Expand Down
39 changes: 22 additions & 17 deletions client/features/data-conversion.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,36 @@ export const convertEventPayloadsWithRemoteEncoder = async (events, endpoint) =>
const requests = [];

events.forEach(event => {
let payloads = [];
let payloadsWrapper;

if (event.details.input) {
payloads = event.details.input.payloads;
payloadsWrapper = event.details.input;
} else if (event.details.result) {
payloads = event.details.result.payloads;
payloadsWrapper = event.details.result;
}

payloads.forEach((payload, i) => {
requests.push(
fetch(`${endpoint}/decode`, { method: 'POST', headers: headers, body: JSON.stringify(payload) })
if (!payloadsWrapper) {
return;
}

requests.push(
fetch(`${endpoint}/decode`, { method: 'POST', headers: headers, body: JSON.stringify(payloadsWrapper) })

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought we should make sure wherever this is happening has http2.1 otherwise we'll only be able to decode 8 things at a time which would severely block rendering.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server aspect will be up to the users, it's not something we control. We can document that it should support http2 though.

.then((response) => response.json())
.then((decodedPayload) => {
let data = window.atob(decodedPayload.data);
try {
payloads[i] = JSON.parse(data);
} catch {
payloads[i] = data;
}
.then((decodedPayloadsWrapper) => decodedPayloadsWrapper.payloads)
.then((decodedPayloads) => {
decodedPayloads.forEach((payload, i) => {
let data = window.atob(payload.data);
try {
payloadsWrapper.payloads[i] = JSON.parse(data);
} catch {
payloadsWrapper.payloads[i] = data;
}
});
})
)
});
});
)
})

await Promise.allSettled(requests)
await Promise.allSettled(requests);

return events;
};
Expand Down
4 changes: 2 additions & 2 deletions client/routes/workflow/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export default {
)}/${encodeURIComponent(runId)}`;
},
historyUrl() {
const rawPayloads = (this.webSettings?.dataConverter?.port || this.webSettings?.remoteDataEncoder?.endpoint)
const rawPayloads = (this.webSettings?.dataConverter?.port || this.webSettings?.dataEncoder?.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?.remoteDataEncoder?.endpoint;
const endpoint = this.webSettings?.dataEncoder?.endpoint;

if (port !== undefined) {
return convertEventPayloadsWithWebsocket(events, port).catch(error => {
Expand Down
4 changes: 4 additions & 0 deletions server/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ auth:
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
14 changes: 14 additions & 0 deletions server/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 readConfigSync = () => {
const cfgContents = readFileSync(configPath, {
Expand All @@ -27,6 +28,18 @@ const getAuthConfig = async () => {
return auth;
};

const getDataEncoderConfig = async () => {
let { data_encoder } = await readConfig();

// Data encoder endpoint from the environment takes precedence over
// configuration file value.
const dataEncoderConfig = {
endpoint: dataEncoderEndpoint || data_encoder?.endpoint
}

return dataEncoderConfig;
}

const getRoutingConfig = async () => {
const { routing } = await readConfig();

Expand Down Expand Up @@ -71,6 +84,7 @@ logger.log(

module.exports = {
getAuthConfig,
getDataEncoderConfig,
getRoutingConfig,
getTlsConfig,
};
14 changes: 10 additions & 4 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 } = require('./config'),
{ getAuthConfig, getRoutingConfig, getDataEncoderConfig } = require('./config'),
authRoutes = require('./routes-auth'),
{ getTemporalClient: tClient } = require('./temporal-client-provider');

Expand Down Expand Up @@ -351,16 +351,22 @@ router.post('/api/web-settings/data-converter/:port', async (ctx) => {
});

router.post('/api/web-settings/remote-data-encoder/:endpoint', async (ctx) => {
ctx.session.remoteDataEncoder = { endpoint: ctx.params.endpoint };
ctx.session.dataEncoder = { 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 permitWriteApi = isWriteApiPermitted();
const dataConverter = ctx.session.dataConverter;
const remoteDataEncoder = ctx.session.remoteDataEncoder;

// 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;
}

const auth = { enabled }; // only include non-sensitive data

Expand All @@ -369,7 +375,7 @@ router.get('/api/web-settings', async (ctx) => {
auth,
permitWriteApi,
dataConverter,
remoteDataEncoder,
dataEncoder,
};
});

Expand Down