Skip to content
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
6 changes: 6 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ POSTGRES_PASSWORD=

HASURA_GRAPHQL_ADMIN_SECRET=
HASURA_GRAPHQL_JWT_SECRET=

# Optional: comma-separated list of browser cookie names to forward to actions as secrets (e.g. ssosession)
# ACTION_COOKIE_NAMES=

# Optional: allowed CORS origin for the action server. Required for cross-origin deployments with cookie forwarding.
# ACTION_CORS_ALLOWED_ORIGIN=
22 changes: 18 additions & 4 deletions action-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions action-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"dependencies": {
"@nasa-jpl/aerie-actions": "1.1.1",
"cookie": "^1.1.1",
"express": "^5.0.1",
"jsonwebtoken": "9.0.2",
"pg": "^8.13.3",
Expand Down
8 changes: 7 additions & 1 deletion action-server/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import express, { Response } from "express";
import express from "express";
import { configuration } from "./config";
import {authMiddleware, corsMiddleware, jsonErrorMiddleware} from "./middleware";
import { ActionRunner } from "./type/actionRunner";
import { extractCookies } from "./utils/auth";


// init express app and middleware
Expand All @@ -24,8 +26,12 @@ app.post(
const { action_run_id, secrets } = req.body;
const actionRunId = action_run_id as string;

const { ACTION_COOKIE_NAMES } = configuration();
const forwardedCookies = extractCookies(req.headers.cookie ?? '', ACTION_COOKIE_NAMES);

const fullSecrets = {
...secrets,
...forwardedCookies,
authorization: res.locals.authorization,
user: JSON.stringify(res.locals.user),
userRole: res.locals.userRole
Expand Down
6 changes: 4 additions & 2 deletions action-server/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {Algorithm} from "jsonwebtoken";

export interface Config {
AERIE_DB: string;
AERIE_DB_HOST: string;
AERIE_DB_PORT: string;
ACTION_COOKIE_NAMES: string[];
ACTION_CORS_ALLOWED_ORIGIN: string;
ACTION_DB_USER: string;
ACTION_DB_PASSWORD: string;
ACTION_LOCAL_STORE: string;
Expand All @@ -26,6 +26,8 @@ export const configuration = (): Config => {
AERIE_DB: env.AERIE_DB ?? "aerie",
AERIE_DB_HOST: env.AERIE_DB_HOST ?? "postgres",
AERIE_DB_PORT: env.AERIE_DB_PORT ?? "5432",
ACTION_COOKIE_NAMES: (env.ACTION_COOKIE_NAMES ?? '').split(',').map(s => s.trim()).filter(Boolean),
ACTION_CORS_ALLOWED_ORIGIN: env.ACTION_CORS_ALLOWED_ORIGIN ?? "",
ACTION_DB_USER: env.ACTION_DB_USER ?? "",
ACTION_DB_PASSWORD: env.ACTION_DB_PASSWORD ?? "",
ACTION_LOCAL_STORE: env.ACTION_LOCAL_STORE ?? "/usr/src/app/action_file_store",
Expand Down
14 changes: 11 additions & 3 deletions action-server/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ErrorRequestHandler, RequestHandler, NextFunction, Request, Response } from "express";
import { configuration } from "./config";
import {decodeJwt} from "./utils/auth";

// custom error handling middleware so we always return a JSON object for errors
Expand All @@ -12,10 +13,17 @@ export const jsonErrorMiddleware: ErrorRequestHandler = (err, req, res, next) =>
});
};

// temporary CORS middleware to allow access from all origins
// TODO: set more strict CORS rules
export const corsMiddleware: RequestHandler = (req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
const { ACTION_CORS_ALLOWED_ORIGIN } = configuration();

if (ACTION_CORS_ALLOWED_ORIGIN) {
// Explicit origin configured: strict CORS with credentials support
res.setHeader("Access-Control-Allow-Origin", ACTION_CORS_ALLOWED_ORIGIN);
res.setHeader("Access-Control-Allow-Credentials", "true");
} else {
// No origin configured: allow access from all origins but without credentials
res.setHeader("Access-Control-Allow-Origin", "*");
}
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, authorization, x-hasura-role");
next();
Expand Down
19 changes: 19 additions & 0 deletions action-server/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request } from 'express';
import { configuration } from "../config";
import jwt, {Algorithm} from "jsonwebtoken";
import { parseCookie } from "cookie";

export type JsonWebToken = string;

Expand Down Expand Up @@ -63,6 +64,24 @@ export type UserRoles = {
};


/**
* Extracts specified cookies from a cookie header string.
* Returns a record of cookie name to cookie value for matching cookies found in the header.
*/
export function extractCookies(cookieHeader: string, cookieNames: string[]): Record<string, string> {
if (!cookieHeader || cookieNames.length === 0) return {};

const parsedCookies = parseCookie(cookieHeader);
const selectedCookies: Record<string,string> = {};

for (const name of cookieNames) {
if (parsedCookies[name] !== undefined) {
selectedCookies[name] = parsedCookies[name];
}
}
return selectedCookies;
}

export function authorizationHeaderToToken(authorizationHeader: string | undefined | null): JsonWebToken | never {
if (authorizationHeader !== null && authorizationHeader !== undefined) {
if (authorizationHeader.startsWith('Bearer ')) {
Expand Down
52 changes: 52 additions & 0 deletions action-server/tests/app.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,55 @@ test('auth middleware', async () => {
assert.equal(res.status, 401);
});
});

test('cookie forwarding', async () => {
const validToken = jwt.sign({ sub: 'user-123' }, key, { algorithm: 'HS256', expiresIn: '1h' });

await test('should forward configured cookies as secrets', async () => {
process.env.ACTION_COOKIE_NAMES = 'ssosession,other_cookie';
called.length = 0; // reset array, can't re-assign since our mock has a static reference to this array

const res = await request(app)
.post('/secrets')
.send({ action_run_id: 'test-run-1', secrets: { mySecret: 'value' } })
.set('Authorization', `Bearer ${validToken}`)
.set('Cookie', 'ssosession=token123; other_cookie=abc; unrelated=xyz');

assert.equal(res.status, 200);
assert.equal(called.length, 1);
assert.equal(called[0].secrets.ssosession, 'token123');
assert.equal(called[0].secrets.other_cookie, 'abc');
assert.equal(called[0].secrets.unrelated, undefined);
assert.equal(called[0].secrets.mySecret, 'value');
});

await test('should not forward cookies when ACTION_COOKIE_NAMES is unset', async () => {
delete process.env.ACTION_COOKIE_NAMES;
called.length = 0;

const res = await request(app)
.post('/secrets')
.send({ action_run_id: 'test-run-2', secrets: {} })
.set('Authorization', `Bearer ${validToken}`)
.set('Cookie', 'ssosession=token123');

assert.equal(res.status, 200);
assert.equal(called.length, 1);
assert.equal(called[0].secrets.ssosession, undefined);
});

await test('should handle cookies with equals signs in values', async () => {
process.env.ACTION_COOKIE_NAMES = 'ssosession';
called.length = 0;

const res = await request(app)
.post('/secrets')
.send({ action_run_id: 'test-run-3', secrets: {} })
.set('Authorization', `Bearer ${validToken}`)
.set('Cookie', 'ssosession=base64value==;');

assert.equal(res.status, 200);
assert.equal(called.length, 1);
assert.equal(called[0].secrets.ssosession, 'base64value==');
});
});
8 changes: 7 additions & 1 deletion deployment/.env
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ HASURA_GRAPHQL_JWT_SECRET=
POSTGRES_USER=
POSTGRES_PASSWORD=

# Optionally define the host PlanDev will run on, if you need HTTPS / TLS
# Optional: defines the host PlanDev will run on, if you need HTTPS / TLS
# See the deployment/proxy folder for more info
# AERIE_HOST=

# Optional: comma-separated list of browser cookie names to forward to actions as secrets (e.g. ssosession)
# ACTION_COOKIE_NAMES=

# Optional: allowed CORS origin for the action server. Required for cross-origin deployments with cookie forwarding.
# ACTION_CORS_ALLOWED_ORIGIN=
17 changes: 17 additions & 0 deletions deployment/Environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This document provides detailed information about environment variables for each
- [PlanDev Merlin](#plandev-merlin)
- [PlanDev Scheduler](#plandev-scheduler)
- [PlanDev Sequencing](#plandev-sequencing)
- [PlanDev Action Server](#plandev-action-server)
- [PlanDev UI](#plandev-ui)
- [Hasura](#hasura)
- [Postgres](#postgres)
Expand Down Expand Up @@ -86,6 +87,22 @@ See the [environment variables document](https://github.com/NASA-AMMOS/aerie-gat
| `SEQUENCING_SERVER_PORT` | Port the server listens on | `number` | 27184 |
| `SEQUENCING_LANGUAGE` | The language that sequences are generated in when using templates ("SEQN", "STOL", or "TEXT") | `string` | "SEQN" |

## PlanDev Action Server

| Name | Description | Type | Default |
|-------------------------------|-------------------------------------------------------------------------------------------------------------------|----------|---------|
| `ACTION_COOKIE_NAMES` | Comma-separated list of browser cookie names to extract and forward to actions as secrets (e.g. `ssosession`) | `string` | |
| `ACTION_CORS_ALLOWED_ORIGIN` | Allowed CORS origin for the action server. If unset, reflects the request's `Origin` header | `string` | |
| `ACTION_DB_USER` | Username of the Action Server DB User | `string` | |
| `ACTION_DB_PASSWORD` | Password of the Action Server DB User | `string` | |
| `ACTION_LOCAL_STORE` | Local storage for the action server in the container | `string` | /usr/src/app/action_file_store |
| `ACTION_WORKER_NUM` | Number of action worker threads | `number` | 1 |
| `ACTION_MAX_WORKER_NUM` | Maximum number of action worker threads | `number` | 1 |
| `HASURA_GRAPHQL_JWT_SECRET` | The JWT secret for JSON web token auth | `string` | |
| `LOG_FILE` | Either an output filepath to log to, or 'console' | `string` | console |
| `LOG_LEVEL` | Logging level for filtering logs | `string` | debug |
| `MERLIN_GRAPHQL_URL` | URI of the PlanDev GraphQL API | `string` | http://hasura:8080/v1/graphql |

## PlanDev UI

See the [environment variables document](https://github.com/NASA-AMMOS/aerie-ui/blob/develop/docs/ENVIRONMENT.md) in the PlanDev UI repository.
Expand Down
3 changes: 3 additions & 0 deletions deployment/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ services:
MERLIN_GRAPHQL_URL: http://hasura:8080/v1/graphql
AERIE_DB_HOST: postgres
AERIE_DB_PORT: 5432
ACTION_COOKIE_NAMES: "${ACTION_COOKIE_NAMES}"
ACTION_CORS_ALLOWED_ORIGIN: "${ACTION_CORS_ALLOWED_ORIGIN}"
ACTION_DB_USER: "${SEQUENCING_USERNAME}"
ACTION_DB_PASSWORD: "${SEQUENCING_PASSWORD}"
ACTION_LOCAL_STORE: /usr/src/app/action_file_store
Expand Down Expand Up @@ -190,6 +192,7 @@ services:
ORIGIN: http://localhost
PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/"
PUBLIC_ACTION_CLIENT_URL: http://localhost:27186
PUBLIC_ACTION_INCLUDE_CREDENTIALS: "false"
PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000
PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000
PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql
Expand Down
12 changes: 12 additions & 0 deletions deployment/kubernetes/aerie-action-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ spec:
value: "5432"
- name: AERIE_DB_HOST
value: postgres
- name: ACTION_COOKIE_NAMES
valueFrom:
secretKeyRef:
name: dev-env
key: ACTION_COOKIE_NAMES
optional: true
- name: ACTION_CORS_ALLOWED_ORIGIN
valueFrom:
secretKeyRef:
name: dev-env
key: ACTION_CORS_ALLOWED_ORIGIN
optional: true
- name: ACTION_LOCAL_STORE
value: /usr/src/app/action_file_store
- name: ACTION_DB_USER
Expand Down
6 changes: 6 additions & 0 deletions deployment/kubernetes/aerie-ui-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ spec:
value: http://127.0.0.1
- name: PUBLIC_AERIE_FILE_STORE_PREFIX
value: /usr/src/app/merlin_file_store/
- name: PUBLIC_ACTION_INCLUDE_CREDENTIALS
valueFrom:
secretKeyRef:
name: dev-env
key: PUBLIC_ACTION_INCLUDE_CREDENTIALS
optional: true
- name: PUBLIC_GATEWAY_CLIENT_URL
value: http://localhost:9000
- name: PUBLIC_GATEWAY_SERVER_URL
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ services:
MERLIN_GRAPHQL_URL: http://hasura:8080/v1/graphql
AERIE_DB_HOST: postgres
AERIE_DB_PORT: 5432
ACTION_COOKIE_NAMES: "${ACTION_COOKIE_NAMES}"
ACTION_CORS_ALLOWED_ORIGIN: "${ACTION_CORS_ALLOWED_ORIGIN}"
ACTION_DB_USER: "${SEQUENCING_USERNAME}"
ACTION_DB_PASSWORD: "${SEQUENCING_PASSWORD}"
ACTION_LOCAL_STORE: /usr/src/app/action_file_store
Expand Down Expand Up @@ -163,6 +165,7 @@ services:
PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/"
ORIGIN: http://localhost
PUBLIC_ACTION_CLIENT_URL: http://localhost:27186
PUBLIC_ACTION_INCLUDE_CREDENTIALS: "false"
PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000
PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000
PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql
Expand Down
3 changes: 3 additions & 0 deletions e2e-tests/docker-compose-many-workers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ services:
ACTION_SERVER_PORT: 27186
AERIE_DB_HOST: postgres
AERIE_DB_PORT: 5432
ACTION_COOKIE_NAMES: "${ACTION_COOKIE_NAMES}"
ACTION_CORS_ALLOWED_ORIGIN: "${ACTION_CORS_ALLOWED_ORIGIN}"
ACTION_DB_USER: "${SEQUENCING_USERNAME}"
ACTION_DB_PASSWORD: "${SEQUENCING_PASSWORD}"
ACTION_LOCAL_STORE: /usr/src/app/action_file_store
Expand Down Expand Up @@ -158,6 +160,7 @@ services:
PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/"
ORIGIN: http://localhost
PUBLIC_ACTION_CLIENT_URL: http://localhost:27186
PUBLIC_ACTION_INCLUDE_CREDENTIALS: "false"
PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000
PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000
PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql
Expand Down
3 changes: 3 additions & 0 deletions e2e-tests/docker-compose-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ services:
MERLIN_GRAPHQL_URL: http://hasura:8080/v1/graphql
AERIE_DB_HOST: postgres
AERIE_DB_PORT: 5432
ACTION_COOKIE_NAMES: "${ACTION_COOKIE_NAMES}"
ACTION_CORS_ALLOWED_ORIGIN: "${ACTION_CORS_ALLOWED_ORIGIN}"
ACTION_DB_USER: "${SEQUENCING_USERNAME}"
ACTION_DB_PASSWORD: "${SEQUENCING_PASSWORD}"
ACTION_LOCAL_STORE: /usr/src/app/action_file_store
Expand Down Expand Up @@ -156,6 +158,7 @@ services:
ORIGIN: http://localhost
PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/"
PUBLIC_ACTION_CLIENT_URL: http://localhost:27186
PUBLIC_ACTION_INCLUDE_CREDENTIALS: "false"
PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000
PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000
PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql
Expand Down
Loading