diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 08b3499..17c36ea 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -7,6 +7,10 @@ on: 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 env: GHCR_REPO: ghcr.io/defguard/defguard-proxy @@ -19,12 +23,17 @@ jobs: - ${{ matrix.runner }} strategy: matrix: - cpu: [arm64, amd64] + cpu: [arm64, amd64, arm/v7] include: - cpu: arm64 runner: ARM64 + tag: arm64 - cpu: amd64 runner: X64 + tag: amd64 + - cpu: arm/v7 + runner: ARM + tag: armv7 steps: - name: Checkout uses: actions/checkout@v4 @@ -49,7 +58,7 @@ jobs: platforms: linux/${{ matrix.cpu }} provenance: false push: true - tags: ${{ env.GHCR_REPO }}:${{ github.sha }}-${{ matrix.cpu }} + tags: "${{ env.GHCR_REPO }}:${{ github.sha }}-${{ matrix.tag }}" cache-from: type=gha cache-to: type=gha,mode=max @@ -63,6 +72,7 @@ jobs: with: images: | ${{ env.GHCR_REPO }} + flavor: ${{ inputs.flavor }} tags: ${{ inputs.tags }} - name: Login to GitHub container registry uses: docker/login-action@v3 @@ -75,6 +85,7 @@ jobs: tags='${{ env.GHCR_REPO }}:${{ github.sha }} ${{ steps.meta.outputs.tags }}' for tag in ${tags} do - docker manifest create --amend ${tag} ${{ env.GHCR_REPO }}:${{ github.sha }}-amd64 ${{ env.GHCR_REPO }}:${{ github.sha }}-arm64 + docker manifest rm ${tag} || true + docker manifest create ${tag} ${{ env.GHCR_REPO }}:${{ github.sha }}-amd64 ${{ env.GHCR_REPO }}:${{ github.sha }}-arm64 ${{ env.GHCR_REPO }}:${{ github.sha }}-armv7 docker manifest push ${tag} done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1952c9b..bedd591 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,12 @@ concurrency: cancel-in-progress: true jobs: - build-latest: + 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: | @@ -19,6 +24,20 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=sha + 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 diff --git a/Cargo.lock b/Cargo.lock index f90a09d..0ce6491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -152,7 +152,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -267,7 +267,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -346,6 +346,8 @@ version = "1.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -396,7 +398,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -486,6 +488,41 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.76", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.76", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -501,7 +538,7 @@ dependencies = [ [[package]] name = "defguard-proxy" -version = "0.5.1" +version = "1.0.0" dependencies = [ "anyhow", "axum", @@ -528,6 +565,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "vergen-git2", ] [[package]] @@ -539,6 +577,37 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" +dependencies = [ + "derive_builder_core", + "syn 2.0.76", +] + [[package]] name = "digest" version = "0.10.7" @@ -680,7 +749,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -740,6 +809,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ghash" version = "0.5.1" @@ -756,6 +837,19 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "globset" version = "0.4.14" @@ -968,6 +1062,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1028,6 +1128,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.70" @@ -1049,6 +1158,30 @@ version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1181,6 +1314,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.36.3" @@ -1270,7 +1412,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -1285,6 +1427,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "polyval" version = "0.6.2" @@ -1325,7 +1473,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.76", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -1364,7 +1536,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn", + "syn 2.0.76", "tempfile", ] @@ -1378,7 +1550,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -1541,7 +1713,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 2.0.76", "walkdir", ] @@ -1706,7 +1878,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -1841,6 +2013,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.76" @@ -1894,7 +2077,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -1915,7 +2098,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -1977,7 +2162,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -2093,7 +2278,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -2189,7 +2374,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -2312,6 +2497,52 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c32e7318e93a9ac53693b6caccfb05ff22e04a44c7cf8a279051f24c09da286f" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen-lib", +] + +[[package]] +name = "vergen-git2" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62c52cd2b2b8b7ec75fc20111b3022ac3ff83e4fc14b9497cfcfd39c54f9c67" +dependencies = [ + "anyhow", + "derive_builder", + "git2", + "rustversion", + "time", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06bee42361e43b60f363bad49d63798d0f42fb1768091812270eca00c784720" +dependencies = [ + "anyhow", + "derive_builder", + "getset", + "rustversion", +] + [[package]] name = "version_check" version = "0.9.5" @@ -2365,7 +2596,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.76", "wasm-bindgen-shared", ] @@ -2387,7 +2618,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2548,7 +2779,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7d7ee08..b0ca2ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard-proxy" -version = "0.5.1" +version = "1.0.0" edition = "2021" license = "Apache-2.0" homepage = "https://github.com/DefGuard/proxy" @@ -47,6 +47,7 @@ mime_guess = "2.0" [build-dependencies] tonic-build = { version = "0.12" } prost-build = { version = "0.13" } +vergen-git2 = { version = "1.0", features = ["build"] } [profile.release] lto = "thin" diff --git a/Dockerfile b/Dockerfile index 48d7c2f..27a0a05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,8 @@ COPY --from=web /app/dist ./web/dist COPY web/src/shared/images/svg ./web/src/shared/images/svg RUN apt-get update && apt-get -y install protobuf-compiler libprotobuf-dev COPY Cargo.toml Cargo.lock build.rs ./ +# for vergen +COPY .git .git COPY src src COPY proto proto RUN cargo install --locked --path . --root /build diff --git a/build.rs b/build.rs index b061c0d..8eb4a15 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,10 @@ +use vergen_git2::{Emitter, Git2Builder}; + fn main() -> Result<(), Box> { + // set VERGEN_GIT_SHA env variable based on git commit hash + let git2 = Git2Builder::default().branch(true).sha(true).build()?; + Emitter::default().add_instructions(&git2)?.emit()?; + // compiling protos using path on build time let mut config = prost_build::Config::new(); // enable optional fields diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..bad9762 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1728018373, + "narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "bc947f541ae55e999ffdb4013441347d83b00feb", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5cc26fe --- /dev/null +++ b/flake.nix @@ -0,0 +1,26 @@ +{ + description = "Rust development flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs, utils, ... }: + utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs {inherit system;}; + in { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + pkg-config + openssl + protobuf + sqlx-cli + cargo + rustc + rust-analyzer + clippy + ]; + }; + }); +} diff --git a/proto b/proto index c71f378..8309982 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit c71f37847279ee23220fcf9e0e45d2c365b3b8ee +Subproject commit 8309982b94e82a7cbe39dd529967f43e49b3ef1d diff --git a/src/config.rs b/src/config.rs index b0cc1d8..994cdf7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,6 @@ use std::{fs, io::Error as IoError}; use clap::Parser; use log::LevelFilter; use serde::Deserialize; -use tracing::info; #[derive(Parser, Debug, Deserialize)] #[command(version)] diff --git a/src/error.rs b/src/error.rs index d5eec03..fec4b2c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,12 @@ -use crate::proto::CoreError; use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use serde_json::json; -use tonic::metadata::errors::InvalidMetadataValue; -use tonic::{Code, Status}; +use tonic::{metadata::errors::InvalidMetadataValue, Code, Status}; + +use crate::proto::CoreError; #[derive(thiserror::Error, Debug)] pub enum ApiError { @@ -24,6 +24,8 @@ pub enum ApiError { InvalidResponseType, #[error("Permission denied: {0}")] PermissionDenied(String), + #[error("Enterprise not enabled")] + EnterpriseNotEnabled, } impl IntoResponse for ApiError { @@ -33,6 +35,10 @@ impl IntoResponse for ApiError { Self::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg), Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), Self::PermissionDenied(msg) => (StatusCode::FORBIDDEN, msg), + Self::EnterpriseNotEnabled => ( + StatusCode::PAYMENT_REQUIRED, + "Enterprise features are not enabled".to_string(), + ), _ => ( StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string(), @@ -55,6 +61,11 @@ impl From for ApiError { Code::Unauthenticated => ApiError::Unauthorized(status.message().to_string()), Code::InvalidArgument => ApiError::BadRequest(status.message().to_string()), Code::PermissionDenied => ApiError::PermissionDenied(status.message().to_string()), + Code::FailedPrecondition => match status.message().to_lowercase().as_str() { + // TODO: find a better way than matching on the error message + "no valid license" => ApiError::EnterpriseNotEnabled, + _ => ApiError::Unexpected(status.to_string()), + }, _ => ApiError::Unexpected(status.to_string()), } } diff --git a/src/handlers/desktop_client_mfa.rs b/src/handlers/desktop_client_mfa.rs index 858ff4b..0f8b576 100644 --- a/src/handlers/desktop_client_mfa.rs +++ b/src/handlers/desktop_client_mfa.rs @@ -1,5 +1,4 @@ use axum::{extract::State, routing::post, Json, Router}; -use tracing::{error, info}; use crate::{ error::ApiError, diff --git a/src/handlers/enrollment.rs b/src/handlers/enrollment.rs index 9a7c4ec..bb8d5da 100644 --- a/src/handlers/enrollment.rs +++ b/src/handlers/enrollment.rs @@ -82,12 +82,11 @@ pub async fn activate_user( let payload = get_core_response(rx).await?; debug!("Receving payload from the core service. Trying to remove private cookie..."); if let core_response::Payload::Empty(()) = payload { + info!("Activated user - phone number {phone:?}"); if let Some(cookie) = private_cookies.get(ENROLLMENT_COOKIE_NAME) { - info!("Activated user - phone number {phone:?}"); debug!("Enrollment finished. Removing session cookie"); private_cookies = private_cookies.remove(cookie); } - Ok(private_cookies) } else { error!("Received invalid gRPC response type: {payload:#?}"); diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 1fb53e9..4d77261 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,15 +1,17 @@ -pub(crate) mod desktop_client_mfa; -pub(crate) mod enrollment; -pub(crate) mod password_reset; +use std::time::Duration; -use crate::{error::ApiError, proto::core_response::Payload}; use axum::{extract::FromRequestParts, http::request::Parts}; use axum_client_ip::{InsecureClientIp, LeftmostXForwardedFor}; use axum_extra::{headers::UserAgent, TypedHeader}; -use std::time::Duration; use tokio::{sync::oneshot::Receiver, time::timeout}; use super::proto::DeviceInfo; +use crate::{error::ApiError, proto::core_response::Payload}; + +pub(crate) mod desktop_client_mfa; +pub(crate) mod enrollment; +pub(crate) mod password_reset; +pub(crate) mod polling; // timeout in seconds for awaiting core response const CORE_RESPONSE_TIMEOUT: u64 = 5; diff --git a/src/handlers/polling.rs b/src/handlers/polling.rs new file mode 100644 index 0000000..3820196 --- /dev/null +++ b/src/handlers/polling.rs @@ -0,0 +1,28 @@ +use axum::{extract::State, Json}; + +use crate::{ + error::ApiError, + handlers::get_core_response, + http::AppState, + proto::{core_request, core_response, InstanceInfoRequest, InstanceInfoResponse}, +}; + +#[instrument(level = "debug", skip(state))] +pub async fn info( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + debug!("Retrieving info for polling request"); + let rx = state + .grpc_server + .send(Some(core_request::Payload::InstanceInfo(req.clone())), None)?; + let payload = get_core_response(rx).await?; + + if let core_response::Payload::InstanceInfo(response) = payload { + info!("Retrieved info for polling request"); + Ok(Json(response)) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) + } +} diff --git a/src/http.rs b/src/http.rs index 3bdfa7a..a6be8d3 100644 --- a/src/http.rs +++ b/src/http.rs @@ -10,7 +10,7 @@ use axum::{ body::Body, extract::{ConnectInfo, FromRef, State}, http::{Request, StatusCode}, - routing::get, + routing::{get, post}, serve, Json, Router, }; use axum_extra::extract::cookie::Key; @@ -29,7 +29,7 @@ use crate::{ config::Config, error::ApiError, grpc::ProxyServer, - handlers::{desktop_client_mfa, enrollment, password_reset}, + handlers::{desktop_client_mfa, enrollment, password_reset, polling}, proto::proxy_server, }; @@ -188,6 +188,7 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { .nest("/enrollment", enrollment::router()) .nest("/password-reset", password_reset::router()) .nest("/client-mfa", desktop_client_mfa::router()) + .route("/poll", post(polling::info)) .route("/health", get(healthcheck)) .route("/health-grpc", get(healthcheckgrpc)) .route("/info", get(app_info)), diff --git a/src/lib.rs b/src/lib.rs index ae7515d..d098454 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,3 +12,5 @@ pub(crate) mod proto { #[macro_use] extern crate tracing; + +pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA")); diff --git a/src/main.rs b/src/main.rs index f92d1ed..1e22f07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use defguard_proxy::{config::get_config, http::run_server, logging::init_tracing}; +use defguard_proxy::{config::get_config, http::run_server, logging::init_tracing, VERSION}; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -10,6 +10,7 @@ async fn main() -> anyhow::Result<()> { // read config from env let config = get_config()?; init_tracing(&config.log_level); + tracing::info!("Starting ... version v{}", VERSION); // run API web server run_server(config).await?; diff --git a/web/src/components/LogoContainer/LogoContainer.tsx b/web/src/components/LogoContainer/LogoContainer.tsx index 34f2525..89abd82 100644 --- a/web/src/components/LogoContainer/LogoContainer.tsx +++ b/web/src/components/LogoContainer/LogoContainer.tsx @@ -8,9 +8,13 @@ import SvgTeoniteLogo from '../../shared/components/svg/TeoniteLogo'; export const LogoContainer = () => { return (
- + + + - + + +
); }; diff --git a/web/src/components/LogoContainer/style.scss b/web/src/components/LogoContainer/style.scss index 76af298..3f2ceb8 100644 --- a/web/src/components/LogoContainer/style.scss +++ b/web/src/components/LogoContainer/style.scss @@ -13,13 +13,13 @@ height: 100%; } - & > .defguard { + & > a > .defguard { path { fill: var(--text-body-primary); } } - & > .teonite { + & > a > .teonite { path { fill: var(--surface-teonite-logo); } diff --git a/web/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx b/web/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx index b1a573c..b968aad 100644 --- a/web/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx +++ b/web/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx @@ -14,11 +14,14 @@ import { TimeLeft } from '../TimeLeft/TimeLeft'; export const EnrollmentSideBar = () => { const { LL } = useI18nContext(); - const vpnOptional = useEnrollmentStore((state) => state.vpnOptional); + const vpnOptional = useEnrollmentStore( + (state) => state.enrollmentSettings?.vpn_setup_optional, + ); const [currentStep, stepsMax] = useEnrollmentStore((state) => [ state.step, state.stepsMax, ]); + const enrollmentSettings = useEnrollmentStore((state) => state.enrollmentSettings); // fetch app version const { getAppInfo } = useApi(); @@ -37,16 +40,20 @@ export const EnrollmentSideBar = () => { }, []); const steps = useMemo((): LocalizedString[] => { - const steps = LL.pages.enrollment.sideBar.steps; - const vpnStep = vpnOptional ? `${steps.vpn()}*` : steps.vpn(); - return [ - steps.welcome(), - steps.verification(), - steps.password(), - vpnStep as LocalizedString, - steps.finish(), + const stepsLL = LL.pages.enrollment.sideBar.steps; + const vpnStep = ( + vpnOptional ? `${stepsLL.vpn()}*` : stepsLL.vpn() + ) as LocalizedString; + const steps = [ + stepsLL.welcome(), + stepsLL.verification(), + stepsLL.password(), + ...(!enrollmentSettings?.only_client_activation + ? [vpnStep, stepsLL.finish()] + : [stepsLL.finish()]), ]; - }, [LL.pages.enrollment.sideBar.steps, vpnOptional]); + return steps; + }, [LL.pages.enrollment.sideBar.steps, vpnOptional, enrollmentSettings]); return (
diff --git a/web/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx b/web/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx index 3133875..fb9c659 100644 --- a/web/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx +++ b/web/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx @@ -7,6 +7,7 @@ import { AdminInfo, Device, DeviceConfig, + EnrollmentSettings, UserInfo, } from '../../../../shared/hooks/api/types'; @@ -30,7 +31,7 @@ const persistKeys: Array = [ 'adminInfo', 'deviceState', 'endContent', - 'vpnOptional', + 'enrollmentSettings', ]; export const useEnrollmentStore = createWithEqualityFn()( @@ -82,7 +83,7 @@ type StoreValues = { userInfo?: UserInfo; userPassword?: string; adminInfo?: AdminInfo; - vpnOptional?: boolean; + enrollmentSettings?: EnrollmentSettings; // Markdown content for final step card endContent?: string; deviceState?: { diff --git a/web/src/pages/enrollment/steps/DeviceStep/DeviceStep.tsx b/web/src/pages/enrollment/steps/DeviceStep/DeviceStep.tsx index 27478ab..1c25590 100644 --- a/web/src/pages/enrollment/steps/DeviceStep/DeviceStep.tsx +++ b/web/src/pages/enrollment/steps/DeviceStep/DeviceStep.tsx @@ -7,9 +7,11 @@ import { useEffect } from 'react'; import { shallow } from 'zustand/shallow'; import { useI18nContext } from '../../../../i18n/i18n-react'; +import { LoaderSpinner } from '../../../../shared/components/layout/LoaderSpinner/LoaderSpinner'; import { MessageBox } from '../../../../shared/components/layout/MessageBox/MessageBox'; import { MessageBoxType } from '../../../../shared/components/layout/MessageBox/types'; import { useApi } from '../../../../shared/hooks/api/useApi'; +import useEffectOnce from '../../../../shared/hooks/api/utils'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; import { ConfigureDeviceCard } from './components/ConfigureDeviceCard/ConfigureDeviceCard'; import { QuickGuideCard } from './components/QuickGuideCard/QuickGuideCard'; @@ -21,7 +23,7 @@ export const DeviceStep = () => { const { LL } = useI18nContext(); const setStore = useEnrollmentStore((state) => state.setState); const deviceState = useEnrollmentStore((state) => state.deviceState); - const vpnOptional = useEnrollmentStore((state) => state.vpnOptional); + const settings = useEnrollmentStore((state) => state.enrollmentSettings); const [userPhone, userPassword] = useEnrollmentStore( (state) => [state.userInfo?.phone_number, state.userPassword], shallow, @@ -32,8 +34,8 @@ export const DeviceStep = () => { ); const cn = classNames({ - required: !vpnOptional, - optional: vpnOptional, + required: !settings?.vpn_setup_optional, + optional: settings?.vpn_setup_optional, }); const { mutate } = useMutation({ @@ -51,7 +53,11 @@ export const DeviceStep = () => { useEffect(() => { if (userPassword) { const sub = nextSubject.subscribe(() => { - if ((deviceState && deviceState.device && deviceState.configs) || vpnOptional) { + if ( + (deviceState && deviceState.device && deviceState.configs) || + settings?.vpn_setup_optional || + settings?.only_client_activation + ) { setStore({ loading: true, }); @@ -66,20 +72,44 @@ export const DeviceStep = () => { sub.unsubscribe(); }; } - }, [deviceState, nextSubject, vpnOptional, setStore, userPhone, userPassword, mutate]); + }, [ + deviceState, + nextSubject, + settings?.vpn_setup_optional, + setStore, + userPhone, + userPassword, + mutate, + settings?.only_client_activation, + ]); + + // If only client activation is enabled, skip manual wireguard setup + useEffectOnce(() => { + if (settings?.only_client_activation) { + nextSubject.next(); + } + }); return (
- {vpnOptional && ( - + {!settings?.only_client_activation ? ( + <> + {settings?.vpn_setup_optional && ( + + )} +
+ + +
+ + ) : ( +
+ +
)} -
- - -
); }; diff --git a/web/src/pages/enrollment/steps/DeviceStep/style.scss b/web/src/pages/enrollment/steps/DeviceStep/style.scss index 7bce008..775dee3 100644 --- a/web/src/pages/enrollment/steps/DeviceStep/style.scss +++ b/web/src/pages/enrollment/steps/DeviceStep/style.scss @@ -45,4 +45,11 @@ } } } + + #loader { + display: flex; + justify-content: center; + align-items: center; + height: 500px; + } } diff --git a/web/src/pages/main/MainPage.tsx b/web/src/pages/main/MainPage.tsx index 2c72311..0779fe3 100644 --- a/web/src/pages/main/MainPage.tsx +++ b/web/src/pages/main/MainPage.tsx @@ -40,8 +40,8 @@ export const MainPage = () => { adminInfo: res.admin, sessionStart, sessionEnd, - vpnOptional: res.vpn_setup_optional, endContent: res.final_page_content, + enrollmentSettings: res.settings, }); navigate(routes.enrollment, { replace: true }); }) diff --git a/web/src/pages/token/components/TokenCard.tsx b/web/src/pages/token/components/TokenCard.tsx index 1f36f8e..b6d38d4 100644 --- a/web/src/pages/token/components/TokenCard.tsx +++ b/web/src/pages/token/components/TokenCard.tsx @@ -73,7 +73,7 @@ export const TokenCard = () => { adminInfo: res.admin, sessionStart, sessionEnd, - vpnOptional: res.vpn_setup_optional, + enrollmentSettings: res.settings, endContent: res.final_page_content, }); navigate(routes.enrollment, { replace: true }); diff --git a/web/src/shared/hooks/api/types.ts b/web/src/shared/hooks/api/types.ts index e205dd8..170e086 100644 --- a/web/src/shared/hooks/api/types.ts +++ b/web/src/shared/hooks/api/types.ts @@ -18,12 +18,17 @@ export type EnrollmentStartRequest = { token: string; }; +export type EnrollmentSettings = { + vpn_setup_optional: boolean; + only_client_activation: boolean; +}; + export type EnrollmentStartResponse = { admin: AdminInfo; user: UserInfo; deadline_timestamp: number; final_page_content: string; - vpn_setup_optional: boolean; + settings: EnrollmentSettings; }; export type ActivateUserRequest = { diff --git a/web/src/shared/hooks/api/utils.ts b/web/src/shared/hooks/api/utils.ts index 50e9a9b..4dc03c7 100644 --- a/web/src/shared/hooks/api/utils.ts +++ b/web/src/shared/hooks/api/utils.ts @@ -1,3 +1,5 @@ +import { useEffect, useRef } from 'react'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export const removeNulls = (obj: any) => { return JSON.parse(JSON.stringify(obj), (_, value) => { @@ -5,3 +7,21 @@ export const removeNulls = (obj: any) => { return value; }); }; + +/** +Under normal circumstances, useEffect should run only once when passed an empty dependency array. +However, in dev mode with react strict mode enabled, everything is rendered twice for debugging purposes. +This also causes useEffect to run twice, which is not always desirable. +This custom hook ensures that the effect runs only once in dev mode as well. +*/ +export default function useEffectOnce(fn: () => void) { + const isMounted = useRef(false); + useEffect(() => { + if (isMounted.current) { + return; + } + + fn(); + isMounted.current = true; + }, [fn]); +} diff --git a/web/vite.config.ts b/web/vite.config.ts index 999e582..2504172 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,14 +1,14 @@ -import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; import autoprefixer from 'autoprefixer'; import * as path from 'path'; +import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { strictPort: true, - port: 3000, + port: 3002, proxy: { '/api': { target: 'http://127.0.0.1:8080/',