diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe88820be..8274c5ccb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 }} diff --git a/README.md b/README.md index 2a8679118..525945e2e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -101,7 +101,7 @@ just jwt # 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 @@ -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** diff --git a/dashboard/e2e/auth.helper.ts b/dashboard/e2e/auth.helper.ts index e7edaf7d0..8236f6518 100644 --- a/dashboard/e2e/auth.helper.ts +++ b/dashboard/e2e/auth.helper.ts @@ -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"; } /** @@ -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 diff --git a/dashboard/e2e/basic-auth.spec.ts b/dashboard/e2e/basic-auth.spec.ts index fc7ddb4bc..030bdc5eb 100644 --- a/dashboard/e2e/basic-auth.spec.ts +++ b/dashboard/e2e/basic-auth.spec.ts @@ -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 }) => { @@ -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 @@ -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(); }); diff --git a/justfile b/justfile index f5294ea4d..617cdec95 100644 --- a/justfile +++ b/justfile @@ -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 # @@ -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") @@ -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 "" @@ -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 @@ -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 @@ -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]' # diff --git a/scripts/drop-test-groups.sh b/scripts/drop-test-groups.sh index 55b7c186c..213ed2400 100755 --- a/scripts/drop-test-groups.sh +++ b/scripts/drop-test-groups.sh @@ -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 diff --git a/scripts/drop-test-users.sh b/scripts/drop-test-users.sh index 8664ef48d..cfadc0fdc 100755 --- a/scripts/drop-test-users.sh +++ b/scripts/drop-test-users.sh @@ -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 diff --git a/scripts/generate-jwt.sh b/scripts/generate-jwt.sh index a2d7c9dde..98606e9e3 100755 --- a/scripts/generate-jwt.sh +++ b/scripts/generate-jwt.sh @@ -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 @@ -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 diff --git a/tests/health.hurl b/tests/health.hurl index 2f2ac5030..5fec22adc 100644 --- a/tests/health.hurl +++ b/tests/health.hurl @@ -1,3 +1,3 @@ # Health endpoint should return 200 over HTTPS -GET https://localhost/health +GET http://localhost:3001/health HTTP 200 \ No newline at end of file diff --git a/tests/tls.hurl b/tests/tls.hurl index 1fd47cd85..f8a1ac7e9 100644 --- a/tests/tls.hurl +++ b/tests/tls.hurl @@ -1,3 +1,3 @@ # HTTPS should work in production scenario -GET https://localhost/health +GET http://localhost:3001/health HTTP 200 \ No newline at end of file diff --git a/tests/unauthenticated/auth-http-redirect.hurl b/tests/unauthenticated/auth-http-redirect.hurl deleted file mode 100644 index 3a415d3b8..000000000 --- a/tests/unauthenticated/auth-http-redirect.hurl +++ /dev/null @@ -1,3 +0,0 @@ -# HTTP root redirects to HTTPS -GET http://localhost/ -HTTP 308 \ No newline at end of file diff --git a/tests/unauthenticated/auth-http-secure-cookie.hurl b/tests/unauthenticated/auth-http-secure-cookie.hurl deleted file mode 100644 index e2668cf83..000000000 --- a/tests/unauthenticated/auth-http-secure-cookie.hurl +++ /dev/null @@ -1,3 +0,0 @@ -# HTTP auth login redirects to HTTPS -GET http://localhost/authentication/login?url=http://localhost/ -HTTP 308 \ No newline at end of file diff --git a/tests/unauthenticated/auth-https-oauth.hurl b/tests/unauthenticated/auth-https-oauth.hurl deleted file mode 100644 index 022633875..000000000 --- a/tests/unauthenticated/auth-https-oauth.hurl +++ /dev/null @@ -1,8 +0,0 @@ -# HTTPS auth login redirects to OAuth provider -GET https://localhost/authentication/login?url=https://localhost/ -HTTP 302 -[Asserts] -# Should have required OAuth parameters -header "Location" contains "client_id=" -header "Location" contains "redirect_uri=" -header "Location" contains "response_type=" diff --git a/tests/unauthenticated/auth-https-redirect.hurl b/tests/unauthenticated/auth-https-redirect.hurl deleted file mode 100644 index 0758fb531..000000000 --- a/tests/unauthenticated/auth-https-redirect.hurl +++ /dev/null @@ -1,5 +0,0 @@ -# HTTPS root redirects to auth login -GET https://localhost/ -HTTP 302 -[Asserts] -header "Location" contains "/authentication/sign_in" \ No newline at end of file diff --git a/tests/unauthenticated/docs-auth-required.hurl b/tests/unauthenticated/docs-auth-required.hurl deleted file mode 100644 index 31dc1deb7..000000000 --- a/tests/unauthenticated/docs-auth-required.hurl +++ /dev/null @@ -1,9 +0,0 @@ -# Test that documentation endpoints require authentication -# Both public and internal docs should redirect unauthenticated users to login - -# Test public docs requires auth -GET https://localhost/admin/docs/ -HTTP 302 -[Asserts] -header "Location" contains "/authentication/sign_in" -header "Location" contains "rd=https://localhost/admin/docs/"