Compare commits

..
Author SHA1 Message Date
Viktor Liu e8c6d44452 Add transparent proxy inspection policies 2026-04-18 13:13:18 +02:00
683 changed files with 7342 additions and 66780 deletions

No files matched your search

-7
View File
@@ -10,10 +10,3 @@ Select exactly one:
Paste the PR link from https://github.com/netbirdio/docs here:
https://github.com/netbirdio/docs/pull/__
## E2E tests
Optional: override the image tags used by the Playwright e2e workflow.
Defaults to `main` when omitted.
management-cloud-tag: main
reverse-proxy-tag: main
+13 -42
View File
@@ -7,24 +7,17 @@ on:
- "**"
pull_request:
# Cancel in-progress runs on the same ref (PR or branch) when a new commit
# arrives, so we don't waste CI building superseded commits.
concurrency:
group: build-and-push-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DOCKERHUB_IMAGE: netbirdio/dashboard
GHCR_IMAGE: ghcr.io/netbirdio/dashboard-cloud
IMAGE_NAME: netbirdio/dashboard
jobs:
build_n_push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v3
- name: setup-node
uses: actions/setup-node@v5
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
@@ -76,43 +69,25 @@ jobs:
NEXT_PUBLIC_DASHBOARD_VERSION: ${{ steps.version.outputs.version }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v4
with:
images: |
${{ env.DOCKERHUB_IMAGE }}
${{ env.GHCR_IMAGE }}
flavor: |
latest=false
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=ref,event=pr
type=sha
- name: Log in to Docker Hub
uses: docker/login-action@v3
images: ${{ env.IMAGE_NAME }}
-
name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.NB_DOCKER_USER }}
password: ${{ secrets.NB_DOCKER_TOKEN }}
- name: Log in to the GitHub Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
-
name: Docker build and push
uses: docker/build-push-action@v3
with:
context: .
file: docker/Dockerfile
@@ -120,7 +95,3 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- run: |
echo '### Pushed tags' >> $GITHUB_STEP_SUMMARY
echo '${{ steps.meta.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
-162
View File
@@ -1,162 +0,0 @@
name: Playwright E2E Tests
on:
push:
branches: [main]
pull_request:
# `edited` is included so that updating the PR description (e.g. to set
# `management-cloud-tag: <tag>` or `reverse-proxy-tag: <tag>`) re-triggers
# the e2e run with the new tag.
types: [opened, synchronize, reopened, edited]
workflow_dispatch:
inputs:
management-cloud-tag:
description: 'Management Cloud image tag'
required: true
type: string
default: 'main'
reverse-proxy-tag:
description: 'Reverse Proxy image tag'
required: true
type: string
default: 'main'
env:
REGISTRY: ghcr.io
jobs:
playwright-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
fetch-depth: 0
- name: setup-node
uses: actions/setup-node@v5
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Log in to the Container registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.CI_DOCKER_PULL_GITHUB_TOKEN }}
- run: echo '{}' > .local-config.json
- name: Install jq
run: sudo apt-get install jq
- name: Resolve management-cloud image tag
id: management_tag
env:
INPUT_TAG: ${{ inputs.management-cloud-tag }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# Use workflow_dispatch input if provided, otherwise parse PR body.
# Falls back to `main` when not specified.
if [ -n "$INPUT_TAG" ]; then
TAG="$INPUT_TAG"
else
TAG=$(printf '%s' "$PR_BODY" \
| grep -iE '^[[:space:]]*management-cloud-tag:[[:space:]]*[A-Za-z0-9._-]+[[:space:]]*$' \
| head -n1 \
| sed -E 's/^[[:space:]]*[Mm]anagement-cloud-tag:[[:space:]]*([A-Za-z0-9._-]+)[[:space:]]*$/\1/' \
|| true)
if [ -z "$TAG" ]; then
TAG="main"
fi
fi
echo "Using management-cloud tag: $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Resolve reverse-proxy image tag
id: reverse_proxy_tag
env:
INPUT_TAG: ${{ inputs.reverse-proxy-tag }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
if [ -n "$INPUT_TAG" ]; then
TAG="$INPUT_TAG"
else
TAG=$(printf '%s' "$PR_BODY" \
| grep -iE '^[[:space:]]*reverse-proxy-tag:[[:space:]]*[A-Za-z0-9._-]+[[:space:]]*$' \
| head -n1 \
| sed -E 's/^[[:space:]]*[Rr]everse-proxy-tag:[[:space:]]*([A-Za-z0-9._-]+)[[:space:]]*$/\1/' \
|| true)
if [ -z "$TAG" ]; then
TAG="main"
fi
fi
echo "Using reverse-proxy tag: $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Setup test environment
env:
MANAGEMENT_IMAGE_TAG: ${{ steps.management_tag.outputs.tag }}
REVERSE_PROXY_IMAGE_TAG: ${{ steps.reverse_proxy_tag.outputs.tag }}
run: cd ./e2e/environment && bash create-test-env.sh
- name: Run Playwright tests
id: playwright
run: |
set -o pipefail
npm run test:ci 2>&1 | tee playwright-output.log
- name: Append Playwright summary to job summary
if: always() && hashFiles('e2e/test-results/results.json') != ''
run: |
if [ -f e2e/test-results/results.json ]; then
passed=$(jq '.stats.expected // 0' e2e/test-results/results.json)
failed=$(jq '.stats.unexpected // 0' e2e/test-results/results.json)
skipped=$(jq '.stats.skipped // 0' e2e/test-results/results.json)
duration=$(jq '.stats.duration // 0' e2e/test-results/results.json)
{
echo '### Playwright results'
echo ''
echo "| Passed | Failed | Skipped | Duration |"
echo "|--------|--------|---------|----------|"
echo "| $passed | $failed | $skipped | ${duration}ms |"
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Collect container logs
if: failure()
run: |
cd e2e/environment
docker compose logs management --tail=500 --no-color > management.log 2>&1 || true
docker compose logs reverse-proxy --tail=500 --no-color > reverse-proxy.log 2>&1 || true
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report
path: e2e/playwright-report/
- uses: actions/upload-artifact@v7
if: ${{ failure() }}
with:
name: playwright-traces
path: e2e/test-results/
- uses: actions/upload-artifact@v7
if: ${{ failure() }}
with:
name: management-logs
path: e2e/environment/management.log
- uses: actions/upload-artifact@v7
if: ${{ failure() }}
with:
name: reverse-proxy-logs
path: e2e/environment/reverse-proxy.log
+2 -7
View File
@@ -36,14 +36,9 @@ yarn-error.log*
next-env.d.ts
# config
.local*config*.json
.local-config.json
.test-config.json
e2e/playwright.env.json
e2e/fixtures/auth/*.json
e2e/test-results/
e2e/playwright-report/
/test-results/
/playwright-report/
cypress.env.json
.configs/.local-config.zitadel.json
.configs/.staging-config.json
.configs/.temp-config.json
+1 -1
View File
@@ -1,3 +1,3 @@
Mikhail Bragin (https://github.com/braginini)
Maycon Santos (https://github.com/mlsmaycon)
NetBird GmbH
Wiretrustee UG (haftungsbeschränkt)
+3 -3
View File
@@ -1,8 +1,8 @@
[
{
"tag": "Preview",
"text": "The new NetBird desktop app is here - now available as a 0.75 release candidate.",
"link": "https://github.com/netbirdio/netbird/discussions/6483",
"tag": "New",
"text": "NetBird Reverse Proxy - Expose internal services to the public with automatic TLS and optional authentication.",
"link": "https://docs.netbird.io/manage/reverse-proxy",
"linkText": "Learn more",
"variant": "important",
"isExternal": true,
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env bash
#
# Build the netbird-dashboard Docker image for linux/amd64 and export it as a
# portable tarball that can be loaded and run on a remote server.
#
# The dashboard image is nginx serving a Next.js *static export*: the Dockerfile
# only COPYs the pre-built out/ directory, so we build out/ on the host first
# (the static files are arch-independent) and then assemble the amd64 image.
#
# Usage:
# ./build.sh # build + export netbird-dashboard.tar.gz
# IMAGE_TAG=v1 ./build.sh # override the image tag
# SKIP_BUILD=1 ./build.sh # reuse an existing out/ (skip npm build)
#
set -euo pipefail
# Always run relative to this script's directory (the project root).
cd "$(dirname "$0")"
IMAGE_NAME="${IMAGE_NAME:-netbird-dashboard}"
IMAGE_TAG="${IMAGE_TAG:-amd64}"
IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
OUTPUT="${OUTPUT:-${IMAGE_NAME}.tar.gz}"
# Build the static export (out/) unless told to reuse an existing one. Next 16
# no longer runs ESLint during `next build`; types are checked by the build via
# tsc (verify separately with `npx tsc --noEmit`).
if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
if [[ ! -d node_modules ]]; then
echo "==> Installing dependencies (npm ci)"
npm ci
fi
echo "==> Building static export (next build) into out/"
npx next build
fi
if [[ ! -d out ]]; then
echo "ERROR: out/ not found — the static export did not build." >&2
exit 1
fi
# The .dockerignore lives under docker/, but Docker reads it from the build
# context root. Stage it for the duration of the build so node_modules/.next
# don't get shipped into the build context.
cleanup() { rm -f .dockerignore; }
trap cleanup EXIT
cp docker/.dockerignore .dockerignore
echo "==> Building ${IMAGE} for linux/amd64"
docker build \
--platform linux/amd64 \
-f docker/Dockerfile \
-t "${IMAGE}" \
.
echo "==> Exporting ${IMAGE} to ${OUTPUT}"
docker save "${IMAGE}" | gzip > "${OUTPUT}"
echo "==> Done: ${OUTPUT} ($(du -h "${OUTPUT}" | cut -f1))"
echo
echo "Copy and run on the server:"
echo " scp ${OUTPUT} user@your-server:/tmp/"
echo " docker load < /tmp/${OUTPUT}"
echo
echo "Run (nginx serves on container port 80). Set the runtime env vars the"
echo "dashboard templates at startup — for the Agent Network onboarding test,"
echo "flip NETBIRD_AGENT_NETWORK_ONLY=true:"
echo " docker run -d --name ${IMAGE_NAME} --restart unless-stopped \\"
echo " -p 8080:80 \\"
echo " -e NETBIRD_MGMT_API_ENDPOINT=https://your-mgmt:443 \\"
echo " -e AUTH_AUTHORITY=https://your-idp/realms/netbird \\"
echo " -e AUTH_CLIENT_ID=netbird-dashboard \\"
echo " -e AUTH_AUDIENCE=netbird-dashboard \\"
echo " -e AUTH_SUPPORTED_SCOPES='openid profile email' \\"
echo " -e USE_AUTH0=false \\"
echo " -e NETBIRD_AGENT_NETWORK_ONLY=true \\"
echo " ${IMAGE}"
+1 -11
View File
@@ -14,15 +14,5 @@
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
"authServiceUrl": "$NETBIRD_AUTH_SERVICE_URL",
"wasmPath": "$NETBIRD_WASM_PATH",
"licensed": "$NETBIRD_LICENSED",
"cloud": "$NETBIRD_CLOUD",
"agentNetworkOnly": "$NETBIRD_AGENT_NETWORK_ONLY",
"agentNetworkEnabled": "$NETBIRD_AGENT_NETWORK_ENABLED",
"hubspotPortalId": "$NETBIRD_HUBSPOT_PORTAL_ID",
"hubspotSignupFormId": "$NETBIRD_HUBSPOT_SIGNUP_FORM_ID",
"hubspotOnboardingFormId": "$NETBIRD_HUBSPOT_ONBOARDING_FORM_ID",
"hubspotSurveyFormId": "$NETBIRD_HUBSPOT_SURVEY_FORM_ID",
"analyticsExcludedEmails": "$NETBIRD_ANALYTICS_EXCLUDED_EMAILS"
"wasmPath": "$NETBIRD_WASM_PATH"
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
},
component: {
devServer: {
framework: "next",
bundler: "webpack",
},
},
viewportWidth: 1920,
viewportHeight: 1080,
});
+13
View File
@@ -0,0 +1,13 @@
describe("Click all tabs in peer modal", () => {
it("passes", () => {
cy.visit("/install");
cy.get("div").contains("Linux").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Windows").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Android").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Docker").click();
cy.get("[data-cy=copy-to-clipboard]").click();
});
});
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
+37
View File
@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
+20
View File
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"baseUrl": "http://localhost:3000",
"types": ["cypress", "node"],
},
"include": ["**/*.ts"]
}
-12
View File
@@ -1,12 +0,0 @@
# The image only needs the pre-built static export (out/) and the docker/
# config files. Keep the build context small by dropping everything heavy and
# irrelevant — without this, `docker build .` ships node_modules and .next.
node_modules
.next
.git
.github
e2e
src
public
*.tar.gz
*.tar
+2 -12
View File
@@ -14,24 +14,14 @@ server {
location / {
try_files $uri $uri.html $uri/ =404;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss: https:;" always;
add_header Last-Modified "";
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
expires off;
}
error_page 404 /404.html;
location = /404.html {
internal;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss: https:;" always;
add_header Last-Modified "";
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
expires off;
}
}
+2 -120
View File
@@ -61,109 +61,12 @@ export NETBIRD_GOOGLE_ANALYTICS_ID=${NETBIRD_GOOGLE_ANALYTICS_ID}
export NETBIRD_GOOGLE_TAG_MANAGER_ID=${NETBIRD_GOOGLE_TAG_MANAGER_ID}
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
export NETBIRD_AUTH_SERVICE_URL=${NETBIRD_AUTH_SERVICE_URL}
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH}
export NETBIRD_CSP=${NETBIRD_CSP}
export NETBIRD_LICENSED=${NETBIRD_LICENSED:-false}
export NETBIRD_CLOUD=${NETBIRD_CLOUD:-false}
export NETBIRD_AGENT_NETWORK_ONLY=${NETBIRD_AGENT_NETWORK_ONLY:-false}
export NETBIRD_AGENT_NETWORK_ENABLED=${NETBIRD_AGENT_NETWORK_ENABLED:-false}
export NETBIRD_HUBSPOT_PORTAL_ID=${NETBIRD_HUBSPOT_PORTAL_ID}
export NETBIRD_HUBSPOT_SIGNUP_FORM_ID=${NETBIRD_HUBSPOT_SIGNUP_FORM_ID}
export NETBIRD_HUBSPOT_ONBOARDING_FORM_ID=${NETBIRD_HUBSPOT_ONBOARDING_FORM_ID}
export NETBIRD_HUBSPOT_SURVEY_FORM_ID=${NETBIRD_HUBSPOT_SURVEY_FORM_ID}
export NETBIRD_ANALYTICS_EXCLUDED_EMAILS=${NETBIRD_ANALYTICS_EXCLUDED_EMAILS}
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# Build CSP
FIRST_PARTY_CSP="pkgs.netbird.io"
FIRST_PARTY_CSP_CONNECT_SRC="wss://*.netbird.io"
THIRD_PARTY_CSP="*.licdn.com *.linkedin.com *.vector.co *.sibforms.com *.hotjar.com *.hotjar.io *.redditstatic.com pixel-config.reddit.com *.clarity.ms c.bing.com *.microsoft.com googleads.g.doubleclick.net pagead2.googlesyndication.com www.google.com www.googleadservices.com *.google-analytics.com *.googletagmanager.com analytics.google.com *.hubapi.com *.hs-banner.com *.hubspot.com *.hubspot.net js.hs-analytics.com *.hsforms.net *.hscollectedforms.net *.hs-analytics.net *.hsforms.com track.hubspot.com *.hsadspixel.net static.hsappstatic.net"
THIRD_PARTY_CSP_CONNECT_SRC="https://api.github.com/repos/netbirdio/netbird/releases/latest https://raw.githubusercontent.com/netbirdio/dashboard/ wss://ws.hotjar.com"
THIRD_PARTY_CSP_SCRIPT_SRC="'sha256-7knV6EIjKUvCpYWE2rCYx8dYV2WCNb2bpTuitFXzBcA=' *.hs-scripts.com"
CSP_DOMAINS=""
CSP_DOMAINS_CONNECT_SRC=""
if [[ -n "${NETBIRD_CSP}" ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_CSP"
fi
# Add AUTH_AUTHORITY to CSP
if [[ -n "${AUTH_AUTHORITY}" ]]; then
CSP_DOMAINS="$CSP_DOMAINS $AUTH_AUTHORITY"
AUTH_AUTHORITY_ORIGIN=$(echo "$AUTH_AUTHORITY" | sed -E 's|^(https?://[^/]+).*|\1|')
CSP_DOMAINS="$CSP_DOMAINS $AUTH_AUTHORITY_ORIGIN"
fi
# Add AUTH_AUDIENCE to CSP
if [[ -n "${AUTH_AUDIENCE}" && ("${AUTH_AUDIENCE}" == *"http://"* || "${AUTH_AUDIENCE}" == *"https://"*) ]]; then
CSP_DOMAINS="$CSP_DOMAINS $AUTH_AUDIENCE"
fi
# Add NETBIRD_AUTH_SERVICE_URL to CSP
if [[ -n "${NETBIRD_AUTH_SERVICE_URL}" ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_AUTH_SERVICE_URL"
fi
# Add NETBIRD_MGMT_API_ENDPOINT to CSP
if [[ -n "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
MGMT_DOMAIN=$(echo "$NETBIRD_MGMT_API_ENDPOINT" | sed -E 's|https?://||' | cut -d'/' -f1 | cut -d':' -f1)
if [[ -n "$MGMT_DOMAIN" ]]; then
if [[ "$NETBIRD_MGMT_API_ENDPOINT" == https://* ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_MGMT_API_ENDPOINT"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC wss://$MGMT_DOMAIN"
elif [[ "$NETBIRD_MGMT_API_ENDPOINT" == http://* ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_MGMT_API_ENDPOINT"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC ws://$MGMT_DOMAIN"
fi
fi
fi
# Add LETSENCRYPT_DOMAIN to CSP
if [[ -n "${LETSENCRYPT_DOMAIN}" ]]; then
if [[ "$LETSENCRYPT_DOMAIN" == *"localhost"* ]]; then
CSP_DOMAINS="$CSP_DOMAINS http://$LETSENCRYPT_DOMAIN"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC ws://$LETSENCRYPT_DOMAIN"
else
CSP_DOMAINS="$CSP_DOMAINS https://$LETSENCRYPT_DOMAIN"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC wss://$LETSENCRYPT_DOMAIN"
fi
fi
CSP_CONNECT_SRC="$CSP_DOMAINS $CSP_DOMAINS_CONNECT_SRC $FIRST_PARTY_CSP $FIRST_PARTY_CSP_CONNECT_SRC $THIRD_PARTY_CSP $THIRD_PARTY_CSP_CONNECT_SRC"
CSP_FRAME_SRC="$CSP_DOMAINS $FIRST_PARTY_CSP $THIRD_PARTY_CSP"
CSP_SCRIPT_SRC="$CSP_DOMAINS $FIRST_PARTY_CSP $THIRD_PARTY_CSP $THIRD_PARTY_CSP_SCRIPT_SRC"
# Remove duplicates
CSP_CONNECT_SRC=$(echo $CSP_CONNECT_SRC | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')
CSP_FRAME_SRC=$(echo $CSP_FRAME_SRC | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')
CSP_SCRIPT_SRC=$(echo $CSP_SCRIPT_SRC | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')
# upgrade-insecure-requests tells the browser to rewrite every http subresource,
# fetch and WebSocket to https/wss. That is correct for https deployments but breaks
# plain-http ones (e.g. local or self-hosted over http), where it would rewrite the
# working http/ws endpoints to unreachable https/wss. Emit it only when the backend
# is served over https, matching the ws:// vs wss:// scheme logic above.
CSP_UPGRADE_INSECURE=" upgrade-insecure-requests;"
if [[ "$NETBIRD_MGMT_API_ENDPOINT" == http://* || "$AUTH_AUTHORITY" == http://* ]]; then
CSP_UPGRADE_INSECURE=""
fi
# Update CSP in nginx config
CSP_POLICY="default-src 'none'; connect-src 'self' $CSP_CONNECT_SRC; frame-src 'self' $CSP_FRAME_SRC; script-src 'self' 'wasm-unsafe-eval' $CSP_SCRIPT_SRC; font-src 'self'; img-src * data:; manifest-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';$CSP_UPGRADE_INSECURE"
CSP_HEADER="add_header Content-Security-Policy \"$CSP_POLICY\" always;"
echo "CSP header: $CSP_HEADER"
# Replace CSP header in nginx config
sed -i "s|add_header Content-Security-Policy \"[^\"]*\" always;|$CSP_HEADER|g" /etc/nginx/http.d/default.conf || {
echo "Failed to replace CSP header"
}
# replace ENVs in the config
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_AUTH_SERVICE_URL \$\$NETBIRD_WASM_PATH \$\$NETBIRD_LICENSED \$\$NETBIRD_CLOUD \$\$NETBIRD_AGENT_NETWORK_ONLY \$\$NETBIRD_AGENT_NETWORK_ENABLED \$\$NETBIRD_HUBSPOT_PORTAL_ID \$\$NETBIRD_HUBSPOT_SIGNUP_FORM_ID \$\$NETBIRD_HUBSPOT_ONBOARDING_FORM_ID \$\$NETBIRD_HUBSPOT_SURVEY_FORM_ID \$\$NETBIRD_ANALYTICS_EXCLUDED_EMAILS"
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
@@ -171,25 +74,4 @@ for f in $(grep -R -l AUTH_SUPPORTED_SCOPES /usr/share/nginx/html); do
cp "$f" "$f".copy
envsubst "$ENV_STR" < "$f".copy > "$f"
rm "$f".copy
done
# Reload nginx so the patched CSP header takes effect.
# supervisord starts nginx (priority 100) before this script (priority 201) and
# never reloads it, so without this nginx keeps serving the static default.conf
# CSP that has no connect-src, breaking OIDC discovery over plain HTTP.
reloaded=false
for i in $(seq 1 10); do
if nginx -s reload 2>/dev/null; then
echo "Reloaded nginx to apply updated CSP configuration"
reloaded=true
break
fi
echo "Waiting for nginx to be ready before reload (attempt $i)..."
sleep 1
done
if [[ "$reloaded" != true ]]; then
echo "Failed to reload nginx after patching CSP header" >&2
nginx -t || true
exit 1
fi
done
-233
View File
@@ -1,233 +0,0 @@
# Playwright E2E Testing Guide
Complete reference for writing, running, and debugging Playwright E2E tests in the NetBird Dashboard.
## Philosophy
Tests simulate real user behavior: navigate via sidebar, click buttons, type into inputs, verify outcomes on screen. Use `{ force: true }` for Radix modal pointer-events issues.
## Setup & Running
```bash
npm run test:setup # Create docker-based test environment with Zitadel
npm run test:dev # Start app in test mode on http://localhost:1337
npm run test # Run all e2e tests headless
npm run test:ui # Open Playwright interactive UI
npx playwright test --config=e2e/playwright.config.ts tests/networks.spec.ts # Single spec
npm run test:clean # Tear down test environment
```
Config: `e2e/playwright.config.ts` (baseURL: `http://localhost:1337`). Auth: `e2e/playwright.env.json` (gitignored).
### Config Details
- `fullyParallel: false` — tests run sequentially within each spec
- Workers: 2 in CI, 4 locally
- Retries: 1
- Viewport: 1920x1080
- Timeouts: action 10s, navigation 15s
- On failure: screenshot, trace, video retained
## File Structure
```
e2e/
playwright.config.ts
helpers/
fixtures.ts # dashboardAsOwner / dashboardAsUser fixtures
auth.ts # loginToApp(), navigateTo()
navigation.ts # visitByNavigation()
utils.ts # generateRandomName(), clearScrollLock()
api.ts # Direct REST API helpers (list/delete for all entities)
reverse-proxy-l4.ts # Shared L4 reverse proxy helpers
fixtures/auth/ # Generated storageState files (gitignored)
environment/ # Docker compose, setup/teardown scripts
tests/
login.spec.ts # Auth setup (login both users, save storageState)
*.spec.ts # Test specs
```
## Architecture
Auth is handled by `login.spec.ts`, which runs as a separate Playwright project (`"login"`) that all other tests depend on via `dependencies: ["login"]` in the config. It logs in both users and saves Zitadel session cookies to `fixtures/auth/`. If auth files already exist, login is skipped. Each test file that modifies shared state (e.g., user roles) must restore it before finishing.
## Authentication
Two test users authenticated via the `login` project, saved as `storageState`:
| User | File | Role | Usage |
|------|------|------|-------|
| owner | `fixtures/auth/owner.json` | Owner | Default for all tests |
| user | `fixtures/auth/user.json` | User (changeable) | Role-based testing |
### Custom Fixtures (`helpers/fixtures.ts`)
Tests use custom fixtures instead of raw `page`:
```typescript
import { test, expect } from "../helpers/fixtures";
test("example", async ({ dashboardAsOwner: page }) => {
// Pre-authenticated as owner, reused across worker
});
test("multi-user", async ({ dashboardAsUser: page }) => {
// Pre-authenticated as user
});
```
- `dashboardAsOwner` — Pre-authenticated Page for the owner user (worker-scoped, reused across tests)
- `dashboardAsUser` — Pre-authenticated Page for the user user (worker-scoped)
For multi-context scenarios (e.g., approval/billing tests), create a new browser context directly:
```typescript
const context = await browser.newContext({ storageState: "e2e/fixtures/auth/user.json" });
const page = await context.newPage();
```
## Helpers Reference
### `auth.ts`
- **`loginToApp(page, user?)`** — Full Zitadel OIDC login flow. Handles app ready, setup modal, approval pending, onboarding, account selection, and login form states.
- **`navigateTo(page, path)`** — `page.goto(path)` + dismisses setup modal if present + clears scroll-lock.
### `navigation.ts`
- **`visitByNavigation(page, navText)`** — Clicks sidebar items by exact text via `left-navigation-item` testid.
### `utils.ts`
- **`generateRandomName(prefix?)`** — Returns `prefix` + 7 random alphanumeric chars.
- **`clearScrollLock(page)`** — Removes Radix artifacts: `data-scroll-locked`, `pointer-events: none`, stale overlay divs.
### `api.ts`
Direct REST API helpers that extract Bearer tokens from intercepted responses. Used for cleanup (deleting test artifacts by prefix). Covers: groups, networks, policies, routes, setup keys, DNS zones, nameserver groups, notification channels, reverse proxy services, users.
Pattern: `listX(page)` / `deleteXById(page, id)` / `deleteXByPrefix(page, prefix)`
### `reverse-proxy-l4.ts`
Shared helpers for TCP/TLS/UDP reverse proxy service tests:
- **`createNetwork(page)`** — Creates network, returns name
- **`addResource(page, networkName, address)`** — Adds resource to a network
- **`selectL4Resource(page, resourceName)`** — Selects resource in L4 target dropdown
- **`addAccessControlRules(page)`** / **`removeAllAccessControlRules(page)`** — Manages standard test rules
- **`resetServiceFilters(page)`** — Clicks "Reset Filters & Search" button if visible
- **`openServiceEdit(page, subdomain)`** — Navigates to services, resets filters, opens edit modal
- **`deleteService(page, subdomain)`** — Deletes service via action dropdown
- **`saveServiceEdit(page)`** — Saves with "No Protection" confirmation handling
- **`deleteNetwork(page, networkName)`** — Navigates to networks and deletes by name
## Writing Tests
### Standard Structure
```typescript
import { test, expect } from "../helpers/fixtures";
import { navigateTo } from "../helpers/auth";
import { generateRandomName } from "../helpers/utils";
test.describe.serial("Feature Name", () => {
test("Should create an item", async ({ dashboardAsOwner: page }) => {
await navigateTo(page, "/feature-page");
const name = generateRandomName("prefix-");
// ... create item
});
test("Should delete the item", async ({ dashboardAsOwner: page }) => {
// ... cleanup
});
});
```
### Key Patterns
**Selectors** — Always use `data-testid` via `page.getByTestId()`:
```typescript
page.getByTestId("group-name-input") // [data-testid="group-name-input"]
page.getByTestId("confirmation.confirm") // Confirmation dialogs
```
**Text matching:**
```typescript
page.getByText("Some text")
page.locator("tr").filter({ hasText: name })
```
**Assertions:**
```typescript
await expect(locator).toBeVisible()
await expect(locator).not.toBeVisible()
await expect(locator).toHaveAttribute("data-state", "checked")
await expect(locator).toContainText("text")
```
**Form inputs:**
```typescript
await input.fill("text") // Clears and types
await input.press("Enter")
await input.press("Escape")
```
**Radix modal workaround:**
```typescript
await button.click({ force: true }); // Force click, bypasses pointer-events checks
```
**Waiting for API responses:**
```typescript
const responsePromise = page.waitForResponse(
resp => resp.url().includes("/api/...") && resp.request().method() === "POST",
{ timeout: 30_000 },
);
await page.getByTestId("submit").click();
const response = await responsePromise;
expect([200, 201]).toContain(response.status());
```
**Cleanup with API helpers:**
```typescript
import { deleteGroupsByPrefix, deleteServicesByPrefix } from "../helpers/api";
// At the start of a test or in cleanup
await deleteServicesByPrefix(page, "my-prefix-");
await deleteGroupsByPrefix(page, "my-prefix-");
```
### Sidebar Navigation
```typescript
await visitByNavigation(page, "Access Control"); // Expand parent
await visitByNavigation(page, "Policies"); // Click child
```
| Parent | Children |
|--------|----------|
| Access Control | Policies, Groups, Posture Checks |
| Team | Users, Service Users |
| DNS | Nameservers, Zones, DNS Settings |
| Reverse Proxy | Custom Domains, Services |
## Test Coverage
| Area | Spec Files | Tag |
|------|-----------|-----|
| Access Control | `access-control.spec.ts`, `access-control-groups.spec.ts` | `@access-control` |
| DNS | `dns-zones.spec.ts`, `dns-nameservers.spec.ts`, `dns-settings.spec.ts` | `@dns` |
| Networks | `networks.spec.ts`, `network-routes.spec.ts` | `@network` |
| Reverse Proxy | `reverse-proxy-services-https.spec.ts`, `reverse-proxy-services-tcp.spec.ts`, `reverse-proxy-services-tls.spec.ts`, `reverse-proxy-services-udp.spec.ts`, `reverse-proxy-custom-domains.spec.ts` | `@reverse-proxy` |
| Settings | `settings-authentication.spec.ts`, `settings-clients.spec.ts`, `settings-groups.spec.ts`, `settings-networks.spec.ts`, `settings-permissions.spec.ts` | `@settings` |
| Notifications | `settings-notifications-email.spec.ts`, `settings-notifications-slack.spec.ts`, `settings-notifications-webhook.spec.ts` | `@notifications` |
| Team | `team-users.spec.ts`, `team-service-users.spec.ts`, `team-users-approval-and-billing.spec.ts` | `@team` |
| Setup Keys | `setup-keys.spec.ts` | `@setup-keys` |
## Debugging
1. `e2e/test-results/` — traces and screenshots on failure
2. `npx playwright show-report` — open the HTML report
3. `npm run test:ui` — interactive mode with step-by-step execution
4. `npx playwright test --config=e2e/playwright.config.ts --debug tests/<file>` — debugger mode
## `data-testid` Conventions
- Use `data-testid` selectors throughout. Add new ones to React components as needed.
- Kebab-case naming: `feature-field-input`, `action-feature`, `feature-actions`.
- Always use `data-testid` — both on native HTML elements and custom components. Custom components declare `"data-testid"?: string` in their props interface and place it on the appropriate internal DOM element.
-12
View File
@@ -1,12 +0,0 @@
# Ignore zitadel environment
.env
Caddyfile
management.json
turnserver.conf
zitadel.env
proxy.env
proxy-no-ports.env
proxy-certs/
proxy-certs-no-ports/
docker-compose.yml
/machinekey
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash
check_docker_compose() {
if command -v docker-compose &> /dev/null
then
echo "docker-compose"
return
fi
if docker compose --help &> /dev/null
then
echo "docker compose"
return
fi
echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr
exit 1
}
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
$DOCKER_COMPOSE_COMMAND down --volumes
rm -f docker-compose.yml Caddyfile zitadel.env .env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env
rm -rf proxy-certs proxy-certs-no-ports
rm -f ../../.test-config.json ../playwright.env.json
rm -f ../fixtures/auth/owner.json ../fixtures/auth/user.json
Loaded 20 of 683 files, more files were not shown because too many files have changed in this diff. Show more