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
104 changes: 104 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,107 @@ jobs:

curl -X POST "https://charts.somnial.co/doubleword-control-layer/critical-vulnerabilities?value=${CRITICAL}" || echo "Failed to report critical vulnerabilities"
curl -X POST "https://charts.somnial.co/doubleword-control-layer/high-vulnerabilities?value=${HIGH}" || echo "Failed to report high vulnerabilities"

e2e-test-docker:
runs-on: depot-ubuntu-24.04
needs: build

permissions:
contents: read
packages: read

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install just
uses: extractions/setup-just@v2

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: dashboard/package-lock.json

- name: Install dashboard dependencies
working-directory: ./dashboard
run: npm ci

- name: Install Playwright browsers
working-directory: ./dashboard
run: npx playwright install chromium --only-shell

- name: Install hurl
uses: gacts/install-hurl@v1

- name: Install jwt-cli
run: |
curl -L https://github.com/mike-engel/jwt-cli/releases/latest/download/jwt-linux.tar.gz | tar xz
sudo mv jwt /usr/local/bin/

- name: Install mkcert
run: |
# Install mkcert for certificate generation
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert

# Note, not needed for this workflow, but required by 'just setup'
- name: Install PostgreSQL tools
run: |
sudo apt-get install -y postgresql-client

# Note, not needed for this workflow, but required by 'just setup'
- name: Install Kind
uses: helm/kind-action@v1
with:
install_only: true

# Note, not needed for this workflow, but required by 'just setup'
- name: Install kubectl
uses: azure/setup-kubectl@v4

# Note, not needed for this workflow, but required by 'just setup'
- name: Install Helm
uses: azure/setup-helm@v4

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Compose
uses: docker/setup-compose-action@v1

# Install self-signed certs, check all deps are installed.
- name: Setup development environment
run: just setup

# Run end to end docker-compose tests
- name: Run Docker end-to-end tests
id: e2e-step
run: |
START_TIME=$(date +%s)
just test docker
END_TIME=$(date +%s)
E2E_DURATION=$((END_TIME - START_TIME))
echo "e2e_duration=$E2E_DURATION" >> $GITHUB_OUTPUT
echo "Docker E2E tests completed in ${E2E_DURATION}s"

# Report metrics to Somnial
curl -X POST "https://charts.somnial.co/doubleword-control-layer/e2e-docker-duration?value=${E2E_DURATION}" || true
env:
TAG: ${{ needs.build.outputs.image-tag }}
GOOGLE_ADMIN_EMAIL: ${{ secrets.E2E_ADMIN_EMAIL }}
ALLOWED_EMAIL_DOMAINS: ${{ secrets.E2E_ALLOWED_EMAIL_DOMAINS }}
FQDN: ${{ secrets.E2E_FQDN }}
PROVIDER: ${{ secrets.E2E_PROVIDER }}
CLIENT_ID: ${{ secrets.E2E_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.E2E_CLIENT_SECRET }}
AUTH_URL: ${{ secrets.E2E_AUTH_URL }}
TOKEN_URL: ${{ secrets.E2E_TOKEN_URL }}
REDIRECT_URL: ${{ secrets.E2E_REDIRECT_URL }}
JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Complete with web dashboard, user authentication, and API gateway.

```bash
# Install CLI tools (macOS)
brew install just jwt-cli hurl mkcert sops kind kubectl helm gh
brew install just jwt-cli hurl mkcert kind kubectl helm gh

# Or install manually:
# just: https://github.com/casey/just
Expand Down Expand Up @@ -101,7 +101,7 @@ just jwt <email> # Generate auth token

## CI Metrics

View real-time build and performance metrics for this project [here](https://charts.somnial.co/doubleword-control-layer).
View real-time build and performance metrics for [this project](https://charts.somnial.co/doubleword-control-layer).

## FAQ

Expand Down Expand Up @@ -155,7 +155,7 @@ just down
ensure prepared SQL queries are up to date. Ensure you're using Rust 1.88 or higher (`rustc --version`).

If you see something like "error returned from database: password authentication failed for user "postgres""
then you'll need to change your pg_hba.conf file - see [here](https://stackoverflow.com/a/55039419).
then you'll need to change your [pg_hba.conf file](https://stackoverflow.com/a/55039419).
N.B. I needed to use sudo vim pg_hba.conf and then run `sudo service postgresql restart` afterwards.

**"Test database missing or inaccessible" from check-db, and db-setup doesn't fix it**
Expand Down
6 changes: 3 additions & 3 deletions dashboard/e2e/auth.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class AuthHelper {

constructor(private page: Page) {
// Get admin email from environment variable, with fallback for backward compatibility
this.adminEmail = process.env.ADMIN_EMAIL || "yicheng@doubleword.ai";
this.adminEmail = process.env.ADMIN_EMAIL || "test@doubleword.ai";
}

/**
Expand Down Expand Up @@ -68,12 +68,12 @@ export class AuthHelper {
// Path to the generate-jwt.sh script relative to the dashboard directory
const scriptPath = path.resolve(
__dirname,
"../../../scripts/generate-jwt.sh",
"../../scripts/generate-jwt.sh",
);

// Execute the script with username and password
return execSync(
`USERNAME="${username}" PASSWORD="${password}" ${scriptPath}`,
`EMAIL="${username}" PASSWORD="${password}" ${scriptPath}`,
{
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"], // Capture stderr separately to avoid validation output
Expand Down
16 changes: 10 additions & 6 deletions dashboard/e2e/basic-auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import { test, expect } from "@playwright/test";
import { AuthHelper } from "./auth.helper";

// Get admin email from environment variable, with fallback for backward compatibility
const adminEmail = process.env.ADMIN_EMAIL || "yicheng@doubleword.ai";
const adminEmail = process.env.ADMIN_EMAIL || "test@doubleword.ai";

test.describe("Authentication Flow", () => {
test("should redirect unauthenticated users to login", async ({ page }) => {
// Navigate to the dashboard without authentication via nginx
await page.goto("https://localhost/");
// Navigate to the dashboard without authentication
await page.goto("http://localhost:3001/");

// Should be redirected to OAuth provider (Google in this case)
await expect(page).toHaveURL(/accounts\.google\.com/);
// Should be redirected to login page
await expect(page).toHaveURL(/\/login$/);

// Should see login form
await expect(page.getByText(/sign in/i).first()).toBeVisible();
});

test("admin should have full dashboard access", async ({ page }) => {
Expand All @@ -20,7 +23,7 @@ test.describe("Authentication Flow", () => {
await auth.loginAsAdmin();
await page.goto("/");

// Should reach the dashboard (sso isn't up though so go direct to clay)
// Should reach the dashboard
await expect(page).toHaveURL(/^http:\/\/localhost:3001\/(models)?$/);

// Should see admin user info in the sidebar
Expand All @@ -38,6 +41,7 @@ test.describe("Authentication Flow", () => {
await expect(
page.getByRole("link", { name: /users.*groups/i }),
).toBeVisible();

await expect(page.getByRole("link", { name: /endpoints/i })).toBeVisible();
});

Expand Down
14 changes: 7 additions & 7 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ get-admin-email:
# - Access to doublewordai GCP project (for environment decryption)
#
# First-time setup:
# brew install docker hurl jwt-cli mkcert sops kind kubectl helm gh postgresql
# brew install docker hurl jwt-cli mkcert kind kubectl helm gh postgresql
# gcloud auth login # Required for environment decryption
# just setup
#
Expand All @@ -39,7 +39,7 @@ setup:
missing_tools=()

# Required tools
required_tools=("docker" "hurl" "jwt" "mkcert" "sops" "kind" "kubectl" "helm" "gh" "psql" "createdb")
required_tools=("docker" "hurl" "jwt" "mkcert" "kind" "kubectl" "helm" "gh" "psql" "createdb")
for tool in "${required_tools[@]}"; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing_tools+=("$tool")
Expand All @@ -59,7 +59,7 @@ setup:
done
echo ""
echo "Install with:"
echo " brew install docker hurl jwt-cli mkcert sops kind kubectl helm gh postgresql"
echo " brew install docker hurl jwt-cli mkcert kind kubectl helm gh postgresql"
echo ""
echo "Note: docker compose-plugin is included with Docker Desktop"
echo ""
Expand Down Expand Up @@ -353,8 +353,8 @@ test target="" *args="":
fi

# Generate admin JWT
ADMIN_JWT=$(USERNAME=$ADMIN_EMAIL PASSWORD=$ADMIN_PASSWORD ./scripts/generate-jwt.sh 2>&1)
if [ $? -eq 0 ]; then

if ADMIN_JWT=$(EMAIL=$ADMIN_EMAIL PASSWORD=$ADMIN_PASSWORD ./scripts/generate-jwt.sh); then
echo "admin_jwt=$ADMIN_JWT" > test.env
echo "✅ Admin JWT generated successfully"
else
Expand All @@ -373,7 +373,7 @@ test target="" *args="":

# Generate user JWT
echo "Generating user JWT..."
if USER_JWT=$(USERNAME=user@example.org PASSWORD=user_password ./scripts/generate-jwt.sh); then
if USER_JWT=$(EMAIL=user@example.org PASSWORD=user_password ./scripts/generate-jwt.sh); then
echo "user_jwt=$USER_JWT" >> test.env
echo "✅ User JWT generated successfully"
else
Expand Down Expand Up @@ -618,7 +618,7 @@ jwt:
# Generate cookie for the configured admin user
# Requires ADMIN_PASSWORD environment variable
jwt-admin:
@USERNAME="$(just get-admin-email)" PASSWORD="${ADMIN_PASSWORD}" ./scripts/generate-jwt.sh
@EMAIL="$(just get-admin-email)" PASSWORD="${ADMIN_PASSWORD}" ./scripts/generate-jwt.sh

# Run CI pipeline locally: 'just ci [rust|ts]'
#
Expand Down
2 changes: 1 addition & 1 deletion scripts/drop-test-groups.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if [ -z "$ADMIN_PASSWORD" ]; then
fi

# Generate admin cookie for authentication
ADMIN_JWT=$(USERNAME="$ADMIN_EMAIL" PASSWORD="$ADMIN_PASSWORD" ./scripts/generate-jwt.sh)
ADMIN_JWT=$(EMAIL="$ADMIN_EMAIL" PASSWORD="$ADMIN_PASSWORD" ./scripts/generate-jwt.sh)

if [ -z "$ADMIN_JWT" ]; then
echo "Failed to generate admin JWT" >&2
Expand Down
2 changes: 1 addition & 1 deletion scripts/drop-test-users.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if [ -z "$ADMIN_PASSWORD" ]; then
fi

# Generate admin JWT for authentication
ADMIN_JWT=$(USERNAME=$ADMIN_EMAIL PASSWORD=$ADMIN_PASSWORD ./scripts/generate-jwt.sh 2>/dev/null)
ADMIN_JWT=$(EMAIL=$ADMIN_EMAIL PASSWORD=$ADMIN_PASSWORD ./scripts/generate-jwt.sh 2>/dev/null)

if [ -z "$ADMIN_JWT" ]; then
echo "Failed to generate admin JWT" >&2
Expand Down
19 changes: 10 additions & 9 deletions scripts/generate-jwt.sh
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
#!/bin/bash

# Check if username and password are provided via environment variables
if [ -z "$USERNAME" ]; then
echo "USERNAME environment variable not set" >&2
echo "Usage: USERNAME=user@example.com PASSWORD=yourpassword $0" >&2
# Check if email and password are provided via environment variables
if [ -z "$EMAIL" ]; then
echo "EMAIL environment variable not set" >&2
echo "Usage: EMAIL=user@example.com PASSWORD=yourpassword $0" >&2
exit 1
fi

if [ -z "$PASSWORD" ]; then
echo "PASSWORD environment variable not set" >&2
echo "Usage: USERNAME=user@example.com PASSWORD=yourpassword $0" >&2
echo "Usage: EMAIL=user@example.com PASSWORD=yourpassword $0" >&2
exit 1
fi

# Call the login endpoint and capture the cookie
echo "Attempting login with email: $EMAIL" >&2
RESPONSE=$(curl -s -c - -w "\nHTTP_STATUS:%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-d "{\"email\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" \
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" \
http://localhost:3001/authentication/login 2>/dev/null)

HTTP_STATUS=$(echo "$RESPONSE" | grep -oP 'HTTP_STATUS:\K\d+')
HTTP_STATUS=$(echo "$RESPONSE" | sed -n 's/.*HTTP_STATUS:\([0-9]*\).*/\1/p')

if [ "$HTTP_STATUS" = "200" ]; then
# Extract the cookie value from the response
COOKIE=$(echo "$RESPONSE" | grep -oP 'clay_session\s+\K[^\s]+' | head -1)
COOKIE=$(echo "$RESPONSE" | awk '/clay_session/ {for(i=1;i<=NF;i++) if($i=="clay_session") print $(i+1); exit}')
if [ -n "$COOKIE" ]; then
echo "$COOKIE"
else
Expand All @@ -34,7 +35,7 @@ if [ "$HTTP_STATUS" = "200" ]; then
exit 1
fi
else
echo "❌ Login failed for $USERNAME (HTTP $HTTP_STATUS)" >&2
echo "❌ Login failed for $EMAIL (HTTP $HTTP_STATUS)" >&2
echo "Response body:" >&2
echo "$RESPONSE" | grep -v "HTTP_STATUS:" >&2
exit 1
Expand Down
2 changes: 1 addition & 1 deletion tests/health.hurl
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Health endpoint should return 200 over HTTPS
GET https://localhost/health
GET http://localhost:3001/health
HTTP 200
2 changes: 1 addition & 1 deletion tests/tls.hurl
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# HTTPS should work in production scenario
GET https://localhost/health
GET http://localhost:3001/health
HTTP 200
3 changes: 0 additions & 3 deletions tests/unauthenticated/auth-http-redirect.hurl

This file was deleted.

3 changes: 0 additions & 3 deletions tests/unauthenticated/auth-http-secure-cookie.hurl

This file was deleted.

8 changes: 0 additions & 8 deletions tests/unauthenticated/auth-https-oauth.hurl

This file was deleted.

5 changes: 0 additions & 5 deletions tests/unauthenticated/auth-https-redirect.hurl

This file was deleted.

9 changes: 0 additions & 9 deletions tests/unauthenticated/docs-auth-required.hurl

This file was deleted.