diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml
deleted file mode 100644
index eb7f2ed2dd..0000000000
--- a/.github/workflows/build-docker.yml
+++ /dev/null
@@ -1,140 +0,0 @@
-name: Build Docker image
-
-on:
- workflow_call:
- inputs:
- tags:
- description: "List of tags as key-value pair attributes"
- required: false
- type: string
- flavor:
- description: "List of flavors as key-value pair attributes"
- required: false
- type: string
- trivy-exit-code:
- description: "Exit code for Trivy when vulnerabilities are found (0 = warn only, 1 = fail)"
- required: false
- type: string
- default: "1"
-
-env:
- GHCR_REPO: ghcr.io/defguard/defguard
-
-jobs:
- build-docker:
- runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- image:${{ matrix.os }}
- instance-size:${{ matrix.size }}
-
- strategy:
- matrix:
- cpu: [arm64, amd64]
- include:
- - os: arm-3.0
- size: xlarge
- cpu: arm64
- tag: arm64
- - os: ubuntu-7.0
- size: xlarge
- cpu: amd64
- tag: amd64
-
- permissions:
- contents: read
- packages: write
-
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- submodules: recursive
-
- - name: Login to GitHub container registry
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
-
- - name: Sanitize branch name
- run: echo "SAFE_REF=${GITHUB_REF_NAME//\//-}" >> $GITHUB_ENV
-
- - name: Build container
- uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
- with:
- context: .
- platforms: linux/${{ matrix.cpu }}
- provenance: false
- push: true
- tags: "${{ env.GHCR_REPO }}:${{ github.sha }}-${{ matrix.tag }}"
- cache-from: |
- type=registry,ref=${{ env.GHCR_REPO }}:cache-${{ matrix.tag }}
- type=registry,ref=${{ env.GHCR_REPO }}:cache-${{ matrix.tag }}-${{ env.SAFE_REF }}
- cache-to: type=registry,mode=max,ref=${{ env.GHCR_REPO }}:cache-${{ matrix.tag }}-${{ env.SAFE_REF }}
-
- - name: Scan image with Trivy
- uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
- env:
- TRIVY_SHOW_SUPPRESSED: 1
- TRIVY_IGNOREFILE: "./.trivyignore.yaml"
- with:
- image-ref: "${{ env.GHCR_REPO }}:${{ github.sha }}-${{ matrix.tag }}"
- format: "table"
- exit-code: ${{ inputs.trivy-exit-code }}
- ignore-unfixed: true
- vuln-type: "os,library"
- severity: "CRITICAL,HIGH,MEDIUM"
-
- docker-manifest:
- runs-on: [self-hosted, Linux]
-
- permissions:
- contents: read
- packages: write
- id-token: write # needed for signing the images with GitHub OIDC Token
-
- needs: [build-docker]
-
- steps:
- - name: Install Cosign
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
-
- - name: Docker meta
- id: meta
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
- with:
- images: |
- ${{ env.GHCR_REPO }}
- flavor: ${{ inputs.flavor }}
- tags: ${{ inputs.tags }}
-
- - name: Login to GitHub container registry
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Create and push manifests
- run: |
- tags='${{ env.GHCR_REPO }}:${{ github.sha }} ${{ steps.meta.outputs.tags }}'
- for tag in ${tags}
- do
- docker manifest rm ${tag} || true
- docker manifest create ${tag} ${{ env.GHCR_REPO }}:${{ github.sha }}-amd64 ${{ env.GHCR_REPO }}:${{ github.sha }}-arm64
- docker manifest push ${tag}
- done
-
- - name: Sign the images with GitHub OIDC Token
- run: |
- images='${{ env.GHCR_REPO }}:${{ github.sha }} ${{ steps.meta.outputs.tags }}'
- cosign sign --yes ${images}
-
- - name: Verify image signatures
- run: |
- images='${{ env.GHCR_REPO }}:${{ github.sha }} ${{ steps.meta.outputs.tags }}'
- cosign verify ${images} --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp="https://github.com/DefGuard/defguard" -o text
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4fb3af96fc..48650aaf56 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,19 +25,15 @@ permissions:
jobs:
lint:
runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- - instance-size:large
-
+ - self-hosted
+ - Linux
+ - X64
container: public.ecr.aws/docker/library/rust:1
env:
CARGO_TERM_COLOR: always
SQLX_OFFLINE: true
RUSTC_WRAPPER: sccache
- SCCACHE_BUCKET: defguard-gh-build-cache
- SCCACHE_REGION: eu-central-1
- AWS_ACCESS_KEY_ID: ${{ secrets.S3_CACHE_ACCESS_KEY }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_CACHE_SECRET_KEY }}
steps:
- name: Checkout
@@ -103,8 +99,9 @@ jobs:
build:
runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- - instance-size:2xlarge
+ - self-hosted
+ - Linux
+ - X64
container: public.ecr.aws/docker/library/rust:1
@@ -112,10 +109,6 @@ jobs:
CARGO_TERM_COLOR: always
SQLX_OFFLINE: true
RUSTC_WRAPPER: sccache
- SCCACHE_BUCKET: defguard-gh-build-cache
- SCCACHE_REGION: eu-central-1
- AWS_ACCESS_KEY_ID: ${{ secrets.S3_CACHE_ACCESS_KEY }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_CACHE_SECRET_KEY }}
steps:
- name: Checkout
@@ -149,6 +142,9 @@ jobs:
with:
tool: cargo-nextest
+ - name: Mark workspace as safe for git
+ run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
+
- name: Build and archive tests
run: |
cargo nextest archive \
@@ -171,10 +167,10 @@ jobs:
test:
needs: build
-
runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- - instance-size:large
+ - self-hosted
+ - Linux
+ - X64
container: public.ecr.aws/docker/library/rust:1
@@ -233,10 +229,10 @@ jobs:
test-ldap:
needs: build
-
runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- - instance-size:large
+ - self-hosted
+ - Linux
+ - X64
steps:
- name: Checkout
diff --git a/.github/workflows/current.yml b/.github/workflows/current.yml
deleted file mode 100644
index 493ef351f9..0000000000
--- a/.github/workflows/current.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Build current image
-permissions:
- contents: read
- id-token: write
- packages: write
-on:
- push:
- branches:
- - dev
- - "release/**"
- - "stable/**"
- paths-ignore:
- - "*.md"
- - "LICENSE"
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- build-current:
- uses: ./.github/workflows/build-docker.yml
- with:
- tags: |
- type=ref,event=branch
- type=sha
- trivy-exit-code: "0"
-
- trigger-e2e:
- needs: build-current
- uses: ./.github/workflows/e2e.yml
- secrets: inherit
-
- trigger-dev-deploy:
- needs: build-current
- if: ${{ github.event_name != 'pull_request' && (github.ref_name == 'dev' || startsWith(github.ref_name, 'release/'))}}
- uses: ./.github/workflows/dev-deployment.yml
- secrets: inherit
-
- trigger-staging-deploy:
- needs: build-current
- if: ${{ github.event_name != 'pull_request' && startsWith(github.ref_name, 'release/') }}
- uses: ./.github/workflows/staging-deployment.yml
- secrets: inherit
diff --git a/.github/workflows/dev-deployment.yml b/.github/workflows/dev-deployment.yml
deleted file mode 100644
index 6e2ee81264..0000000000
--- a/.github/workflows/dev-deployment.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-name: Deploy to DEV environment
-on:
- workflow_call:
-
-jobs:
- deploy-dev:
- runs-on: [self-hosted, Linux, X64]
- environment: DEV
- if: ${{ github.event_name != 'pull_request' && (github.ref_name == 'dev' || startsWith(github.ref_name, 'release/'))}}
- env:
- KUBE_HOST: ${{ secrets.KUBE_HOST }}
- KUBE_CERTIFICATE: ${{ secrets.KUBE_CERTIFICATE }}
- KUBE_TOKEN: ${{ secrets.KUBE_TOKEN }}
- steps:
- - name: Add SHORT_SHA env variable
- run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-7`" >> $GITHUB_ENV
- - name: Deploy new image version
- uses: actions-hub/kubectl@2639090a038d46a3b9b98b220ae0837676ded8b7 # v1.34.3
- with:
- args: --namespace defguard-dev set image deployment/defguard defguard=ghcr.io/defguard/defguard:sha-${{ env.SHORT_SHA }}
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
deleted file mode 100644
index 33be1b8358..0000000000
--- a/.github/workflows/e2e.yml
+++ /dev/null
@@ -1,323 +0,0 @@
-name: E2E tests
-
-on:
- workflow_call:
-
-permissions:
- contents: read
- packages: write
- id-token: write
-
-jobs:
- test:
- runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- instance-size:medium
- strategy:
- fail-fast: false
- matrix:
- shard: [1, 2, 3, 4, 5, 6, 7, 8]
-
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
-
- - name: Login to GitHub container registry
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Export image tag
- run: |
- BRANCH=${GITHUB_REF#refs/heads/}
- if [[ "$BRANCH" == release/* ]] || [[ "$BRANCH" == stable/* ]]; then
- IMAGE_TAG=${BRANCH//\//-}
- else
- IMAGE_TAG=$BRANCH
- fi
- echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
- echo "E2E tests will run on IMAGE_TAG=$IMAGE_TAG"
-
- - name: Set up Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
- with:
- node-version-file: "./e2e/.nvmrc"
-
- - name: Install pnpm
- id: pnpm-install
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- with:
- # FIXME: temporarily pinned because of https://github.com/pnpm/pnpm/pull/9959
- version: 10.17
- run_install: false
-
- - name: Get pnpm store directory
- id: pnpm-cache
- shell: bash
- run: |
- echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
-
- - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
- name: Setup pnpm cache
- with:
- path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Pull images
- run: docker compose --file './docker-compose.e2e.yaml' pull
-
- - name: Install E2E dependencies
- working-directory: ./e2e
- run: pnpm install --frozen-lockfile
-
- - name: Cache Playwright browsers
- id: playwright-cache
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
- with:
- path: ~/.cache/ms-playwright
- key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-playwright-
-
- - name: Install playwright chromium
- working-directory: ./e2e
- run: |
- if [[ "${{ steps.playwright-cache.outputs.cache-hit }}" == "true" ]]; then
- # Browsers are cached; only install missing system dependencies.
- npx playwright install-deps chromium
- else
- npx playwright install --with-deps chromium
- fi
-
- - name: run tests
- id: run-test
- working-directory: ./e2e
- env:
- DEFGUARD_LICENSE_KEY: ${{ secrets.DEFGUARD_LICENSE_KEY }}
- run: pnpm test --shard=${{ matrix.shard }}/8
-
- - name: Stop compose
- if: always()
- run: docker compose --file './docker-compose.e2e.yaml' down
-
- - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
- if: failure()
- with:
- name: playwright-report-shard-${{ matrix.shard }}
- path: |
- ./e2e/playwright-report
- retention-days: 7
-
- test-migration-wizard:
- runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- instance-size:medium
-
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
-
- - name: Login to GitHub container registry
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Export image tag
- run: |
- BRANCH=${GITHUB_REF#refs/heads/}
- if [[ "$BRANCH" == release/* ]] || [[ "$BRANCH" == stable/* ]]; then
- IMAGE_TAG=${BRANCH//\//-}
- else
- IMAGE_TAG=$BRANCH
- fi
- echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
-
- - name: Set up Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
- with:
- node-version-file: "./e2e/.nvmrc"
-
- - name: Install pnpm
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- with:
- # FIXME: temporarily pinned because of https://github.com/pnpm/pnpm/pull/9959
- version: 10.17
- run_install: false
-
- - name: Get pnpm store directory
- id: pnpm-cache
- shell: bash
- run: |
- echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
-
- - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
- name: Setup pnpm cache
- with:
- path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Pull images
- run: docker compose --file './docker-compose.e2e.yaml' pull
-
- - name: Install E2E dependencies
- working-directory: ./e2e
- run: pnpm install --frozen-lockfile
-
- - name: Cache Playwright browsers
- id: playwright-cache
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
- with:
- path: ~/.cache/ms-playwright
- key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-playwright-
-
- - name: Install playwright chromium
- working-directory: ./e2e
- run: |
- if [[ "${{ steps.playwright-cache.outputs.cache-hit }}" == "true" ]]; then
- npx playwright install-deps chromium
- else
- npx playwright install --with-deps chromium
- fi
-
- - name: Run migration wizard tests
- working-directory: ./e2e
- env:
- DEFGUARD_LICENSE_KEY: ${{ secrets.DEFGUARD_LICENSE_KEY }}
- run: pnpm playwright test --config playwright.config.migration.ts
-
- - name: Stop compose
- if: always()
- run: docker compose --file './docker-compose.e2e.yaml' down
-
- - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
- if: failure()
- with:
- name: playwright-report-migration-wizard
- path: |
- ./e2e/playwright-report
- retention-days: 7
-
- test-auto-adoption-wizard:
- runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- instance-size:medium
-
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
-
- - name: Login to GitHub container registry
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Export image tag
- run: |
- BRANCH=${GITHUB_REF#refs/heads/}
- if [[ "$BRANCH" == release/* ]] || [[ "$BRANCH" == stable/* ]]; then
- IMAGE_TAG=${BRANCH//\//-}
- else
- IMAGE_TAG=$BRANCH
- fi
- echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
-
- - name: Set up Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
- with:
- node-version-file: "./e2e/.nvmrc"
-
- - name: Install pnpm
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- with:
- # FIXME: temporarily pinned because of https://github.com/pnpm/pnpm/pull/9959
- version: 10.17
- run_install: false
-
- - name: Get pnpm store directory
- id: pnpm-cache
- shell: bash
- run: |
- echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
-
- - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
- name: Setup pnpm cache
- with:
- path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Pull images
- run: docker compose --file './docker-compose.e2e-auto-adoption.yaml' pull
-
- - name: Install E2E dependencies
- working-directory: ./e2e
- run: pnpm install --frozen-lockfile
-
- - name: Cache Playwright browsers
- id: playwright-cache
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
- with:
- path: ~/.cache/ms-playwright
- key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-playwright-
-
- - name: Install playwright chromium
- working-directory: ./e2e
- run: |
- if [[ "${{ steps.playwright-cache.outputs.cache-hit }}" == "true" ]]; then
- npx playwright install-deps chromium
- else
- npx playwright install --with-deps chromium
- fi
-
- - name: Run auto-adoption wizard tests
- working-directory: ./e2e
- env:
- DEFGUARD_LICENSE_KEY: ${{ secrets.DEFGUARD_LICENSE_KEY }}
- run: pnpm playwright test --config playwright.config.auto-adoption.ts
-
- - name: Stop compose
- if: always()
- run: docker compose --file './docker-compose.e2e-auto-adoption.yaml' down
-
- - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
- if: failure()
- with:
- name: playwright-report-auto-adoption-wizard
- path: |
- ./e2e/playwright-report
- retention-days: 7
-
- trigger-dev-deploy:
- needs: [test, test-migration-wizard, test-auto-adoption-wizard]
- if: ${{ github.event_name != 'pull_request' && github.ref_name == 'dev' && needs.test.result == 'success' && needs.test-migration-wizard.result == 'success' && needs.test-auto-adoption-wizard.result == 'success' }}
- uses: ./.github/workflows/dev-deployment.yml
- secrets: inherit
-
- trigger-staging-deploy:
- needs: [test, test-migration-wizard, test-auto-adoption-wizard]
- if: |
- github.event_name != 'pull_request' &&
- startsWith(github.ref_name, 'release/') &&
- needs.test.result == 'success' &&
- needs.test-migration-wizard.result == 'success' &&
- needs.test-auto-adoption-wizard.result == 'success'
- uses: ./.github/workflows/staging-deployment.yml
- secrets: inherit
diff --git a/.github/workflows/lint-e2e.yml b/.github/workflows/lint-e2e.yml
deleted file mode 100644
index 5ff2ad98cb..0000000000
--- a/.github/workflows/lint-e2e.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-on:
- push:
- branches:
- - dev
- - 'release/**'
- - 'stable/**'
- paths:
- - "e2e/**"
- pull_request:
- branches:
- - dev
- - 'release/**'
- - 'stable/**'
- paths:
- - "e2e/**"
-
-jobs:
- lint-e2e:
- runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- submodules: recursive
-
- - name: Install NodeJS
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
- with:
- node-version: 25
-
- - name: Install pnpm
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- with:
- version: 10
-
- # Change to '--frozen-lockfile' once this gets fixed:
- # https://github.com/pnpm/action-setup/issues/40
- - name: Build and lint e2e
- working-directory: e2e
- run: |
- pnpm install --no-frozen-lockfile
- pnpm lint
diff --git a/.github/workflows/lint-web.yml b/.github/workflows/lint-web.yml
index 9845aff660..e5af4be273 100644
--- a/.github/workflows/lint-web.yml
+++ b/.github/workflows/lint-web.yml
@@ -19,7 +19,9 @@ on:
jobs:
lint-web:
runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
+ - self-hosted
+ - Linux
+ - X64
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
diff --git a/.github/workflows/publish-docker-latest.yml b/.github/workflows/publish-docker-latest.yml
deleted file mode 100644
index 495e979b09..0000000000
--- a/.github/workflows/publish-docker-latest.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-name: Publish Docker latest tag
-
-on:
- release:
- types: [published]
-
-jobs:
- tag-docker-latest:
- # Only run when the release is marked as "Latest release" in the GitHub UI
- if: github.event.release.make_latest == 'true'
- runs-on: [self-hosted, Linux]
-
- env:
- GHCR_REPO: ghcr.io/defguard/defguard
-
- permissions:
- packages: write
- id-token: write # needed for Cosign keyless signing
-
- steps:
- - name: Install Cosign
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
-
- - name: Login to GitHub container registry
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
-
- - name: Derive semver tag
- run: |
- # Strip the leading 'v' from the release tag name (e.g. v1.2.3 -> 1.2.3)
- VERSION="${{ github.event.release.tag_name }}"
- echo "VERSION=${VERSION#v}" >> $GITHUB_ENV
-
- - name: Tag image as latest
- run: |
- docker buildx imagetools create \
- --tag ${{ env.GHCR_REPO }}:latest \
- ${{ env.GHCR_REPO }}:${{ env.VERSION }}
-
- - name: Sign the latest tag with GitHub OIDC Token
- run: cosign sign --yes ${{ env.GHCR_REPO }}:latest
-
- - name: Verify image signature
- run: |
- cosign verify ${{ env.GHCR_REPO }}:latest \
- --certificate-oidc-issuer https://token.actions.githubusercontent.com \
- --certificate-identity-regexp="https://github.com/DefGuard/defguard" \
- -o text
diff --git a/.github/workflows/release-generator.yml b/.github/workflows/release-generator.yml
new file mode 100644
index 0000000000..3213d08c70
--- /dev/null
+++ b/.github/workflows/release-generator.yml
@@ -0,0 +1,99 @@
+name: Build defguard_generator release binaries
+on:
+ push:
+ tags:
+ - v*.*.*
+ branches:
+ - defguard-demo-mode
+permissions:
+ contents: write
+ packages: write
+ id-token: write
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ create-release:
+ name: create-release
+ runs-on: self-hosted
+ outputs:
+ upload_url: ${{ steps.release.outputs.upload_url }}
+ steps:
+ - name: Create GitHub release
+ id: release
+ uses: shogo82148/actions-create-release@6a396031bc74c57403da1018fec74d24c6aa03cd # v1
+ with:
+ draft: true
+ generate_release_notes: true
+ tag_name: v2.0.2
+ build-binaries:
+ needs:
+ - create-release
+ runs-on:
+ - self-hosted
+ - Linux
+ - X64
+ env:
+ SQLX_OFFLINE: "1"
+ # Force the installed stable toolchain. RUSTUP_TOOLCHAIN takes precedence
+ # over any stale rustup directory override left on the self-hosted runner.
+ RUSTUP_TOOLCHAIN: "stable"
+ # sccache
+ SCCACHE_GHA_ENABLED: "true"
+ RUSTC_WRAPPER: "sccache"
+ steps:
+ # Store the version, stripping any v-prefix
+ - name: Write release version
+ run: |
+ # VERSION=${GITHUB_REF_NAME#v}
+ VERSION=2.0.2
+ echo Version: $VERSION
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
+
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ submodules: recursive
+
+ - name: Install Rust stable
+ uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
+ with:
+ targets: "aarch64-unknown-linux-gnu"
+
+ - name: Run sccache-cache
+ uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
+
+ - name: Build Linux x86_64 binary
+ run: |
+ cargo build --locked --release -p defguard_generator --target x86_64-unknown-linux-gnu
+ mv target/x86_64-unknown-linux-gnu/release/defguard_generator defguard_generator-${{ env.VERSION }}-x86_64-unknown-linux-gnu
+
+ - name: Build Linux aarch64 binary
+ env:
+ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
+ PKG_CONFIG_SYSROOT_DIR: /usr/lib/aarch64-linux-gnu
+ run: |
+ cargo build --locked --release -p defguard_generator --target aarch64-unknown-linux-gnu
+ mv target/aarch64-unknown-linux-gnu/release/defguard_generator defguard_generator-${{ env.VERSION }}-aarch64-unknown-linux-gnu
+
+ - name: Upload Linux x86_64 binary
+ uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ needs.create-release.outputs.upload_url }}
+ asset_path: defguard_generator-${{ env.VERSION }}-x86_64-unknown-linux-gnu
+ asset_content_type: application/octet-stream
+ overwrite: true
+
+ - name: Upload Linux aarch64 binary
+ uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ needs.create-release.outputs.upload_url }}
+ asset_path: defguard_generator-${{ env.VERSION }}-aarch64-unknown-linux-gnu
+ asset_content_type: application/octet-stream
+ overwrite: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 53b7a36ce7..9f9746c9a3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,40 +13,7 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
-
-
jobs:
- build-docker-release:
- # Ignore tags with -, like v1.0.0-alpha
- # This job will build the docker container with the "latest" tag which
- # is a tag used in production, thus it should only be run for full releases.
- if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-')
- name: Build Release Docker image
- uses: ./.github/workflows/build-docker.yml
- with:
- tags: |
- type=semver,pattern={{version}}
- type=semver,pattern={{major}}.{{minor}}
- type=semver,pattern={{major}}
- type=sha
- # Explicitly disable latest tag. It will be added otherwise.
- flavor: |
- latest=false
-
- build-docker-prerelease:
- # Only build tags with -, like v1.0.0-alpha
- if: startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-')
- name: Build Pre-release Docker image
- uses: ./.github/workflows/build-docker.yml
- with:
- tags: |
- type=raw,value=pre-release
- type=semver,pattern={{version}}
- type=sha
- # Explicitly disable latest tag. It will be added otherwise.
- flavor: |
- latest=false
-
create-release:
name: create-release
runs-on: self-hosted
@@ -60,14 +27,6 @@ jobs:
draft: true
generate_release_notes: true
- create-sbom:
- needs:
- - create-release
- - build-docker-release
- uses: ./.github/workflows/sbom.yml
- with:
- upload_url: ${{ needs.create-release.outputs.upload_url }}
-
build-binaries:
needs:
- create-release
@@ -139,13 +98,13 @@ jobs:
tar -zcf defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz \
defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu
- - name: Build FreeBSD binary
- run: |
- rsync -rlptxzH -e 'ssh -l root' --del ./ freebsd:work/defguard/
- ssh root@freebsd 'cd work/defguard && cargo build --locked --release'
- scp root@freebsd:work/defguard/target/release/defguard defguard-${{ env.VERSION }}-x86_64-unknown-freebsd
- tar -zcf defguard-${{ env.VERSION }}-x86_64-unknown-freebsd.tar.gz \
- defguard-${{ env.VERSION }}-x86_64-unknown-freebsd
+ # - name: Build FreeBSD binary
+ # run: |
+ # rsync -rlptxzH -e 'ssh -l root' --del ./ freebsd:work/defguard/
+ # ssh root@freebsd 'cd work/defguard && cargo build --locked --release'
+ # scp root@freebsd:work/defguard/target/release/defguard defguard-${{ env.VERSION }}-x86_64-unknown-freebsd
+ # tar -zcf defguard-${{ env.VERSION }}-x86_64-unknown-freebsd.tar.gz \
+ # defguard-${{ env.VERSION }}-x86_64-unknown-freebsd
- name: Build x86_64 DEB package
uses: defGuard/fpm-action@ebb2575fbb892876fbdd326bb6d12524fbd7398c # main
@@ -154,8 +113,7 @@ jobs:
"defguard-${{ env.VERSION }}-x86_64-unknown-linux-gnu=/usr/bin/defguard
linux/defguard.service=/usr/lib/systemd/system/defguard.service
.env.example=/etc/defguard/core.conf"
- fpm_opts:
- "--architecture amd64
+ fpm_opts: "--architecture amd64
--output-type deb
--version ${{ env.VERSION }}
--package defguard-${{ env.VERSION }}-x86_64-unknown-linux-gnu.deb
@@ -171,8 +129,7 @@ jobs:
"defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu=/usr/bin/defguard
linux/defguard.service=/usr/lib/systemd/system/defguard.service
.env.example=/etc/defguard/core.conf"
- fpm_opts:
- "--architecture arm64
+ fpm_opts: "--architecture arm64
--output-type deb
--version ${{ env.VERSION }}
--package defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu.deb
@@ -181,54 +138,51 @@ jobs:
--before-remove linux/prerm
--after-remove linux/postrm"
- - name: Build x86_64 RPM package
- uses: defGuard/fpm-action@ebb2575fbb892876fbdd326bb6d12524fbd7398c # main
- with:
- fpm_args:
- "defguard-${{ env.VERSION }}-x86_64-unknown-linux-gnu=/usr/bin/defguard
- linux/defguard.service=/usr/lib/systemd/system/defguard.service
- .env.example=/etc/defguard/core.conf"
- fpm_opts:
- "--architecture amd64
- --output-type rpm
- --version ${{ env.VERSION }}
- --package defguard-${{ env.VERSION }}-x86_64-unknown-linux-gnu.rpm
- --before-install linux/preinst
- --after-install linux/postinst
- --before-remove linux/prerm
- --after-remove linux/postrm"
-
- - name: Build aarch64 RPM package
- uses: defGuard/fpm-action@ebb2575fbb892876fbdd326bb6d12524fbd7398c # main
- with:
- fpm_args:
- "defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu=/usr/bin/defguard
- linux/defguard.service=/usr/lib/systemd/system/defguard.service
- .env.example=/etc/defguard/core.conf"
- fpm_opts:
- "--architecture arm64
- --output-type rpm
- --version ${{ env.VERSION }}
- --package defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu.rpm
- --before-install linux/preinst
- --after-install linux/postinst
- --before-remove linux/prerm
- --after-remove linux/postrm"
-
- - name: Build FreeBSD package
- uses: defGuard/fpm-action@ebb2575fbb892876fbdd326bb6d12524fbd7398c # main
- with:
- fpm_args:
- "defguard-${{ env.VERSION }}-x86_64-unknown-freebsd=/usr/local/bin/defguard
- freebsd/defguard=/usr/local/etc/rc.d/defguard
- .env.example=/etc/defguard/core.conf"
- fpm_opts:
- "--architecture amd64
- --output-type freebsd
- --version ${{ env.VERSION }}
- --package defguard-${{ env.VERSION }}_x86_64-unknown-freebsd.pkg
- --freebsd-osversion '*'
- --depends openssl"
+ # - name: Build x86_64 RPM package
+ # uses: defGuard/fpm-action@ebb2575fbb892876fbdd326bb6d12524fbd7398c # main
+ # with:
+ # fpm_args:
+ # "defguard-${{ env.VERSION }}-x86_64-unknown-linux-gnu=/usr/bin/defguard
+ # linux/defguard.service=/usr/lib/systemd/system/defguard.service
+ # .env.example=/etc/defguard/core.conf"
+ # fpm_opts: "--architecture amd64
+ # --output-type rpm
+ # --version ${{ env.VERSION }}
+ # --package defguard-${{ env.VERSION }}-x86_64-unknown-linux-gnu.rpm
+ # --before-install linux/preinst
+ # --after-install linux/postinst
+ # --before-remove linux/prerm
+ # --after-remove linux/postrm"
+
+ # - name: Build aarch64 RPM package
+ # uses: defGuard/fpm-action@ebb2575fbb892876fbdd326bb6d12524fbd7398c # main
+ # with:
+ # fpm_args:
+ # "defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu=/usr/bin/defguard
+ # linux/defguard.service=/usr/lib/systemd/system/defguard.service
+ # .env.example=/etc/defguard/core.conf"
+ # fpm_opts: "--architecture arm64
+ # --output-type rpm
+ # --version ${{ env.VERSION }}
+ # --package defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu.rpm
+ # --before-install linux/preinst
+ # --after-install linux/postinst
+ # --before-remove linux/prerm
+ # --after-remove linux/postrm"
+
+ # - name: Build FreeBSD package
+ # uses: defGuard/fpm-action@ebb2575fbb892876fbdd326bb6d12524fbd7398c # main
+ # with:
+ # fpm_args:
+ # "defguard-${{ env.VERSION }}-x86_64-unknown-freebsd=/usr/local/bin/defguard
+ # freebsd/defguard=/usr/local/etc/rc.d/defguard
+ # .env.example=/etc/defguard/core.conf"
+ # fpm_opts: "--architecture amd64
+ # --output-type freebsd
+ # --version ${{ env.VERSION }}
+ # --package defguard-${{ env.VERSION }}_x86_64-unknown-freebsd.pkg
+ # --freebsd-osversion '*'
+ # --depends openssl"
- name: Upload Linux x86_64 archive
uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
@@ -250,15 +204,15 @@ jobs:
asset_content_type: application/gzip
overwrite: true
- - name: Upload FreeBSD x86_64 archive
- uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create-release.outputs.upload_url }}
- asset_path: defguard-${{ env.VERSION }}-x86_64-unknown-freebsd.tar.gz
- asset_content_type: application/gzip
- overwrite: true
+ # - name: Upload FreeBSD x86_64 archive
+ # uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
+ # env:
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # with:
+ # upload_url: ${{ needs.create-release.outputs.upload_url }}
+ # asset_path: defguard-${{ env.VERSION }}-x86_64-unknown-freebsd.tar.gz
+ # asset_content_type: application/gzip
+ # overwrite: true
- name: Upload Linux x86_64 DEB
uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
@@ -280,35 +234,35 @@ jobs:
asset_content_type: application/gzip
overwrite: true
- - name: Upload Linux x86_64 RPM
- uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create-release.outputs.upload_url }}
- asset_path: defguard-${{ env.VERSION }}-x86_64-unknown-linux-gnu.rpm
- asset_content_type: application/gzip
- overwrite: true
-
- - name: Upload Linux aarch64 RPM
- uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create-release.outputs.upload_url }}
- asset_path: defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu.rpm
- asset_content_type: application/gzip
- overwrite: true
-
- - name: Upload FreeBSD package
- uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create-release.outputs.upload_url }}
- asset_path: defguard-${{ env.VERSION }}_x86_64-unknown-freebsd.pkg
- asset_content_type: application/x-pkg
- overwrite: true
+ # - name: Upload Linux x86_64 RPM
+ # uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
+ # env:
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # with:
+ # upload_url: ${{ needs.create-release.outputs.upload_url }}
+ # asset_path: defguard-${{ env.VERSION }}-x86_64-unknown-linux-gnu.rpm
+ # asset_content_type: application/gzip
+ # overwrite: true
+
+ # - name: Upload Linux aarch64 RPM
+ # uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
+ # env:
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # with:
+ # upload_url: ${{ needs.create-release.outputs.upload_url }}
+ # asset_path: defguard-${{ env.VERSION }}-aarch64-unknown-linux-gnu.rpm
+ # asset_content_type: application/gzip
+ # overwrite: true
+
+ # - name: Upload FreeBSD package
+ # uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
+ # env:
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # with:
+ # upload_url: ${{ needs.create-release.outputs.upload_url }}
+ # asset_path: defguard-${{ env.VERSION }}_x86_64-unknown-freebsd.pkg
+ # asset_content_type: application/x-pkg
+ # overwrite: true
ubuntu-22-04-build:
needs:
diff --git a/.github/workflows/sbom-regenerate.yml b/.github/workflows/sbom-regenerate.yml
deleted file mode 100644
index 9675a6ec69..0000000000
--- a/.github/workflows/sbom-regenerate.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: Periodic SBOM Regeneration
-permissions:
- contents: write
-
-on:
- schedule:
- - cron: '30 2 * * *' # 2:30 AM UTC
-
-jobs:
- list-releases:
- name: List releases
- runs-on: ubuntu-latest
- outputs:
- releases: ${{ steps.get-releases.outputs.releases }}
- steps:
- - name: Get list of releases
- id: get-releases
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- RELEASES_JSON=$(gh api repos/${{ github.repository }}/releases \
- --jq '[.[]
- | select(.draft == false and (.tag_name | test("^v[0-9]+\\.[0-9]+\\.[0-9]+$")))
- | {tagName: .tag_name, uploadUrl: .upload_url}][:1]')
- echo "releases=$RELEASES_JSON" >> $GITHUB_OUTPUT
- regenerate-for-release:
- name: Regenerate SBOM for release
- needs: list-releases
- # Don't run if no releases were found.
- if: needs.list-releases.outputs.releases != '[]'
- strategy:
- fail-fast: false
- matrix:
- release: ${{ fromJson(needs.list-releases.outputs.releases) }}
- uses: ./.github/workflows/sbom.yml
- with:
- upload_url: ${{ matrix.release.uploadUrl }}
- tag: ${{ matrix.release.tagName }}
- secrets: inherit
diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml
deleted file mode 100644
index edeb524f8d..0000000000
--- a/.github/workflows/sbom.yml
+++ /dev/null
@@ -1,99 +0,0 @@
-name: Create SBOM files
-
-on:
- workflow_call:
- inputs:
- upload_url:
- description: "Release assets upload URL"
- required: true
- type: string
- tag:
- description: "The git tag to generate SBOM for - used in scheduled runs"
- required: false
- type: string
-
-jobs:
- create-sbom:
- permissions:
- contents: write
- runs-on: [self-hosted, Linux, X64]
-
- steps:
- - name: Determine release tag and version
- id: vars
- # Uses inputs.tag for scheduled runs, otherwise github.ref_name.
- run: |
- TAG_NAME=${{ inputs.tag || github.ref_name }}
- VERSION=${TAG_NAME#v}
- echo "TAG_NAME=$TAG_NAME" >> $GITHUB_OUTPUT
- echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
-
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- ref: ${{ steps.vars.outputs.TAG_NAME }}
- submodules: recursive
-
- - name: Create SBOM with Trivy
- uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
- env:
- TRIVY_SHOW_SUPPRESSED: 1
- TRIVY_IGNOREFILE: "./.trivyignore.yaml"
- with:
- scan-type: 'fs'
- format: 'spdx-json'
- output: "defguard-${{ steps.vars.outputs.VERSION }}.sbom.json"
- scan-ref: '.'
- severity: "CRITICAL,HIGH,MEDIUM,LOW"
- scanners: "vuln"
- skip-dirs: "e2e"
-
- - name: Create Docker image SBOM with Trivy
- uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
- env:
- TRIVY_SHOW_SUPPRESSED: 1
- TRIVY_IGNOREFILE: "./.trivyignore.yaml"
- with:
- image-ref: "ghcr.io/defguard/defguard:${{ steps.vars.outputs.VERSION }}"
- scan-type: 'image'
- format: 'spdx-json'
- output: "defguard-${{ steps.vars.outputs.VERSION }}-docker.sbom.json"
- severity: "CRITICAL,HIGH,MEDIUM,LOW"
- scanners: "vuln"
-
- - name: Create security advisory file with Trivy
- uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
- env:
- TRIVY_SHOW_SUPPRESSED: 1
- TRIVY_IGNOREFILE: "./.trivyignore.yaml"
- with:
- scan-type: 'fs'
- format: 'json'
- output: "defguard-${{ steps.vars.outputs.VERSION }}.advisories.json"
- scan-ref: '.'
- severity: "CRITICAL,HIGH,MEDIUM,LOW"
- scanners: "vuln"
- skip-dirs: "e2e"
-
- - name: Create docker image security advisory file with Trivy
- uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
- env:
- TRIVY_SHOW_SUPPRESSED: 1
- TRIVY_IGNOREFILE: "./.trivyignore.yaml"
- with:
- image-ref: "ghcr.io/defguard/defguard:${{ steps.vars.outputs.VERSION }}"
- scan-type: 'image'
- format: 'json'
- output: "defguard-${{ steps.vars.outputs.VERSION }}-docker.advisories.json"
- severity: "CRITICAL,HIGH,MEDIUM,LOW"
- scanners: "vuln"
-
- - name: Upload SBOMs and advisories
- uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ inputs.upload_url }}
- asset_path: "defguard-*.json"
- asset_content_type: application/json
- overwrite: true
diff --git a/.github/workflows/staging-deployment.yml b/.github/workflows/staging-deployment.yml
deleted file mode 100644
index fbc67eb449..0000000000
--- a/.github/workflows/staging-deployment.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: Deploy to release staging environment
-permissions:
- contents: read
-on:
- workflow_call:
-
-jobs:
- deploy-staging:
- runs-on: [self-hosted, Linux, X64]
- environment: STAGING
- if: ${{ github.event_name != 'pull_request' && startsWith(github.ref_name, 'release/') }}
- env:
- KUBE_HOST: ${{ secrets.KUBE_HOST }}
- KUBE_CERTIFICATE: ${{ secrets.KUBE_CERTIFICATE }}
- KUBE_TOKEN: ${{ secrets.KUBE_TOKEN }}
- steps:
- - name: Add SHORT_SHA env variable
- run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-7`" >> $GITHUB_ENV
- - name: Deploy new image version
- uses: actions-hub/kubectl@2639090a038d46a3b9b98b220ae0837676ded8b7 # v1.34.3
- with:
- args: --namespace defguard-staging set image deployment/defguard defguard=ghcr.io/defguard/defguard:sha-${{ env.SHORT_SHA }}
diff --git a/.github/workflows/test-apt-repo.yml b/.github/workflows/test-apt-repo.yml
deleted file mode 100644
index 0d58dc7111..0000000000
--- a/.github/workflows/test-apt-repo.yml
+++ /dev/null
@@ -1,112 +0,0 @@
-name: Test APT repository
-
-"on":
- schedule:
- - cron: "0 */6 * * *"
- workflow_dispatch:
- workflow_run:
- workflows: ["Update repositories with packages"]
- types: [completed]
-
-jobs:
- test-apt-install:
- name: "${{ matrix.package }} / ${{ matrix.component }}"
- runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
- - instance-size:small
-
- container: public.ecr.aws/docker/library/debian:trixie-slim
-
- strategy:
- fail-fast: false
- matrix:
- package: [defguard, defguard-proxy, defguard-gateway]
- component: [release, pre-release, release-2.0, pre-release-2.0]
- include:
- - package: defguard
- github_repo: DefGuard/defguard
- - package: defguard-proxy
- github_repo: DefGuard/proxy
- - package: defguard-gateway
- github_repo: DefGuard/gateway
-
- steps:
- - name: Install prerequisites
- run: apt-get update -y && apt-get install -y ca-certificates curl jq libmnl0 libnftnl11
-
- - name: Add Defguard GPG key
- run: |
- install -m 0755 -d /etc/apt/keyrings
- curl -fsSL https://apt.defguard.net/defguard.asc -o /etc/apt/keyrings/defguard.asc
- chmod a+r /etc/apt/keyrings/defguard.asc
-
- - name: Add Defguard APT repository
- run: |
- echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/defguard.asc] https://apt.defguard.net/ trixie ${{ matrix.component }}" \
- > /etc/apt/sources.list.d/defguard.list
-
- - name: Update APT cache
- run: apt-get update -y
-
- - name: Check package availability in component
- run: |
- CANDIDATE=$(apt-cache policy ${{ matrix.package }} | awk '/Candidate:/ {print $2}')
- if [ -z "$CANDIDATE" ] || [ "$CANDIDATE" = "(none)" ]; then
- echo "::notice::${{ matrix.package }} not available in component ${{ matrix.component }}, skipping"
- echo "SKIP=true" >> $GITHUB_ENV
- else
- echo "Candidate version: $CANDIDATE"
- echo "SKIP=false" >> $GITHUB_ENV
- fi
-
- - name: Get expected version from GitHub
- if: env.SKIP != 'true'
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- case "${{ matrix.component }}" in
- release-2.0) PRERELEASE=false; MAJOR=v2. ;;
- pre-release-2.0) PRERELEASE=true; MAJOR=v2. ;;
- release) PRERELEASE=false; MAJOR=v1. ;;
- pre-release) PRERELEASE=true; MAJOR=v1. ;;
- esac
- VERSION=$(curl -sf \
- -H "Authorization: Bearer $GH_TOKEN" \
- https://api.github.com/repos/${{ matrix.github_repo }}/releases \
- | jq -r --argjson pre "$PRERELEASE" --arg major "$MAJOR" \
- '[.[] | select(.prerelease == $pre and (.tag_name | startswith($major)))][0].tag_name // empty')
- if [ -z "$VERSION" ]; then
- echo "::notice::no $MAJOR release (prerelease=$PRERELEASE) of ${{ matrix.package }} on GitHub, skipping"
- echo "SKIP=true" >> $GITHUB_ENV
- exit 0
- fi
- VERSION="${VERSION#v}"
-
- # legacy pre-release still holds 2.0 betas published before the
- # component split; accept them instead of expecting the latest v1.x
- CANDIDATE=$(apt-cache policy ${{ matrix.package }} | awk '/Candidate:/ {print $2}')
- if [ "${{ matrix.component }}" = "pre-release" ] && [ "${CANDIDATE%%.*}" != "1" ]; then
- echo "::notice::candidate $CANDIDATE is from before the component split, skipping version comparison"
- VERSION=""
- fi
-
- echo "Expected version: $VERSION"
- echo "EXPECTED_VERSION=$VERSION" >> $GITHUB_ENV
-
- - name: Install ${{ matrix.package }}
- if: env.SKIP != 'true'
- run: apt-get install -y ${{ matrix.package }}
-
- - name: Verify ${{ matrix.package }} version
- if: env.SKIP != 'true'
- run: |
- INSTALLED=$(dpkg -s ${{ matrix.package }} | grep '^Version:' | awk '{print $2}')
- echo "Installed version: $INSTALLED"
- if [ -n "$EXPECTED_VERSION" ]; then
- echo "Expected version: $EXPECTED_VERSION"
- if [ "$INSTALLED" != "$EXPECTED_VERSION" ]; then
- echo "Version mismatch!"
- exit 1
- fi
- fi
- ${{ matrix.package }} -V
diff --git a/.github/workflows/test-web.yml b/.github/workflows/test-web.yml
index 887223cddc..3762c99b07 100644
--- a/.github/workflows/test-web.yml
+++ b/.github/workflows/test-web.yml
@@ -15,7 +15,9 @@ permissions:
jobs:
test-web:
runs-on:
- - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }}
+ - self-hosted
+ - Linux
+ - X64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
diff --git a/.github/workflows/update-repositories.yml b/.github/workflows/update-repositories.yml
deleted file mode 100644
index 0295f1f6f3..0000000000
--- a/.github/workflows/update-repositories.yml
+++ /dev/null
@@ -1,149 +0,0 @@
-name: Update repositories with packages
-
-on:
- release:
- types: [published]
-
-permissions:
- contents: read
-
-jobs:
- update-apt:
- runs-on:
- - self-hosted
- - Linux
- - X64
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Install gh cli
- run: |
- sudo apt-get install -y gh
-
- - name: Download .deb assets from release
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- mkdir debs
- gh release download "${{ github.event.release.tag_name }}" \
- --pattern "*.deb" \
- --dir debs
-
- - name: Install ruby with deb-s3
- run: |
- sudo apt-get install -y ruby
- gem install deb-s3
- echo "$(ruby -r rubygems -e 'puts Gem.user_dir')/bin" >> $GITHUB_PATH
-
- - name: Upload DEB to APT repository
- run: |
- if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then
- component="pre-release-2.0"
- else
- component="release-2.0"
- fi
-
- for deb_file in debs/*.deb; do
- if [[ "$deb_file" == *"ubuntu-22-04-lts"* ]]; then
- codename="bookworm"
- else
- codename="trixie"
- fi
-
- echo "Uploading $deb_file to $codename"
- deb-s3 upload -p -l \
- --bucket=apt.defguard.net \
- --access-key-id=${{ secrets.AWS_ACCESS_KEY_APT }} \
- --secret-access-key=${{ secrets.AWS_SECRET_KEY_APT }} \
- --s3-region=eu-north-1 \
- --no-fail-if-exists \
- --codename="$codename" \
- --component="$component" \
- "$deb_file"
- done
-
- apt-sign:
- needs:
- - update-apt
- runs-on:
- - self-hosted
- - Linux
- - X64
- steps:
- - name: Sign APT repository
- run: |
- export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_APT }}
- export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_KEY_APT }}
- export AWS_REGION=eu-north-1
- sudo apt update -y
- sudo apt install -y awscli curl jq
-
- for DIST in trixie bookworm; do
- aws s3 cp s3://apt.defguard.net/dists/${DIST}/Release .
-
- curl -X POST "${{ secrets.DEFGUARD_SIGNING_URL }}?signature_type=both" \
- -H "Authorization: Bearer ${{ secrets.DEFGUARD_SIGNING_API_KEY }}" \
- -F "file=@Release" \
- -o response.json
-
- cat response.json | jq -r '.files["Release.gpg"].content' | base64 --decode > Release.gpg
- cat response.json | jq -r '.files.Release.content' | base64 --decode > InRelease
-
- aws s3 cp Release.gpg s3://apt.defguard.net/dists/${DIST}/ --acl public-read
- aws s3 cp InRelease s3://apt.defguard.net/dists/${DIST}/ --acl public-read
-
- done
- (aws s3 ls s3://apt.defguard.net/dists/ --recursive; aws s3 ls s3://apt.defguard.net/pool/ --recursive) | awk '{print ""$4"
"}' > index.html
- aws s3 cp index.html s3://apt.defguard.net/ --acl public-read
-
- verify-apt-repo:
- needs:
- - apt-sign
- runs-on:
- - self-hosted
- - Linux
- - X64
- steps:
- - name: Verify published repository signatures and metadata
- run: |
- set -euo pipefail
- sudo apt update -y
- sudo apt install -y curl gpg
-
- WORKDIR=$(mktemp -d)
- trap 'rm -rf "$WORKDIR"' EXIT
- cd "$WORKDIR"
-
- curl -fsSL https://apt.defguard.net/defguard.asc | gpg --dearmor -o keyring.gpg
-
- for DIST in trixie bookworm; do
- echo "=== Verifying $DIST ==="
- curl -fsSL "https://apt.defguard.net/dists/${DIST}/Release" -o Release
- curl -fsSL "https://apt.defguard.net/dists/${DIST}/Release.gpg" -o Release.gpg
- curl -fsSL "https://apt.defguard.net/dists/${DIST}/InRelease" -o InRelease
-
- gpgv --keyring "$WORKDIR/keyring.gpg" Release.gpg Release
- gpgv --keyring "$WORKDIR/keyring.gpg" InRelease
-
- for COMPONENT in $(awk '/^Components:/ {for (i=2; i<=NF; i++) print $i}' Release); do
- PACKAGES_PATH="${COMPONENT}/binary-amd64/Packages"
- EXPECTED_SHA=$(awk -v p="$PACKAGES_PATH" '/^SHA256:/{s=1; next} /^[A-Za-z]/{s=0} s && $3 == p {print $1; exit}' Release)
- # InRelease must describe the same metadata as the detached-signed
- # Release, otherwise apt clients see a signature/content mismatch
- INRELEASE_SHA=$(awk -v p="$PACKAGES_PATH" '/^SHA256:/{s=1; next} /^[A-Za-z-]/{s=0} s && $3 == p {print $1; exit}' InRelease)
- if [ -z "$EXPECTED_SHA" ] || [ "$EXPECTED_SHA" != "$INRELEASE_SHA" ]; then
- echo "SHA256 entry for $PACKAGES_PATH missing or differs between Release and InRelease in $DIST"
- exit 1
- fi
- curl -fsSL "https://apt.defguard.net/dists/${DIST}/${PACKAGES_PATH}" -o Packages
- ACTUAL_SHA=$(sha256sum Packages | awk '{print $1}')
- if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
- echo "Checksum mismatch for $DIST/$PACKAGES_PATH"
- echo "expected: $EXPECTED_SHA"
- echo "actual: $ACTUAL_SHA"
- exit 1
- fi
- echo "$DIST/$PACKAGES_PATH OK"
- done
- done
diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml
new file mode 100644
index 0000000000..827f425840
--- /dev/null
+++ b/.github/workflows/upstream-sync.yml
@@ -0,0 +1,37 @@
+name: Weekly upstream sync
+
+on:
+ schedule:
+ - cron: "0 8 * * 1"
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ sync:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout fork
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ ref: stable/2.x
+ fetch-depth: 0
+
+ - name: Pull in upstream stable/2.x (keep our own workflows)
+ run: |
+ git remote add upstream https://github.com/defguard/defguard.git
+ git fetch upstream stable/2.x
+ git reset --hard upstream/stable/2.x
+ rm -rf .github/workflows
+ git checkout origin/stable/2.x -- .github/workflows
+
+ - name: Create or update pull request
+ uses: peter-evans/create-pull-request@v8
+ with:
+ token: ${{ secrets.SYNC_PAT }}
+ branch: sync/upstream-stable-2.x
+ base: stable/2.x
+ title: "Weekly upstream sync: stable/2.x"
+ body: "Automated weekly sync from upstream defguard/defguard stable/2.x (workflows excluded). Review and merge manually."
diff --git a/Cargo.lock b/Cargo.lock
index 57786e150a..d00d6f472d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -88,14 +88,13 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ammonia"
-version = "4.1.2"
+version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
+checksum = "68b9d3370580a12f4b7a10fdcc18b28942c083ba570e3d954fe59d10951b85a2"
dependencies = [
- "cssparser 0.35.0",
- "html5ever 0.35.0",
+ "cssparser",
+ "html5ever",
"maplit",
- "tendril 0.4.3",
"url",
]
@@ -160,9 +159,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.102"
+version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "arbitrary"
@@ -1149,8 +1148,8 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2f845f973dd2b71c48976c504756d7a163c5068dd6857b8d2f8ee1e92a190f6"
dependencies = [
- "cssparser 0.37.0",
- "html5ever 0.39.0",
+ "cssparser",
+ "html5ever",
"lru",
"memchr",
"precomputed-hash",
@@ -1162,42 +1161,19 @@ dependencies = [
"url",
]
-[[package]]
-name = "cssparser"
-version = "0.35.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa"
-dependencies = [
- "cssparser-macros 0.6.1",
- "dtoa-short",
- "itoa",
- "phf 0.11.3",
- "smallvec",
-]
-
[[package]]
name = "cssparser"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98"
dependencies = [
- "cssparser-macros 0.7.0",
+ "cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.13.1",
"smallvec",
]
-[[package]]
-name = "cssparser-macros"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
-dependencies = [
- "quote",
- "syn",
-]
-
[[package]]
name = "cssparser-macros"
version = "0.7.0"
@@ -2357,16 +2333,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
-[[package]]
-name = "futf"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
-dependencies = [
- "mac",
- "new_debug_unreachable",
-]
-
[[package]]
name = "futures"
version = "0.3.32"
@@ -2770,17 +2736,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "html5ever"
-version = "0.35.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4"
-dependencies = [
- "log",
- "markup5ever 0.35.0",
- "match_token",
-]
-
[[package]]
name = "html5ever"
version = "0.39.0"
@@ -2788,7 +2743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
dependencies = [
"log",
- "markup5ever 0.39.0",
+ "markup5ever",
]
[[package]]
@@ -3539,29 +3494,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
-[[package]]
-name = "mac"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
-
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
-[[package]]
-name = "markup5ever"
-version = "0.35.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3"
-dependencies = [
- "log",
- "tendril 0.4.3",
- "web_atoms 0.1.3",
-]
-
[[package]]
name = "markup5ever"
version = "0.39.0"
@@ -3569,19 +3507,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
dependencies = [
"log",
- "tendril 0.5.0",
- "web_atoms 0.2.4",
-]
-
-[[package]]
-name = "match_token"
-version = "0.35.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "tendril",
+ "web_atoms",
]
[[package]]
@@ -4529,7 +4456,6 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
- "phf_macros 0.11.3",
"phf_shared 0.11.3",
]
@@ -4539,7 +4465,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
- "phf_macros 0.13.1",
+ "phf_macros",
"phf_shared 0.13.1",
"serde",
]
@@ -4584,19 +4510,6 @@ dependencies = [
"phf_shared 0.13.1",
]
-[[package]]
-name = "phf_macros"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
-dependencies = [
- "phf_generator 0.11.3",
- "phf_shared 0.11.3",
- "proc-macro2",
- "quote",
- "syn",
-]
-
[[package]]
name = "phf_macros"
version = "0.13.1"
@@ -5617,7 +5530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d"
dependencies = [
"bitflags 2.13.0",
- "cssparser 0.37.0",
+ "cssparser",
"derive_more 2.1.1",
"log",
"new_debug_unreachable",
@@ -6308,19 +6221,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
-[[package]]
-name = "string_cache"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
-dependencies = [
- "new_debug_unreachable",
- "parking_lot",
- "phf_shared 0.11.3",
- "precomputed-hash",
- "serde",
-]
-
[[package]]
name = "string_cache"
version = "0.9.0"
@@ -6333,18 +6233,6 @@ dependencies = [
"precomputed-hash",
]
-[[package]]
-name = "string_cache_codegen"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
-dependencies = [
- "phf_generator 0.11.3",
- "phf_shared 0.11.3",
- "proc-macro2",
- "quote",
-]
-
[[package]]
name = "string_cache_codegen"
version = "0.6.1"
@@ -6492,17 +6380,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "tendril"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
-dependencies = [
- "futf",
- "mac",
- "utf-8",
-]
-
[[package]]
name = "tendril"
version = "0.5.0"
@@ -7457,18 +7334,6 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "web_atoms"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414"
-dependencies = [
- "phf 0.11.3",
- "phf_codegen 0.11.3",
- "string_cache 0.8.9",
- "string_cache_codegen 0.5.4",
-]
-
[[package]]
name = "web_atoms"
version = "0.2.4"
@@ -7477,8 +7342,8 @@ checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538"
dependencies = [
"phf 0.13.1",
"phf_codegen 0.13.1",
- "string_cache 0.9.0",
- "string_cache_codegen 0.6.1",
+ "string_cache",
+ "string_cache_codegen",
]
[[package]]
diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs
index 611cddf3e4..5ba5273cfe 100644
--- a/crates/defguard/src/main.rs
+++ b/crates/defguard/src/main.rs
@@ -22,6 +22,7 @@ use defguard_common::{
};
use defguard_core::{
auth::failed_login::FailedLoginMap,
+ change_user_password,
db::AppEvent,
enterprise::{
activity_log_stream::activity_log_stream_manager::run_activity_log_stream_manager,
@@ -125,6 +126,10 @@ async fn main() -> Result<(), anyhow::Error> {
let config = gateway_config(&pool, args).await?;
println!("{config:?}");
}
+ Command::ChangePassword(args) => {
+ change_user_password(&pool, args).await?;
+ println!("Password for user {} changed", args.username);
+ }
}
// return early
diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs
index bb5b245087..fb05fc2128 100644
--- a/crates/defguard_common/src/config.rs
+++ b/crates/defguard_common/src/config.rs
@@ -19,9 +19,7 @@ use crate::{VERSION, db::models::Settings};
pub static SERVER_CONFIG: OnceLock = OnceLock::new();
pub fn server_config() -> &'static DefGuardConfig {
- SERVER_CONFIG
- .get()
- .expect("Server configuration not set yet")
+ SERVER_CONFIG.get_or_init(DefGuardConfig::new_test_config)
}
#[derive(Clone, Debug, Parser, Serialize)]
@@ -202,6 +200,10 @@ pub struct DefGuardConfig {
/// Set to 0 to disable rate limiting.
#[arg(long, env = "DEFGUARD_RATELIMIT_BURST", default_value_t = 0)]
pub rate_limit_burst: u32,
+
+ /// Run the instance in demo mode
+ #[arg(long = "demo-mode", env = "DEFGUARD_DEMO_MODE", default_value = "true")]
+ pub is_demo_mode: bool,
}
#[derive(Clone, Debug, Subcommand)]
@@ -216,6 +218,16 @@ pub enum Command {
InitVpnLocation(InitVpnLocationArgs),
#[command(about = "Output the gateway gRPC configuration payload for a VPN location by ID.")]
GatewayConfig(GatewayConfigArgs),
+ #[command(about = "Change a user's password.")]
+ ChangePassword(ChangePasswordArgs),
+}
+
+#[derive(Args, Debug, Clone)]
+pub struct ChangePasswordArgs {
+ #[arg(long)]
+ pub username: String,
+ #[arg(long)]
+ pub password: String,
}
#[derive(Args, Debug, Clone)]
@@ -304,6 +316,7 @@ impl DefGuardConfig {
adopt_edge: None,
rate_limit_per_second: 0,
rate_limit_burst: 0,
+ is_demo_mode: false,
};
config
diff --git a/crates/defguard_common/src/db/models/settings/mod.rs b/crates/defguard_common/src/db/models/settings/mod.rs
index 2671763200..4cb24230d1 100644
--- a/crates/defguard_common/src/db/models/settings/mod.rs
+++ b/crates/defguard_common/src/db/models/settings/mod.rs
@@ -767,6 +767,25 @@ impl Settings {
Ok(())
}
+ /// Check if all required SMTP options are configured.
+ #[must_use]
+ pub fn smtp_configured(&self) -> bool {
+ self.smtp.server.is_some()
+ && self.smtp.port.is_some()
+ && self.smtp.sender.is_some()
+ && self.smtp.server != Some(String::new())
+ && self.smtp.sender != Some(String::new())
+ }
+
+ #[must_use]
+ pub fn demo_locked_fields_differ(&self, other: &Self) -> bool {
+ self.demo_locked_fields() != other.demo_locked_fields()
+ }
+
+ fn demo_locked_fields(&self) -> impl PartialEq + '_ {
+ (&self.license, &self.defguard_url, &self.public_proxy_url)
+ }
+
/// Check if all required LDAP options are configured.
///
/// Meant to be used to check if LDAP integration can be enabled.
diff --git a/crates/defguard_core/src/appstate.rs b/crates/defguard_core/src/appstate.rs
index 499775c14d..e1cb58c8ef 100644
--- a/crates/defguard_core/src/appstate.rs
+++ b/crates/defguard_core/src/appstate.rs
@@ -2,7 +2,9 @@ use std::sync::{Arc, Mutex, RwLock, atomic::AtomicBool};
use axum::extract::FromRef;
use axum_extra::extract::cookie::Key;
-use defguard_common::{db::models::Settings, types::proxy::ProxyControlMessage};
+use defguard_common::{
+ config::server_config, db::models::Settings, types::proxy::ProxyControlMessage,
+};
use reqwest::Client;
use serde_json::json;
use sqlx::PgPool;
@@ -54,6 +56,10 @@ impl AppState {
let reqwest_client = Client::builder().user_agent("reqwest").build().unwrap();
while let Some(msg) = rx.recv().await {
debug!("WebHook triggered");
+ if server_config().is_demo_mode {
+ debug!("Demo mode enabled; skipping webhook trigger");
+ continue;
+ }
debug!("Retrieving webhooks");
if let Ok(webhooks) = WebHook::all_enabled(&pool, &msg).await {
debug!("Found webhooks: {webhooks:?}");
diff --git a/crates/defguard_core/src/enterprise/activity_log_stream/activity_log_stream_manager.rs b/crates/defguard_core/src/enterprise/activity_log_stream/activity_log_stream_manager.rs
index 07852e0e43..264e6386b0 100644
--- a/crates/defguard_core/src/enterprise/activity_log_stream/activity_log_stream_manager.rs
+++ b/crates/defguard_core/src/enterprise/activity_log_stream/activity_log_stream_manager.rs
@@ -1,6 +1,7 @@
use std::{sync::Arc, time::Duration};
use bytes::Bytes;
+use defguard_common::config::server_config;
use sqlx::PgPool;
use tokio::{sync::broadcast::Receiver, task::JoinSet, time::interval};
use tokio_util::sync::CancellationToken;
@@ -34,7 +35,9 @@ pub async fn run_activity_log_stream_manager(
let cancel_token = Arc::new(CancellationToken::new());
// check if activity log streams can be started
- if enterprise_features_enabled {
+ if server_config().is_demo_mode {
+ info!("Demo mode is enabled, not starting activity log streams");
+ } else if enterprise_features_enabled {
info!("Starting all configured activity log streams");
let streams = ActivityLogStream::all(&pool).await?;
debug!("Found {} configured activity log streams", streams.len());
diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs
index 1daa1fa8f6..1899055c26 100644
--- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs
+++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs
@@ -4,9 +4,12 @@ use std::{
time::Duration,
};
-use defguard_common::db::{
- Id,
- models::{Settings, group::Group, user::User},
+use defguard_common::{
+ config::server_config,
+ db::{
+ Id,
+ models::{Settings, group::Group, user::User},
+ },
};
use paste::paste;
use reqwest::header::AUTHORIZATION;
@@ -404,6 +407,11 @@ pub(crate) async fn test_directory_sync_connection(
return Ok(());
}
+ if server_config().is_demo_mode {
+ debug!("Demo mode is enabled, skipping testing directory sync connection");
+ return Ok(());
+ }
+
match DirectorySyncClient::build(pool).await {
Ok(mut dir_sync) => {
dir_sync.prepare().await?;
@@ -429,6 +437,11 @@ pub async fn sync_user_groups_if_configured(
return Ok(());
}
+ if server_config().is_demo_mode {
+ debug!("Demo mode is enabled, skipping testing directory sync connection");
+ return Ok(());
+ }
+
let provider = OpenIdProvider::get_current(pool).await?;
if !is_directory_sync_enabled(provider.as_ref()) {
debug!("Directory sync is disabled, skipping syncing user groups");
@@ -1044,6 +1057,11 @@ pub(crate) async fn do_directory_sync(
return Ok(());
}
+ if server_config().is_demo_mode {
+ debug!("Demo mode is enabled, skipping performing directory sync");
+ return Ok(());
+ }
+
// TODO: Reduce the amount of times those settings are retrieved in the whole directory sync
// process.
let provider = OpenIdProvider::get_current(pool).await?;
diff --git a/crates/defguard_core/src/enterprise/handlers/api_tokens.rs b/crates/defguard_core/src/enterprise/handlers/api_tokens.rs
index 56cb814f63..5ef9e469c6 100644
--- a/crates/defguard_core/src/enterprise/handlers/api_tokens.rs
+++ b/crates/defguard_core/src/enterprise/handlers/api_tokens.rs
@@ -4,7 +4,7 @@ use axum::{
http::StatusCode,
};
use chrono::Utc;
-use defguard_common::{db::models::user::User, random::gen_alphanumeric};
+use defguard_common::{config::server_config, db::models::user::User, random::gen_alphanumeric};
use serde_json::json;
use super::LicenseInfo;
@@ -35,6 +35,12 @@ pub async fn add_api_token(
) -> ApiResult {
debug!("Adding API token {:?} for user {username}", data.name);
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Creating API tokens is disabled in demo mode",
+ ));
+ }
+
// authorize request
let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?;
diff --git a/crates/defguard_core/src/enterprise/handlers/mod.rs b/crates/defguard_core/src/enterprise/handlers/mod.rs
index 7f3eb7bffd..d05ae297bc 100644
--- a/crates/defguard_core/src/enterprise/handlers/mod.rs
+++ b/crates/defguard_core/src/enterprise/handlers/mod.rs
@@ -15,6 +15,8 @@ use axum::{
extract::{FromRef, FromRequestParts},
http::{StatusCode, request::Parts},
};
+use chrono::{TimeDelta, Utc};
+use defguard_common::config::server_config;
use serde::Serialize;
use super::{
@@ -106,6 +108,21 @@ pub async fn check_enterprise_info(_admin: AdminRole, _session: SessionInfo) ->
"limits": limits_info,
})
});
+
+ let license_info = license_info.or_else(|| {
+ server_config().is_demo_mode.then(|| {
+ serde_json::json!({
+ "valid_until": Utc::now() + TimeDelta::days(3650),
+ "subscription": true,
+ "expired": false,
+ "limits_exceeded": false,
+ "tier": "Enterprise",
+ "support_type": "DirectEnterprise",
+ "limits": null,
+ })
+ })
+ });
+
Ok(ApiResponse::json(
serde_json::json!({"license_info": license_info}),
StatusCode::OK,
diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
index 82ebef88c3..9b28fb9353 100644
--- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
+++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs
@@ -3,10 +3,13 @@ use axum::{
extract::{Path, State},
http::StatusCode,
};
-use defguard_common::db::models::{
- Settings, WireguardNetwork,
- settings::{OpenIdUsernameHandling, update_current_settings},
- wireguard::LocationMfaMode,
+use defguard_common::{
+ config::server_config,
+ db::models::{
+ Settings, WireguardNetwork,
+ settings::{OpenIdUsernameHandling, update_current_settings},
+ wireguard::LocationMfaMode,
+ },
};
use rsa::{RsaPrivateKey, pkcs8::DecodePrivateKey};
use serde_json::json;
@@ -78,6 +81,47 @@ pub(crate) async fn add_openid_provider(
"User {} adding OpenID provider {}",
session.user.username, provider_data.name
);
+ if server_config().is_demo_mode {
+ let new_provider = OpenIdProvider::new(
+ provider_data.name,
+ provider_data.base_url,
+ provider_data.kind,
+ provider_data.client_id,
+ "SECRET".to_string(),
+ provider_data.display_name,
+ Some("PRIVATE_KEY".to_string()),
+ provider_data.google_service_account_email,
+ provider_data.admin_email,
+ provider_data.directory_sync_enabled,
+ provider_data.directory_sync_interval,
+ provider_data.directory_sync_user_behavior.into(),
+ provider_data.directory_sync_admin_behavior.into(),
+ provider_data.directory_sync_target.into(),
+ Some("PRIVATE_KEY".to_string()),
+ provider_data.okta_dirsync_client_id,
+ Vec::new(),
+ provider_data
+ .jumpcloud_api_key
+ .map(|_| "SECRET".to_string()),
+ provider_data.prefetch_users,
+ None,
+ )
+ .upsert(&appstate.pool)
+ .await?;
+ info!(
+ "User {} added OpenID client {}",
+ session.user.username, new_provider.name
+ );
+ appstate.emit_event(ApiEvent {
+ context,
+ event: Box::new(ApiEventType::OpenIdProviderModified {
+ provider: new_provider,
+ }),
+ })?;
+
+ return Ok(ApiResponse::with_status(StatusCode::CREATED));
+ }
+
let current_provider = OpenIdProvider::get_current(&appstate.pool).await?;
// The key is sent from the frontend only when user explicitly changes it, as we never send it
@@ -344,6 +388,45 @@ pub(crate) async fn modify_openid_provider(
);
let mut transaction = appstate.pool.begin().await?;
let provider = OpenIdProvider::find_by_name(&mut *transaction, &provider_data.name).await?;
+
+ if server_config().is_demo_mode
+ && let Some(mut provider) = provider
+ {
+ provider.base_url = provider_data.base_url;
+ provider.kind = provider_data.kind;
+ provider.client_id = provider_data.client_id;
+ provider.client_secret = "SECRET".to_string();
+ provider.display_name = provider_data.display_name;
+ provider.google_service_account_key = Some("PRIVATE_KEY".to_string());
+ provider.google_service_account_email = provider_data.google_service_account_email;
+ provider.admin_email = provider_data.admin_email;
+ provider.directory_sync_enabled = provider_data.directory_sync_enabled;
+ provider.directory_sync_interval = provider_data.directory_sync_interval;
+ provider.directory_sync_user_behavior = provider_data.directory_sync_user_behavior.into();
+ provider.directory_sync_admin_behavior = provider_data.directory_sync_admin_behavior.into();
+ provider.directory_sync_target = provider_data.directory_sync_target.into();
+ provider.okta_private_jwk = Some("PRIVATE_KEY".to_string());
+ provider.okta_dirsync_client_id = provider_data.okta_dirsync_client_id;
+ provider.directory_sync_group_match = Vec::new();
+ provider.jumpcloud_api_key = provider_data
+ .jumpcloud_api_key
+ .map(|_| "SECRET".to_string());
+ provider.prefetch_users = provider_data.prefetch_users;
+ provider.directory_sync_user_groups = None;
+ provider.save(&mut *transaction).await?;
+ transaction.commit().await?;
+
+ info!(
+ "User {} modified OpenID client {}",
+ session.user.username, provider.name
+ );
+ appstate.emit_event(ApiEvent {
+ context,
+ event: Box::new(ApiEventType::OpenIdProviderModified { provider }),
+ })?;
+
+ return Ok(ApiResponse::with_status(StatusCode::OK));
+ }
if let Some(mut provider) = provider {
let private_key = match &provider_data.google_service_account_key {
Some(key) => {
@@ -527,6 +610,13 @@ pub(crate) async fn test_dirsync_connection(
session.user.username
);
+ if server_config().is_demo_mode {
+ return Ok(ApiResponse::new(
+ json!({"message": "Demo connection successful", "success": true}),
+ StatusCode::OK,
+ ));
+ }
+
if let Err(err) = test_directory_sync_connection(&appstate.pool).await {
error!(
"User {} tested directory sync connection, the connection failed: {err}",
diff --git a/crates/defguard_core/src/enterprise/ldap/client.rs b/crates/defguard_core/src/enterprise/ldap/client.rs
index 7013440566..626b8191fb 100644
--- a/crates/defguard_core/src/enterprise/ldap/client.rs
+++ b/crates/defguard_core/src/enterprise/ldap/client.rs
@@ -4,7 +4,10 @@ use std::{
time::Duration,
};
-use defguard_common::db::models::{Settings, User};
+use defguard_common::{
+ config::server_config,
+ db::models::{Settings, User},
+};
use ldap3::{
LdapConnAsync, LdapConnSettings, Mod, Scope, SearchEntry,
adapters::{Adapter, EntriesOnly, PagedResults},
@@ -18,6 +21,9 @@ const STREAMING_PAGE_SIZE: i32 = 500;
impl LDAPConnection {
pub async fn create() -> Result {
+ if server_config().is_demo_mode {
+ return Err(LdapError::Ldap("LDAP is disabled in demo mode".to_string()));
+ }
let settings = Settings::get_current_settings();
let config = LDAPConfig::try_from(settings.clone())?;
let url = settings.ldap_url.ok_or(LdapError::MissingSettings(
diff --git a/crates/defguard_core/src/enterprise/ldap/mod.rs b/crates/defguard_core/src/enterprise/ldap/mod.rs
index f3b7305ddb..1238a86924 100644
--- a/crates/defguard_core/src/enterprise/ldap/mod.rs
+++ b/crates/defguard_core/src/enterprise/ldap/mod.rs
@@ -1,11 +1,14 @@
use std::{collections::HashSet, future::Future};
-use defguard_common::db::{
- Id,
- models::{
- Settings, User,
- group::Group,
- settings::{LdapSyncStatus, update_current_settings},
+use defguard_common::{
+ config::server_config,
+ db::{
+ Id,
+ models::{
+ Settings, User,
+ group::Group,
+ settings::{LdapSyncStatus, update_current_settings},
+ },
},
};
#[cfg(not(test))]
@@ -66,6 +69,11 @@ pub(crate) async fn do_ldap_sync(
return Ok(());
}
+ if server_config().is_demo_mode {
+ debug!("Demo mode is enabled, not performing LDAP sync");
+ return Ok(());
+ }
+
if !is_business_license_active() {
info!(
"Enterprise features are disabled, not performing LDAP sync and automatically \
diff --git a/crates/defguard_core/src/enterprise/mod.rs b/crates/defguard_core/src/enterprise/mod.rs
index 77b39bbad4..12d45b36ee 100644
--- a/crates/defguard_core/src/enterprise/mod.rs
+++ b/crates/defguard_core/src/enterprise/mod.rs
@@ -11,6 +11,7 @@ pub mod oauth2;
pub mod snat;
mod utils;
+use defguard_common::config::server_config;
use std::time::Duration;
use self::{
@@ -37,6 +38,10 @@ pub fn is_enterprise_license_active() -> bool {
fn is_license_tier_active(tier: LicenseTier) -> bool {
trace!("Checking if features for {tier} license tier should be enabled");
+ if server_config().is_demo_mode {
+ return true;
+ }
+
// get current object counts
let counts = get_counts();
diff --git a/crates/defguard_core/src/handlers/app_info.rs b/crates/defguard_core/src/handlers/app_info.rs
index 24491d0b3f..abd78f04a3 100644
--- a/crates/defguard_core/src/handlers/app_info.rs
+++ b/crates/defguard_core/src/handlers/app_info.rs
@@ -1,6 +1,7 @@
use axum::{extract::State, http::StatusCode};
use defguard_common::{
VERSION,
+ config::server_config,
db::models::{Settings, WireguardNetwork},
};
@@ -25,6 +26,7 @@ pub struct AppInfo {
smtp_enabled: bool,
ldap_info: LdapInfo,
external_openid_enabled: bool,
+ demo_mode: bool,
}
pub async fn get_app_info(State(appstate): State, _session: SessionInfo) -> ApiResult {
@@ -43,6 +45,7 @@ pub async fn get_app_info(State(appstate): State, _session: SessionInf
ad: settings.ldap_uses_ad,
},
external_openid_enabled,
+ demo_mode: server_config().is_demo_mode,
};
Ok(ApiResponse::json(res, StatusCode::OK))
diff --git a/crates/defguard_core/src/handlers/auth.rs b/crates/defguard_core/src/handlers/auth.rs
index 75206242ae..c39c10f735 100644
--- a/crates/defguard_core/src/handlers/auth.rs
+++ b/crates/defguard_core/src/handlers/auth.rs
@@ -441,6 +441,11 @@ pub async fn webauthn_finish(
State(appstate): State,
Json(webauth_reg): Json,
) -> ApiResult {
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Configuring MFA is disabled in demo mode",
+ ));
+ }
info!(
"Finishing WebAuthn registration for user {}",
session.user.username
@@ -641,6 +646,11 @@ pub async fn totp_enable(
State(appstate): State,
Json(data): Json,
) -> ApiResult {
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Configuring MFA is disabled in demo mode",
+ ));
+ }
let mut user = session.user;
debug!("Enabling TOTP for user {}", user.username);
if user.verify_totp_code(&data.code) {
@@ -822,6 +832,11 @@ pub async fn email_mfa_enable(
State(appstate): State,
Json(data): Json,
) -> ApiResult {
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Configuring MFA is disabled in demo mode",
+ ));
+ }
let mut user = session.user;
debug!("Enabling email MFA for user {}", user.username);
if user.verify_email_mfa_code(&data.code) {
diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs
index b5aee72f10..f10fd16398 100644
--- a/crates/defguard_core/src/handlers/component_setup.rs
+++ b/crates/defguard_core/src/handlers/component_setup.rs
@@ -15,6 +15,7 @@ use defguard_certs::der_to_pem;
use defguard_common::{
VERSION,
auth::claims::{Claims, ClaimsType},
+ config::server_config,
db::{
Id,
models::{
@@ -300,6 +301,54 @@ pub async fn setup_proxy_tls_stream(
debug!("Configuration check passed");
+ if server_config().is_demo_mode {
+ yield Ok(flow.step(SetupStep::CheckingAvailability));
+ yield Ok(flow.step(SetupStep::CheckingVersion));
+ yield Ok(flow.step(SetupStep::ObtainingCsr));
+ yield Ok(flow.step(SetupStep::SigningCertificate));
+ yield Ok(flow.step(SetupStep::ConfiguringTls));
+
+ let proxy = Proxy::new(
+ request.common_name.as_str(),
+ ip_or_domain,
+ i32::from(request.grpc_port),
+ session.user.fullname().as_str(),
+ );
+ let proxy = match proxy.save(&pool).await {
+ Ok(proxy) => proxy,
+ Err(err) => {
+ yield Ok(flow.error(&format!("Failed to save Edge to database: {err}")));
+ return;
+ }
+ };
+ debug!(
+ "Edge '{}' registered successfully in demo mode with ID: {}",
+ request.common_name, proxy.id
+ );
+
+ match Wizard::get(&pool).await {
+ Ok(wizard) => {
+ if !wizard.completed {
+ let state = InitialSetupState {
+ step: InitialSetupStep::InternalUrlSettings,
+ };
+ if let Err(err) = state.save(&pool).await {
+ yield Ok(flow.error(&format!("Failed to update setup step in wizard: {err}")));
+ return;
+ }
+ debug!("Initial setup step advanced to 'InternalUrlSettings'");
+ }
+ }
+ Err(err) => {
+ yield Ok(flow.error(&format!("Failed to fetch wizard state: {err}")));
+ return;
+ }
+ }
+
+ yield Ok(flow.step(SetupStep::Done));
+ return;
+ }
+
let url_str = format!("http://{ip_or_domain}:{}", request.grpc_port);
let url = match Url::parse(&url_str) {
Ok(u) => u,
@@ -750,6 +799,28 @@ async fn perform_gateway_adoption(
}
}
+ if server_config().is_demo_mode {
+ step(SetupStep::CheckingAvailability);
+ step(SetupStep::CheckingVersion);
+ step(SetupStep::ObtainingCsr);
+ step(SetupStep::SigningCertificate);
+ step(SetupStep::ConfiguringTls);
+
+ let gateway = Gateway::new(
+ network_id,
+ name,
+ ip_or_domain.to_owned(),
+ grpc_port.into(),
+ modified_by,
+ );
+ let saved = gateway
+ .save(pool)
+ .await
+ .map_err(|e| format!("Failed to save Gateway to database: {e}"))?;
+ debug!("Gateway setup completed successfully in demo mode");
+ return Ok(saved);
+ }
+
let url_str = format!("http://{ip_or_domain}:{grpc_port}");
let url = Url::parse(&url_str).map_err(|e| format!("Invalid URL: {e}"))?;
@@ -1172,6 +1243,15 @@ pub async fn stream_proxy_acme(
proxy_control_tx: Option>>,
) -> Sse>> {
let stream = async_stream::stream! {
+ if server_config().is_demo_mode {
+ yield Ok(acme_error_event(
+ "Connecting",
+ "Certificate management is disabled in demo mode".to_owned(),
+ None,
+ ));
+ return;
+ }
+
let certs = match Certificates::get_or_default(&pool).await {
Ok(c) => c,
Err(e) => {
diff --git a/crates/defguard_core/src/handlers/core_certs.rs b/crates/defguard_core/src/handlers/core_certs.rs
index b5b6f9c4ea..5c165837d1 100644
--- a/crates/defguard_core/src/handlers/core_certs.rs
+++ b/crates/defguard_core/src/handlers/core_certs.rs
@@ -1,6 +1,7 @@
use axum::{Extension, Json, extract::State, http::StatusCode};
use defguard_certs::{CertificateInfo, der_to_pem, parse_pem_certificate};
use defguard_common::{
+ config::server_config,
db::models::{Certificates, Settings},
types::proxy::ProxyControlMessage,
};
@@ -76,6 +77,11 @@ pub(crate) async fn set_internal_url_settings(
"User {} applying core internal URL certificate settings",
session.user.username
);
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Certificate management is disabled in demo mode",
+ ));
+ }
let settings = Settings::get_current_settings();
let cert_info = apply_internal_url_settings(&pool, &settings.defguard_url, config).await?;
reload_core_web_server(&appstate);
@@ -114,6 +120,11 @@ pub(crate) async fn set_external_url_settings(
"User {} applying proxy external URL certificate settings",
session.user.username
);
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Certificate management is disabled in demo mode",
+ ));
+ }
let settings = Settings::get_current_settings();
let ssl_type = config.ssl_type.clone();
let cert_info = apply_external_url_settings(&pool, &settings.public_proxy_url, config).await?;
diff --git a/crates/defguard_core/src/handlers/gateway.rs b/crates/defguard_core/src/handlers/gateway.rs
index 44015c4c9d..3b20e687bf 100644
--- a/crates/defguard_core/src/handlers/gateway.rs
+++ b/crates/defguard_core/src/handlers/gateway.rs
@@ -2,8 +2,11 @@ use axum::{
Json,
extract::{Path, State, rejection::JsonRejection},
};
-use chrono::NaiveDateTime;
-use defguard_common::db::{Id, models::gateway::Gateway};
+use chrono::{NaiveDateTime, Utc};
+use defguard_common::{
+ config::server_config,
+ db::{Id, models::gateway::Gateway},
+};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -57,6 +60,7 @@ impl GatewayInfo {
)
.fetch_all(pool)
.await
+ .map(Self::mock_demo_connection)
}
pub async fn find_by_location_id(pool: &PgPool, location_id: Id) -> sqlx::Result> {
@@ -80,6 +84,20 @@ impl GatewayInfo {
)
.fetch_all(pool)
.await
+ .map(Self::mock_demo_connection)
+ }
+
+ fn mock_demo_connection(mut gateways: Vec) -> Vec {
+ if server_config().is_demo_mode {
+ let now = Utc::now().naive_utc();
+ for gateway in &mut gateways {
+ gateway.enabled = true;
+ gateway.connected = true;
+ gateway.connected_at = Some(now);
+ gateway.disconnected_at = None;
+ }
+ }
+ gateways
}
}
@@ -143,7 +161,14 @@ pub(crate) async fn gateway_details(
);
let gateway = Gateway::find_by_id(&appstate.pool, gateway_id).await?;
let response = match gateway {
- Some(gateway) => ApiResponse::json(gateway, StatusCode::OK),
+ Some(mut gateway) => {
+ if server_config().is_demo_mode {
+ gateway.connected_at = Some(Utc::now().naive_utc());
+ gateway.disconnected_at = None;
+ gateway.enabled = true;
+ }
+ ApiResponse::json(gateway, StatusCode::OK)
+ }
None => ApiResponse::json(Value::Null, StatusCode::NOT_FOUND),
};
info!(
diff --git a/crates/defguard_core/src/handlers/group.rs b/crates/defguard_core/src/handlers/group.rs
index b7ea3efa09..82d90fc7eb 100644
--- a/crates/defguard_core/src/handlers/group.rs
+++ b/crates/defguard_core/src/handlers/group.rs
@@ -4,11 +4,14 @@ use axum::{
extract::{Json, Path, Query, State},
http::StatusCode,
};
-use defguard_common::db::{
- Id,
- models::{
- User,
- group::{Group, Permission},
+use defguard_common::{
+ config::server_config,
+ db::{
+ Id,
+ models::{
+ User,
+ group::{Group, Permission},
+ },
},
};
use sqlx::query_as;
@@ -431,6 +434,11 @@ pub(crate) async fn modify_group(
error!(msg);
return Err(WebError::ObjectNotFound(msg));
};
+ if server_config().is_demo_mode && group.is_admin {
+ return Err(WebError::Forbidden(
+ "Modifying admin groups is disabled in demo mode",
+ ));
+ }
// store group before modifications
let before = group.clone();
@@ -720,6 +728,11 @@ pub(crate) async fn remove_group_member(
Path((id, username)): Path<(i64, String)>,
) -> ApiResult {
if let Some(group) = Group::find_by_id(&appstate.pool, id).await? {
+ if server_config().is_demo_mode && group.is_admin {
+ return Err(WebError::Forbidden(
+ "Modifying admin groups is disabled in demo mode",
+ ));
+ }
if let Some(user) = User::find_by_username(&appstate.pool, &username).await? {
debug!(
"Removing user: {} from group: {}",
diff --git a/crates/defguard_core/src/handlers/mail.rs b/crates/defguard_core/src/handlers/mail.rs
index 0f30513190..df558fa1db 100644
--- a/crates/defguard_core/src/handlers/mail.rs
+++ b/crates/defguard_core/src/handlers/mail.rs
@@ -39,6 +39,11 @@ pub(crate) async fn test_mail(
session.user.username, data.to
);
+ if server_config().is_demo_mode {
+ debug!("Demo mode enabled; skipping test mail to {}", data.to);
+ return Ok(ApiResponse::with_status(StatusCode::OK));
+ }
+
let mut conn = appstate.pool.begin().await?;
templates::test_mail(&data.to, &mut conn, Some(&session.session.into())).await?;
@@ -72,6 +77,12 @@ pub async fn send_support_data(
) -> ApiResult {
debug!("User {} sending support mail", session.user.username);
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Sending support data is disabled in demo mode",
+ ));
+ }
+
let mut conn = appstate.pool.begin().await?;
let proxies = Proxy::all(&mut *conn).await?;
let gateways = Gateway::all(&mut *conn).await?;
diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs
index 060b43b22f..794b1bd92f 100644
--- a/crates/defguard_core/src/handlers/openid_flow.rs
+++ b/crates/defguard_core/src/handlers/openid_flow.rs
@@ -448,6 +448,11 @@ pub async fn authorization(
cookies: CookieJar,
private_cookies: PrivateCookieJar,
) -> Result<(StatusCode, HeaderMap, PrivateCookieJar), WebError> {
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "OpenID Connect is disabled in demo mode",
+ ));
+ }
let error;
let mut is_redirect_allowed = false;
if let Some(oauth2client) =
@@ -625,6 +630,11 @@ pub async fn secure_authorization(
Query(data): Query,
private_cookies: PrivateCookieJar,
) -> Result<(StatusCode, HeaderMap, PrivateCookieJar), WebError> {
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "OpenID Connect is disabled in demo mode",
+ ));
+ }
let error;
let mut is_redirect_allowed = false;
if let Some(oauth2client) =
@@ -876,6 +886,11 @@ pub async fn token(
OAuth2ClientExtractor(oauth2client): OAuth2ClientExtractor,
Form(form): Form,
) -> ApiResult {
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "OpenID Connect is disabled in demo mode",
+ ));
+ }
// TODO: cleanup branches
match form.grant_type {
CoreGrantType::AuthorizationCode => {
diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs
index 50eeaef9ca..533ace0b3d 100644
--- a/crates/defguard_core/src/handlers/proxy.rs
+++ b/crates/defguard_core/src/handlers/proxy.rs
@@ -5,6 +5,7 @@ use axum::{
};
use chrono::Utc;
use defguard_common::{
+ config::server_config,
db::{Id, models::proxy::Proxy},
types::proxy::{ProxyControlMessage, ProxyInfo},
};
@@ -45,7 +46,15 @@ pub async fn proxy_list(
) -> ApiResult {
debug!("User {} displaying proxy list", session.user.username);
let proxies = Proxy::list(&appstate.pool).await?;
- let proxies: Vec = proxies.into_iter().map(Into::into).collect();
+ let mut proxies: Vec = proxies.into_iter().map(Into::into).collect();
+ if server_config().is_demo_mode {
+ let now = Utc::now().naive_utc();
+ for proxy in &mut proxies {
+ proxy.enabled = true;
+ proxy.connected_at = Some(now);
+ proxy.disconnected_at = None;
+ }
+ }
info!("User {} displayed proxy list", session.user.username);
Ok(ApiResponse::json(proxies, StatusCode::OK))
@@ -78,7 +87,14 @@ pub(crate) async fn proxy_details(
);
let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?;
let response = match proxy {
- Some(proxy) => ApiResponse::json(proxy, StatusCode::OK),
+ Some(mut proxy) => {
+ if server_config().is_demo_mode {
+ proxy.enabled = true;
+ proxy.connected_at = Some(Utc::now().naive_utc());
+ proxy.disconnected_at = None;
+ }
+ ApiResponse::json(proxy, StatusCode::OK)
+ }
None => ApiResponse::json(Value::Null, StatusCode::NOT_FOUND),
};
info!(
diff --git a/crates/defguard_core/src/handlers/session_info.rs b/crates/defguard_core/src/handlers/session_info.rs
index a7dc92589a..30ec0183be 100644
--- a/crates/defguard_core/src/handlers/session_info.rs
+++ b/crates/defguard_core/src/handlers/session_info.rs
@@ -3,7 +3,7 @@ use defguard_common::db::models::{ActiveWizard, User, Wizard};
use serde::Serialize;
use super::{ApiResponse, ApiResult};
-use crate::{appstate::AppState, auth::SessionExtractor, error::WebError};
+use crate::{appstate::AppState, auth::SessionExtractor, error::WebError, server_config};
#[derive(Serialize)]
struct SessionInfoResponse {
@@ -11,6 +11,7 @@ struct SessionInfoResponse {
is_admin: bool,
active_wizard: Option,
username: Option,
+ demo_mode: bool,
}
pub async fn get_session_info(
@@ -18,6 +19,7 @@ pub async fn get_session_info(
session: Result,
) -> ApiResult {
let pool = &appstate.pool;
+ let demo_mode = server_config().is_demo_mode;
let wizard = Wizard::get(pool).await?;
let active_wizard = if wizard.completed {
None
@@ -32,6 +34,7 @@ pub async fn get_session_info(
is_admin: false,
active_wizard,
username: None,
+ demo_mode,
},
StatusCode::OK,
));
@@ -44,6 +47,7 @@ pub async fn get_session_info(
is_admin: false,
active_wizard,
username: None,
+ demo_mode,
},
StatusCode::OK,
));
@@ -57,6 +61,7 @@ pub async fn get_session_info(
is_admin: user_admin,
active_wizard,
username: Some(user.username),
+ demo_mode,
},
StatusCode::OK,
))
diff --git a/crates/defguard_core/src/handlers/settings.rs b/crates/defguard_core/src/handlers/settings.rs
index 374c13f7c0..387605590b 100644
--- a/crates/defguard_core/src/handlers/settings.rs
+++ b/crates/defguard_core/src/handlers/settings.rs
@@ -3,11 +3,14 @@ use axum::{
extract::{Json, Path, State},
http::StatusCode,
};
-use defguard_common::db::{
- Id,
- models::{
- Settings, SettingsEssentials,
- settings::{LdapSyncStatus, SettingsPatch, update_current_settings},
+use defguard_common::{
+ config::server_config,
+ db::{
+ Id,
+ models::{
+ Settings, SettingsEssentials,
+ settings::{LdapSyncStatus, SettingsPatch, update_current_settings},
+ },
},
};
use sqlx::PgPool;
@@ -34,6 +37,13 @@ pub async fn get_settings(_admin: AdminRole, State(appstate): State) -
if settings.main_logo_url.is_empty() {
settings.main_logo_url = DEFAULT_MAIN_LOGO_URL.into();
}
+ if server_config().is_demo_mode {
+ settings.secret_key = None;
+ settings.license = None;
+ settings.smtp.password = None;
+ settings.smtp.oauth_client_secret = None;
+ settings.smtp.oauth_refresh_token = None;
+ }
return Ok(ApiResponse::json(settings, StatusCode::OK));
}
debug!("Retrieved settings");
@@ -55,6 +65,17 @@ pub(crate) async fn update_settings(
data.uuid = before.uuid;
data.validate()?;
+
+ if server_config().is_demo_mode && data.demo_locked_fields_differ(&before) {
+ return Err(WebError::Forbidden(
+ "This setting is read-only in demo mode",
+ ));
+ }
+
+ if server_config().is_demo_mode {
+ data.ldap_bind_password = data.ldap_bind_password.map(|_| "SECRET".parse().unwrap());
+ }
+
// clone for event
let after = data.clone();
@@ -150,6 +171,18 @@ pub async fn patch_settings(
settings.apply(data);
settings.validate()?;
+ if server_config().is_demo_mode && settings.demo_locked_fields_differ(&before) {
+ return Err(WebError::Forbidden(
+ "This setting is read-only in demo mode",
+ ));
+ }
+
+ if server_config().is_demo_mode {
+ settings.ldap_bind_password = settings
+ .ldap_bind_password
+ .map(|_| "SECRET".parse().unwrap());
+ }
+
// clone for event
let after = settings.clone();
update_current_settings(&appstate.pool, settings).await?;
@@ -168,6 +201,9 @@ pub async fn patch_settings(
pub(crate) async fn test_ldap_settings(_admin: AdminRole, _license: LicenseInfo) -> ApiResult {
debug!("Testing LDAP connection");
+ if server_config().is_demo_mode {
+ return Ok(ApiResponse::with_status(StatusCode::OK));
+ }
match LDAPConnection::create().await {
Ok(_) => {
debug!("LDAP connected successfully");
diff --git a/crates/defguard_core/src/handlers/support.rs b/crates/defguard_core/src/handlers/support.rs
index fd906f8254..ad846e1bcb 100644
--- a/crates/defguard_core/src/handlers/support.rs
+++ b/crates/defguard_core/src/handlers/support.rs
@@ -16,6 +16,12 @@ pub(crate) async fn configuration(
) -> ApiResult {
debug!("User {} dumping app configuration", session.user.username);
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Configuration export is disabled in demo mode",
+ ));
+ }
+
let mut conn = appstate.pool.begin().await?;
Ok(match dump_config(&mut conn).await {
Ok(config) => {
@@ -34,6 +40,9 @@ pub(crate) async fn configuration(
pub(crate) async fn logs(_admin: AdminRole, session: SessionInfo) -> Result {
debug!("User {} dumping app logs", session.user.username);
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden("Log export is disabled in demo mode"));
+ }
if let Some(ref log_file) = server_config().log_file {
match tokio::fs::read_to_string(log_file).await {
Ok(logs) => {
diff --git a/crates/defguard_core/src/handlers/user.rs b/crates/defguard_core/src/handlers/user.rs
index a08d70bf18..bfd52ebac1 100644
--- a/crates/defguard_core/src/handlers/user.rs
+++ b/crates/defguard_core/src/handlers/user.rs
@@ -6,6 +6,7 @@ use axum::{
};
use axum_extra::extract::Query;
use defguard_common::{
+ config::server_config,
db::{
Id,
models::{BiometricAuth, OAuth2AuthorizedApp, Settings, User, WebAuthn, user::SecurityKey},
@@ -889,6 +890,12 @@ pub(crate) async fn modify_user(
Path(username): Path,
Json(user_info): Json,
) -> ApiResult {
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Modifying user's is disabled in demo mode",
+ ));
+ }
+
debug!("User {} updating user {username}", session.user.username);
let mut user = user_for_admin_or_self(&appstate.pool, &session, &username).await?;
let groups_before = UserInfo::from_user(&appstate.pool, user.clone())
@@ -1151,6 +1158,11 @@ pub(crate) async fn change_self_password(
Json(data): Json,
) -> ApiResult {
debug!("User {} is changing his password.", session.user.username);
+ if server_config().is_demo_mode {
+ return Err(WebError::Forbidden(
+ "Changing the password is disabled in demo mode",
+ ));
+ }
let mut user = session.user;
if user.verify_password(&data.old_password).is_err() {
return Ok(ApiResponse::with_status(StatusCode::BAD_REQUEST));
diff --git a/crates/defguard_core/src/handlers/webhooks.rs b/crates/defguard_core/src/handlers/webhooks.rs
index 24c780b55c..c35d083c88 100644
--- a/crates/defguard_core/src/handlers/webhooks.rs
+++ b/crates/defguard_core/src/handlers/webhooks.rs
@@ -2,7 +2,7 @@ use axum::{
extract::{Json, Path, State},
http::StatusCode,
};
-use defguard_common::db::Id;
+use defguard_common::{config::server_config, db::Id};
use super::{ApiResponse, ApiResult, WebHookData};
use crate::{
@@ -21,7 +21,10 @@ pub async fn add_webhook(
) -> ApiResult {
let url = webhookdata.url.clone();
debug!("User {} adding webhook {url}", session.user.username);
- let webhook: WebHook = webhookdata.into();
+ let mut webhook: WebHook = webhookdata.into();
+ if server_config().is_demo_mode {
+ webhook.token = "SECRET".to_string();
+ }
let status = match webhook.save(&appstate.pool).await {
Ok(webhook) => {
info!("User {} added webhook {url}", session.user.username);
@@ -70,7 +73,11 @@ pub async fn change_webhook(
let before = webhook.clone();
webhook.url = data.url;
webhook.description = data.description;
- webhook.token = data.token;
+ webhook.token = if server_config().is_demo_mode {
+ "SECRET".to_string()
+ } else {
+ data.token
+ };
webhook.enabled = data.enabled;
webhook.on_user_created = data.on_user_created;
webhook.on_user_deleted = data.on_user_deleted;
diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs
index c648d93af0..6235c05835 100644
--- a/crates/defguard_core/src/lib.rs
+++ b/crates/defguard_core/src/lib.rs
@@ -22,7 +22,9 @@ use defguard_certs::CertificateAuthority;
use defguard_common::{
VERSION,
auth::claims::{Claims, ClaimsType},
- config::{DefGuardConfig, GatewayConfigArgs, InitVpnLocationArgs, server_config},
+ config::{
+ ChangePasswordArgs, DefGuardConfig, GatewayConfigArgs, InitVpnLocationArgs, server_config,
+ },
db::{
init_db,
models::{
@@ -1182,6 +1184,18 @@ pub async fn gateway_config(
Ok(config)
}
+pub async fn change_user_password(
+ pool: &PgPool,
+ args: &ChangePasswordArgs,
+) -> Result<(), anyhow::Error> {
+ let mut user = User::find_by_username(pool, &args.username)
+ .await?
+ .ok_or_else(|| anyhow!("User {} not found", args.username))?;
+ user.set_password(&args.password);
+ user.save(pool).await?;
+ Ok(())
+}
+
pub fn is_valid_phone_number(number: &str) -> bool {
PHONE_NUMBER_REGEX.is_match(number)
}
diff --git a/crates/defguard_core/src/mail/mod.rs b/crates/defguard_core/src/mail/mod.rs
index b181bd1492..4e5326bc13 100644
--- a/crates/defguard_core/src/mail/mod.rs
+++ b/crates/defguard_core/src/mail/mod.rs
@@ -12,11 +12,14 @@ mod tests;
use std::{str::FromStr, time::Duration};
-use defguard_common::db::models::{
- MFAMethod, Settings,
- settings::{
- defaults::WELCOME_EMAIL_SUBJECT,
- smtp::{SmtpAuthentication, SmtpEncryption, SmtpSettings},
+use defguard_common::{
+ config::server_config,
+ db::models::{
+ MFAMethod, Settings,
+ settings::{
+ defaults::WELCOME_EMAIL_SUBJECT,
+ smtp::{SmtpAuthentication, SmtpEncryption, SmtpSettings},
+ },
},
};
use lettre::{
@@ -213,6 +216,12 @@ impl Mail {
/// Sends email message using SMTP.
pub async fn send(self) -> Result<(), MailError> {
let (to, subject) = (self.to.clone(), self.subject.clone());
+
+ if server_config().is_demo_mode {
+ debug!("Demo mode enabled; skipping outbound mail to: {to}");
+ return Ok(());
+ }
+
debug!("Sending mail to: {to}, subject: {subject}");
// SMTP settings
diff --git a/crates/defguard_event_logger/src/lib.rs b/crates/defguard_event_logger/src/lib.rs
index 6713365e8f..7cd165b279 100644
--- a/crates/defguard_event_logger/src/lib.rs
+++ b/crates/defguard_event_logger/src/lib.rs
@@ -1,5 +1,5 @@
use bytes::Bytes;
-use defguard_common::db::NoId;
+use defguard_common::{config::server_config, db::NoId};
use defguard_core::db::models::activity_log::{
ActivityLogEvent, ActivityLogModule, EventType,
metadata::{
@@ -596,7 +596,11 @@ async fn process_batch(
user_id,
username,
location,
- ip: ip.map(Into::into),
+ ip: if server_config().is_demo_mode {
+ None
+ } else {
+ ip.map(Into::into)
+ },
event,
module,
device,
diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs
index e9a3f15165..d6af12801c 100644
--- a/crates/defguard_gateway_manager/src/handler.rs
+++ b/crates/defguard_gateway_manager/src/handler.rs
@@ -14,6 +14,7 @@ use std::{
use chrono::{DateTime, TimeDelta};
use defguard_common::{
VERSION,
+ config::server_config,
db::{
Id,
models::{
@@ -537,6 +538,13 @@ impl GatewayHandler {
clients: Arc>>,
reconnect_delay: Duration,
) -> Result<(), GatewayError> {
+ if server_config().is_demo_mode {
+ debug!(
+ "Demo mode enabled; not connecting to Gateway id={}",
+ self.gateway.id
+ );
+ std::future::pending::<()>().await;
+ }
loop {
if let Err(err) = self
.handle_connection_iteration(Arc::clone(&clients), true)
diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs
index cab8d652c0..64823be0bc 100644
--- a/crates/defguard_proxy_manager/src/handler.rs
+++ b/crates/defguard_proxy_manager/src/handler.rs
@@ -11,6 +11,7 @@ use axum_extra::extract::cookie::Key;
use chrono::NaiveDateTime;
use defguard_common::{
VERSION,
+ config::server_config,
db::{
Id,
models::{Certificates, Settings, proxy::Proxy},
@@ -273,6 +274,13 @@ impl ProxyHandler {
incompatible_components: Arc>,
certs_rx: watch::Receiver>>,
) -> Result<(), ProxyError> {
+ if server_config().is_demo_mode {
+ debug!(
+ "Demo mode enabled; not connecting to Edge id={}",
+ self.proxy_id
+ );
+ std::future::pending::<()>().await;
+ }
let parsed_version = Version::parse(VERSION)?;
loop {
let channel = match self.connect_channel(certs_rx.clone()).await {
diff --git a/tools/defguard_generator/README.md b/tools/defguard_generator/README.md
index 562f45d238..252b7053d8 100644
--- a/tools/defguard_generator/README.md
+++ b/tools/defguard_generator/README.md
@@ -18,19 +18,19 @@ This means that if you have a development environment set up already it should j
```bash
cargo run -p defguard_generator -- vpn-session-stats \
- --location-id 1 \
--num-users 10 \
--devices-per-user 2 \
--sessions-per-device 5
+
+# optional: limit generation to one VPN location --location-id
```
### Session generation logic
-For each device the generator always starts with creating an active (not disconnected) session.
+For each device the generator always starts with creating an active (not disconnected) session.
If there are more sessions per device to be generated it goes backwards in time and creates
additional disconnected sessions.
Session duration and gaps between sessions are randomized but there is no logic to verify if
sessions are overlapping so by default the generator runs a `TRUNCATE` query at the start.
To disable this behavior (for example when running it multiple times for separate locations)
use the `--no-truncate` CLI flag.
-
diff --git a/tools/defguard_generator/data/device_names.txt b/tools/defguard_generator/data/device_names.txt
new file mode 100644
index 0000000000..42477b4f2d
--- /dev/null
+++ b/tools/defguard_generator/data/device_names.txt
@@ -0,0 +1,39 @@
+Acer Swift
+Apple iMac
+Apple Mac mini
+Apple Mac Pro
+Apple Mac Studio
+Apple MacBook Air
+Apple MacBook Pro
+Asus ROG Zephyrus
+Asus ZenBook
+Chromebook
+Dell Alienware
+Dell Inspiron
+Dell Latitude
+Dell OptiPlex
+Dell Precision
+Dell XPS
+Google Pixel
+Google Pixel Pro
+HP EliteBook
+HP Envy
+HP Pavilion
+HP ProDesk
+HP Spectre
+iPhone
+iPhone Pro
+iPhone Pro Max
+Lenovo IdeaPad
+Lenovo Legion
+Lenovo ThinkCentre
+Lenovo ThinkPad
+Microsoft Surface Laptop
+Microsoft Surface Studio
+Motorola Edge
+OnePlus
+Samsung Galaxy A
+Samsung Galaxy S
+Samsung Galaxy Z Flip
+Samsung Galaxy Z Fold
+Sony Xperia
diff --git a/tools/defguard_generator/data/first_names.txt b/tools/defguard_generator/data/first_names.txt
new file mode 100644
index 0000000000..812402c371
--- /dev/null
+++ b/tools/defguard_generator/data/first_names.txt
@@ -0,0 +1,99 @@
+Elsie
+Edward
+William
+Robert
+Annie
+Katie
+Alma
+Thelma
+Ellen
+Rebecca
+Estella
+Corine
+Margie
+Emmie
+Myrtis
+Pinkie
+Myrtie
+Jeanette
+Rubie
+Maud
+Trudie
+Barbara
+Glennie
+Lassie
+Liza
+Hettie
+Iona
+Lorraine
+Willie
+Fannie
+Sarah
+Myrtle
+Rosie
+Lillian
+Nellie
+Mamie
+Edna
+Evelyn
+Laura
+Mabel
+Josie
+Pearlie
+Rachel
+Era
+Bettie
+Jean
+Anne
+Lucinda
+Dessie
+Linnie
+Marion
+Dovie
+Ira
+Ossie
+Abbie
+Clyde
+Goldie
+Lenora
+Aline
+Gennie
+Vinnie
+Exie
+Tressie
+Joe
+Louis
+Alfred
+Julius
+Sidney
+Nelson
+Isaac
+Jimmy
+King
+Porter
+Tommy
+Cleo
+Dennis
+Gerald
+Moses
+Green
+Luke
+Richard
+Clarence
+Ernest
+Ralph
+Emmett
+Dave
+Harvey
+Ray
+Simon
+Leslie
+Wilson
+Emmitt
+Gordon
+Hollis
+Claud
+Clayton
+Dewey
+Lamar
+Ruth
diff --git a/tools/defguard_generator/data/last_names.txt b/tools/defguard_generator/data/last_names.txt
new file mode 100644
index 0000000000..82753cb26a
--- /dev/null
+++ b/tools/defguard_generator/data/last_names.txt
@@ -0,0 +1,100 @@
+Smith
+Johnson
+Williams
+Brown
+Jones
+Miller
+Davis
+Wilson
+Anderson
+Thomas
+Taylor
+Moore
+Jackson
+Martin
+Thompson
+White
+Harris
+Clark
+Lewis
+Robinson
+Walker
+Young
+Allen
+King
+Wright
+Scott
+Hill
+Green
+Adams
+Nelson
+Baker
+Hall
+Campbell
+Mitchell
+Carter
+Roberts
+Phillips
+Evans
+Turner
+Parker
+Edwards
+Collins
+Stewart
+Morris
+Murphy
+Cook
+Rogers
+Morgan
+Cooper
+Peterson
+Bailey
+Reed
+Kelly
+Howard
+Cox
+Ward
+Richardson
+Watson
+Brooks
+Wood
+James
+Bennett
+Gray
+Hughes
+Price
+Sanders
+Myers
+Long
+Ross
+Foster
+Powell
+Jenkins
+Perry
+Russell
+Sullivan
+Bell
+Coleman
+Henderson
+Barnes
+Simmons
+Patterson
+Jordan
+Reynolds
+Hamilton
+Graham
+Wallace
+Cole
+West
+Hayes
+Woods
+Harrison
+Gibson
+McDonald
+Marshall
+Murray
+Freeman
+Wells
+Webb
+Simpson
+Tucker
diff --git a/tools/defguard_generator/src/acl_rules.rs b/tools/defguard_generator/src/acl_rules.rs
index 9766af7e7f..98fc5444ff 100644
--- a/tools/defguard_generator/src/acl_rules.rs
+++ b/tools/defguard_generator/src/acl_rules.rs
@@ -7,7 +7,8 @@ pub async fn generate_acl_rules(pool: PgPool, num_rules: u32) -> Result<()> {
for index in 0..num_rules {
let mut acl_rule = AclRule::default();
- acl_rule.name = format!("Generated {index}");
+ let name = format!("Generated {index}");
+ acl_rule.name = name;
acl_rule.state = RuleState::Applied;
acl_rule.all_locations = true;
acl_rule.allow_all_users = true;
diff --git a/tools/defguard_generator/src/main.rs b/tools/defguard_generator/src/main.rs
index a3db8cb6aa..59708fc4ee 100644
--- a/tools/defguard_generator/src/main.rs
+++ b/tools/defguard_generator/src/main.rs
@@ -34,7 +34,7 @@ enum Commands {
/// Generates fake VPN session statistics.
VpnSessionStats {
#[arg(long)]
- location_id: Id,
+ location_id: Option,
#[arg(long)]
num_users: usize,
#[arg(long)]
diff --git a/tools/defguard_generator/src/user_devices.rs b/tools/defguard_generator/src/user_devices.rs
index 19b726a2ad..8e71b90873 100644
--- a/tools/defguard_generator/src/user_devices.rs
+++ b/tools/defguard_generator/src/user_devices.rs
@@ -1,12 +1,16 @@
+use std::collections::HashSet;
+
use anyhow::Result;
use defguard_common::db::{
Id,
- models::{Device, User},
+ models::{Device, DeviceType, User},
};
use rand::{Rng, rngs::ThreadRng};
use sqlx::PgPool;
use tracing::info;
+const DEVICE_NAMES: &str = include_str!("../data/device_names.txt");
+
pub async fn prepare_user_devices(
pool: &PgPool,
rng: &mut ThreadRng,
@@ -24,14 +28,34 @@ pub async fn prepare_user_devices(
);
return Ok(user_devices[..devices_per_user].to_vec());
}
+ let device_names: Vec<&str> = DEVICE_NAMES.lines().collect();
+ let mut taken_names: HashSet = user_devices
+ .iter()
+ .map(|device| device.name.clone())
+ .collect();
// if there are not enough users create new ones
for _ in 0..(devices_per_user - user_devices.len()) {
let mut device: Device = rng.r#gen();
+ let base_name = device_names[rng.gen_range(0..device_names.len())];
+ device.name = unique_device_name(base_name, &mut taken_names);
device.user_id = user.id;
+ device.device_type = DeviceType::User;
+ device.description = None;
let device = device.save(pool).await?;
user_devices.push(device);
}
Ok(user_devices)
}
+
+fn unique_device_name(base: &str, taken: &mut HashSet) -> String {
+ let mut name = base.to_string();
+ let mut suffix = 1;
+ while taken.contains(&name) {
+ suffix += 1;
+ name = format!("{base}{suffix}");
+ }
+ taken.insert(name.clone());
+ name
+}
diff --git a/tools/defguard_generator/src/users.rs b/tools/defguard_generator/src/users.rs
index b223b6d120..821da71a4e 100644
--- a/tools/defguard_generator/src/users.rs
+++ b/tools/defguard_generator/src/users.rs
@@ -1,9 +1,14 @@
+use std::collections::HashSet;
+
use anyhow::Result;
use defguard_common::db::{Id, models::User};
use rand::{Rng, rngs::ThreadRng};
use sqlx::PgPool;
use tracing::info;
+const FIRST_NAMES: &str = include_str!("../data/first_names.txt");
+const LAST_NAMES: &str = include_str!("../data/last_names.txt");
+
pub async fn prepare_users(
pool: &PgPool,
rng: &mut ThreadRng,
@@ -23,12 +28,36 @@ pub async fn prepare_users(
return Ok(all_users[..num_users].to_vec());
}
+ let first_names: Vec<&str> = FIRST_NAMES.lines().collect();
+ let last_names: Vec<&str> = LAST_NAMES.lines().collect();
+ let mut taken_usernames: HashSet =
+ all_users.iter().map(|user| user.username.clone()).collect();
+
// if there are not enough users create new ones
for _ in 0..(num_users - all_users.len()) {
- let user: User = rng.r#gen();
+ let mut user: User = rng.r#gen();
+ let first_name = first_names[rng.gen_range(0..first_names.len())];
+ let last_name = last_names[rng.gen_range(0..last_names.len())];
+ user.username = unique_username(first_name, last_name, &mut taken_usernames);
+ user.first_name = first_name.to_string();
+ user.last_name = last_name.to_string();
+ user.email = format!("{}@defguard.net", user.username);
+ user.phone = Some("123454321".to_string());
let user = user.save(pool).await?;
all_users.push(user);
}
Ok(all_users)
}
+
+fn unique_username(first_name: &str, last_name: &str, taken: &mut HashSet) -> String {
+ let base = format!("{first_name}.{last_name}").to_lowercase();
+ let mut username = base.clone();
+ let mut suffix = 1;
+ while taken.contains(&username) {
+ suffix += 1;
+ username = format!("{base}{suffix}");
+ }
+ taken.insert(username.clone());
+ username
+}
diff --git a/tools/defguard_generator/src/vpn_session_stats.rs b/tools/defguard_generator/src/vpn_session_stats.rs
index 21749ed663..6e008a7483 100644
--- a/tools/defguard_generator/src/vpn_session_stats.rs
+++ b/tools/defguard_generator/src/vpn_session_stats.rs
@@ -12,7 +12,7 @@ use defguard_common::db::{
vpn_session_stats::VpnSessionStats,
},
};
-use rand::{Rng, rngs::ThreadRng};
+use rand::{Rng, rngs::ThreadRng, seq::SliceRandom};
use sqlx::{PgConnection, PgPool, QueryBuilder, query};
use tracing::{debug, info};
@@ -23,7 +23,7 @@ const HANDSHAKE_INTERVAL: Duration = Duration::minutes(2);
#[derive(Debug)]
pub struct VpnSessionGeneratorConfig {
- pub location_id: Id,
+ pub location_id: Option,
pub num_users: usize,
pub devices_per_user: u8,
pub sessions_per_device: u8,
@@ -36,7 +36,6 @@ pub async fn generate_vpn_session_stats(
config: VpnSessionGeneratorConfig,
) -> Result<()> {
info!("Running VPN stats generator with config: {config:#?}");
- let mut rng = rand::thread_rng();
// clear sessions & stats tables unless disabled
if !config.no_truncate {
@@ -44,17 +43,47 @@ pub async fn generate_vpn_session_stats(
truncate_with_restart(&pool).await?;
}
- // fetch specified location
- let location = WireguardNetwork::find_by_id(&pool, config.location_id)
- .await?
- .expect("Location not found");
+ let locations = match config.location_id {
+ Some(location_id) => {
+ let location = WireguardNetwork::find_by_id(&pool, location_id)
+ .await?
+ .expect("Location not found");
+ vec![location]
+ }
+ None => WireguardNetwork::all(&pool).await?,
+ };
+
+ info!("Generating stats for {} VPN location(s)", locations.len());
+
+ for location in locations {
+ generate_stats_for_location(&pool, &config, location).await?;
+ }
+
+ Ok(())
+}
+
+async fn generate_stats_for_location(
+ pool: &PgPool,
+ config: &VpnSessionGeneratorConfig,
+ location: WireguardNetwork,
+) -> Result<()> {
+ let mut rng = rand::thread_rng();
+
+ let location_seed = rng.gen_range(0.0..1_000.0);
+
+ info!(
+ "Generating VPN stats for location {} ({})",
+ location.name, location.id
+ );
// prepare a gateway
- let gateway = prepare_gateway(&pool, location.id).await?;
+ let gateway = prepare_gateway(pool, location.id).await?;
// prepare requested number of users
- let user_count = config.num_users;
- let users = prepare_users(&pool, &mut rng, user_count).await?;
+ let mut users = prepare_users(pool, &mut rng, config.num_users).await?;
+ users.shuffle(&mut rng);
+ let user_count = rng.gen_range((config.num_users / 2).max(1)..=config.num_users.max(1));
+ users.truncate(user_count);
// generate sessions for each user
for (i, user) in users.into_iter().enumerate() {
@@ -67,8 +96,8 @@ pub async fn generate_vpn_session_stats(
let mut transaction = pool.begin().await?;
// prepare requested number of devices
- let devices =
- prepare_user_devices(&pool, &mut rng, &user, config.devices_per_user as usize).await?;
+ let device_count = rng.gen_range(1..=config.devices_per_user.max(1)) as usize;
+ let devices = prepare_user_devices(pool, &mut rng, &user, device_count).await?;
let mut used_ips = location.all_used_ips_for_network(&mut transaction).await?;
// assign devices to the network if not already assigned
@@ -101,8 +130,9 @@ pub async fn generate_vpn_session_stats(
// start with the active session
let mut session_end = Utc::now().naive_utc();
+ let session_count = rng.gen_range(1..=config.sessions_per_device.max(1));
- for i in 0..config.sessions_per_device {
+ for i in 0..session_count {
let session_duration = Duration::minutes(rng.gen_range(10..120));
let session_start = session_end - session_duration;
@@ -112,17 +142,16 @@ pub async fn generate_vpn_session_stats(
device.id,
Some(session_start),
None,
- )
- .save(&mut *transaction)
- .await?;
+ );
// mark all but the first session as disconnected
if i > 0 {
session.state = VpnClientSessionState::Disconnected;
session.disconnected_at = Some(session_end);
- session.save(&mut *transaction).await?;
}
+ let session = session.save(&mut *transaction).await?;
+
debug!("Created session {session:?}");
generate_mock_session_stats(
@@ -133,6 +162,7 @@ pub async fn generate_vpn_session_stats(
session_start,
session_end,
config.stats_batch_size,
+ location_seed,
)
.await?;
@@ -171,7 +201,7 @@ async fn prepare_gateway(pool: &PgPool, location_id: Id) -> Result>
}
}
}
-
+#[allow(clippy::too_many_arguments)]
async fn generate_mock_session_stats(
transaction: &mut PgConnection,
rng: &mut ThreadRng,
@@ -180,6 +210,7 @@ async fn generate_mock_session_stats(
session_start: NaiveDateTime,
session_end: NaiveDateTime,
batch_size: u16,
+ location_seed: f64,
) -> Result<()> {
let mut latest_handshake = session_start;
let mut next_handshake = latest_handshake + HANDSHAKE_INTERVAL;
@@ -190,14 +221,23 @@ async fn generate_mock_session_stats(
// assume the IP remains static within a single session
let endpoint = random_socket_addr(rng).to_string();
+ let upload_scale = rng.gen_range(20_000.0..400_000.0);
+ let download_scale = rng.gen_range(20_000.0..400_000.0);
+
// Vector to accumulate stats before batch insertion
let mut stats_batch: Vec = Vec::new();
while collected_at <= session_end {
+ let minutes = collected_at.and_utc().timestamp() as f64 / 60.0;
+ let activity = |phase: f64| {
+ (0.5 + 0.35 * (minutes * 0.5 + phase + location_seed).sin()).clamp(0.05, 1.0)
+ };
+
// generate traffic
- let upload_diff = rng.gen_range(100..100_000);
+ let upload_diff = (upload_scale * activity(0.0) * rng.gen_range(0.2..1.8)).max(1.0) as i64;
total_upload += upload_diff;
- let download_diff = rng.gen_range(100..100_000);
+ let download_diff =
+ (download_scale * activity(2.0) * rng.gen_range(0.2..1.8)).max(1.0) as i64;
total_download += download_diff;
let stats = VpnSessionStats::new(
@@ -208,7 +248,7 @@ async fn generate_mock_session_stats(
endpoint.clone(),
total_upload,
total_download,
- download_diff,
+ upload_diff,
download_diff,
);
diff --git a/web/messages/en/auth.json b/web/messages/en/auth.json
index e3faa2a44d..039076f8ce 100644
--- a/web/messages/en/auth.json
+++ b/web/messages/en/auth.json
@@ -6,6 +6,8 @@
"login_main_sign_in_with_external_provider": "Sign in with an external provider",
"login_main_forgot": "If you forgot your password, contact your organization's administrator.",
"login_main_attempts_info": "Too many login attempts. Please try again later.",
+ "login_main_demo_info": "This is a demo instance. Sign in with login \"demo\" and password \"demo\".",
+ "login_main_demo_reset_info": "This instance is reset every 15 minutes. Any changes will be lost, and you may be logged out.",
"login_error_invalid": "username or password is incorrect.",
"login_mfa_title": "Two-factor authentication",
"login_totp_subtitle": "Use code from your authentication app to proceed.",
diff --git a/web/messages/en/common.json b/web/messages/en/common.json
index 3c2daa5946..079fdcffc6 100644
--- a/web/messages/en/common.json
+++ b/web/messages/en/common.json
@@ -10,6 +10,7 @@
"misc_clipboard_copy": "Copied to clipboard!",
"misc_secret": "Secret",
"misc_expired": "Expired",
+ "demo_mode_feature_disabled": "This option is disabled in demo mode",
"license_business_required": "Available in Business plan.",
"license_upgrade_business_tooltip": "This feature is part of a paid plan.\nUpgrade to Business to activate it.",
"license_enterprise_required": "Available in Enterprise plan.",
diff --git a/web/src/pages/ActivityLogPage/ActivityLogTable.tsx b/web/src/pages/ActivityLogPage/ActivityLogTable.tsx
index ff2bc56f91..a475ae0a16 100644
--- a/web/src/pages/ActivityLogPage/ActivityLogTable.tsx
+++ b/web/src/pages/ActivityLogPage/ActivityLogTable.tsx
@@ -17,6 +17,7 @@ import { TableCell } from '../../shared/defguard-ui/components/table/TableCell/T
import { TableTop } from '../../shared/defguard-ui/components/table/TableTop/TableTop';
import { useApiToTableState } from '../../shared/defguard-ui/hooks/useApiToTableState';
import { isPresent } from '../../shared/defguard-ui/utils/isPresent';
+import { useApp } from '../../shared/hooks/useApp';
import { displayDate } from '../../shared/utils/displayDate';
import { formatIpForDisplay } from '../../shared/utils/formatIpForDisplay';
@@ -60,6 +61,8 @@ export const ActivityLogTable = ({
defaultSortingKey: 'timestamp',
});
+ const demoMode = useApp((s) => s.appInfo.demo_mode);
+
const columns = useMemo(
() => [
columnHelper.accessor('timestamp', {
@@ -89,6 +92,13 @@ export const ActivityLogTable = ({
header: m.activity_log_col_ip(),
minSize: 150,
cell: (info) => {
+ if (demoMode) {
+ return (
+
+ {''}
+
+ );
+ }
const value = info.getValue();
const displayValue = isPresent(value) ? formatIpForDisplay(value) : value;
return (
@@ -163,7 +173,7 @@ export const ActivityLogTable = ({
),
}),
],
- [],
+ [demoMode],
);
const table = useReactTable({
diff --git a/web/src/pages/GroupsPage/modals/CEGroupModal/CEGroupModal.tsx b/web/src/pages/GroupsPage/modals/CEGroupModal/CEGroupModal.tsx
index fc8742b4a6..0fb1f2017a 100644
--- a/web/src/pages/GroupsPage/modals/CEGroupModal/CEGroupModal.tsx
+++ b/web/src/pages/GroupsPage/modals/CEGroupModal/CEGroupModal.tsx
@@ -18,10 +18,12 @@ import type { SelectionOption } from '../../../../shared/components/SelectionSec
import { Checkbox } from '../../../../shared/defguard-ui/components/Checkbox/Checkbox';
import { Divider } from '../../../../shared/defguard-ui/components/Divider/Divider';
import { ModalControls } from '../../../../shared/defguard-ui/components/ModalControls/ModalControls';
+import { Snackbar } from '../../../../shared/defguard-ui/providers/snackbar/snackbar';
import { ThemeSpacing } from '../../../../shared/defguard-ui/types';
import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent';
import { useAppForm } from '../../../../shared/form';
import { formChangeLogic } from '../../../../shared/formLogic';
+import { useApp } from '../../../../shared/hooks/useApp';
interface ModalState extends OpenCEGroupModal {
step: 'start' | 'users';
@@ -90,6 +92,7 @@ const userToOption = (user: User): SelectionOption => ({
});
const UsersStep = ({ users, startForm, groupInfo, isEdit, setModalState }: StepProps) => {
+ const demoMode = useApp((s) => s.appInfo.demo_mode);
const { mutate: editGroup, isPending: editPending } = useMutation({
mutationFn: api.group.editGroup,
meta: {
@@ -117,6 +120,10 @@ const UsersStep = ({ users, startForm, groupInfo, isEdit, setModalState }: StepP
const handleSubmit = () => {
if (startForm && !addPending && !editPending) {
+ if (demoMode && groupInfo?.is_admin) {
+ Snackbar.error(m.demo_mode_feature_disabled());
+ return;
+ }
const members = Array.from(selected);
const requestData = {
...startForm,
diff --git a/web/src/pages/SupportPage/SupportPage.tsx b/web/src/pages/SupportPage/SupportPage.tsx
index ca7ebb2784..42fbed2b1e 100644
--- a/web/src/pages/SupportPage/SupportPage.tsx
+++ b/web/src/pages/SupportPage/SupportPage.tsx
@@ -22,7 +22,9 @@ import { MarkedSection } from '../../shared/defguard-ui/components/MarkedSection
import { MarkedSectionHeader } from '../../shared/defguard-ui/components/MarkedSectionHeader/MarkedSectionHeader';
import docIllustration from '../../shared/defguard-ui/components/SectionSelect/assets/manual-user.png';
import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox';
+import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar';
import { TextStyle, ThemeSpacing, ThemeVariable } from '../../shared/defguard-ui/types';
+import { useApp } from '../../shared/hooks/useApp';
import { getLicenseInfoQueryOptions } from '../../shared/query';
import { downloadFile } from '../../shared/utils/download';
@@ -48,6 +50,7 @@ export const SupportPage = () => {
const PageContent = () => {
const { data: licenseInfo } = useSuspenseQuery(getLicenseInfoQueryOptions);
+ const demoMode = useApp((s) => s.appInfo.demo_mode);
const supportType = useMemo(
() => licenseInfo?.support_type_narrow ?? 'Free',
@@ -112,6 +115,10 @@ const PageContent = () => {
{
text: m.support_page_bug_btn_download_support_data(),
onClick: async () => {
+ if (demoMode) {
+ Snackbar.error(m.demo_mode_feature_disabled());
+ return;
+ }
const res = await api.support.getSupportData();
const blob = new Blob([JSON.stringify(res.data, null, 2)], {
type: 'application/json',
@@ -162,7 +169,7 @@ const PageContent = () => {
>
)}
- {supportType === 'Direct' && (
+ {supportType === 'Direct' && !demoMode && (
<>
diff --git a/web/src/pages/UsersOverviewPage/modals/EditUserModal/EditUserModal.tsx b/web/src/pages/UsersOverviewPage/modals/EditUserModal/EditUserModal.tsx
index d0633a8874..99a7bb494e 100644
--- a/web/src/pages/UsersOverviewPage/modals/EditUserModal/EditUserModal.tsx
+++ b/web/src/pages/UsersOverviewPage/modals/EditUserModal/EditUserModal.tsx
@@ -11,6 +11,7 @@ import { EvenSplit } from '../../../../shared/defguard-ui/components/EvenSplit/E
import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal';
import { ModalControls } from '../../../../shared/defguard-ui/components/ModalControls/ModalControls';
import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox';
+import { Snackbar } from '../../../../shared/defguard-ui/providers/snackbar/snackbar';
import { TextStyle, ThemeSpacing } from '../../../../shared/defguard-ui/types';
import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent';
import { useAppForm } from '../../../../shared/form';
@@ -22,6 +23,7 @@ import {
} from '../../../../shared/hooks/modalControls/modalsSubjects';
import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes';
import type { OpenEditUserModal } from '../../../../shared/hooks/modalControls/types';
+import { useApp } from '../../../../shared/hooks/useApp';
import { patternSafeUsernameCharacters } from '../../../../shared/patterns';
import { removeEmptyStrings } from '../../../../shared/utils/removeEmptyStrings';
@@ -118,6 +120,7 @@ const ModalContent = ({ user }: ModalData) => {
});
const isSubmitting = useStore(form.store, (s) => s.isSubmitting);
+ const demoMode = useApp((s) => s.appInfo.demo_mode);
return (
<>
@@ -226,6 +229,10 @@ const ModalContent = ({ user }: ModalData) => {
text: m.controls_save_changes(),
loading: isSubmitting,
onClick: () => {
+ if (demoMode) {
+ Snackbar.error(m.demo_mode_feature_disabled());
+ return;
+ }
form.handleSubmit();
},
}}
diff --git a/web/src/pages/auth/LoginMain/LoginMainPage.tsx b/web/src/pages/auth/LoginMain/LoginMainPage.tsx
index c5edcbff8f..e5acba73ac 100644
--- a/web/src/pages/auth/LoginMain/LoginMainPage.tsx
+++ b/web/src/pages/auth/LoginMain/LoginMainPage.tsx
@@ -18,6 +18,7 @@ import { OIDCButton } from '../../../shared/defguard-ui/components/SSOButton/OID
import { isPresent } from '../../../shared/defguard-ui/utils/isPresent';
import { createZodIssue } from '../../../shared/defguard-ui/utils/zod';
import { useAuth } from '../../../shared/hooks/useAuth';
+import { getSessionInfoQueryOptions } from '../../../shared/query';
const formSchema = z.object({
username: z.string(m.form_error_required()).trim().min(1, m.form_error_required()),
@@ -42,6 +43,8 @@ export const LoginMainPage = () => {
retry: false,
});
+ const { data: sessionInfo } = useQuery(getSessionInfoQueryOptions);
+
const form = useAppForm({
defaultValues: defaults,
validationLogic: revalidateLogic({
@@ -99,6 +102,23 @@ export const LoginMainPage = () => {
{m.login_main_title()}
{m.login_main_subtitle()}
+ {isPresent(sessionInfo) && sessionInfo?.demo_mode && (
+ <>
+
+
+
+
+
+ >
+ )}
{tooManyAttempts && (
<>
{
select: (resp) => resp.data,
});
+ const demoMode = useApp((s) => s.appInfo.demo_mode);
+
const downloadCaCert = useCallback(() => {
const caPem = caData?.ca_cert_pem;
if (!isPresent(caPem)) return;
@@ -91,6 +95,22 @@ const Content = () => {
downloadFile(blob, 'defguard-ca', 'pem');
}, [caData?.ca_cert_pem]);
+ const changeCoreCert = useCallback(() => {
+ if (demoMode) {
+ Snackbar.error(m.demo_mode_feature_disabled());
+ return;
+ }
+ void navigate({ to: '/settings-core-certificate' });
+ }, [demoMode, navigate]);
+
+ const changeEdgeCert = useCallback(() => {
+ if (demoMode) {
+ Snackbar.error(m.demo_mode_feature_disabled());
+ return;
+ }
+ void navigate({ to: '/settings-edge-certificate' });
+ }, [demoMode, navigate]);
+
return (
<>
@@ -104,7 +124,7 @@ const Content = () => {