diff --git a/.env b/.env index 344ede6fcd..60c27c65ab 100644 --- a/.env +++ b/.env @@ -13,7 +13,7 @@ DEFGUARD_DEFAULT_ADMIN_PASSWORD=pass123 ### Proxy configuration ### # Optional. URL of proxy gRPC server -# DEFGUARD_PROXY_URL: http://localhost:50051 +# DEFGUARD_PROXY_URL=http://localhost:50051 ### LDAP configuration ### DEFGUARD_LDAP_URL=ldap://localhost:389 diff --git a/.fpm b/.fpm new file mode 100644 index 0000000000..b982fd8328 --- /dev/null +++ b/.fpm @@ -0,0 +1,6 @@ +-s dir +--name defguard +--architecture x86_64 +--description "defguard core service" +--url "https://defguard.net/" +--maintainer "teonite" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fb7214af0..4bb06a295b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ env: jobs: test: runs-on: [self-hosted, Linux] - container: rust:1.75 + container: rust:1.77 services: postgres: @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Cache diff --git a/.github/workflows/current.yml b/.github/workflows/current.yml index c08c4270f5..b2c6aabc92 100644 --- a/.github/workflows/current.yml +++ b/.github/workflows/current.yml @@ -13,7 +13,7 @@ jobs: runs-on: [self-hosted, Linux] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Docker meta @@ -35,7 +35,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: - config-inline: | + buildkitd-config-inline: | [registry."docker.io"] mirrors = ["dockerhub-proxy.teonite.net"] - name: Build container diff --git a/.github/workflows/dev-deployment.yml b/.github/workflows/dev-deployment.yml index 010b2133d3..da8cc336a6 100644 --- a/.github/workflows/dev-deployment.yml +++ b/.github/workflows/dev-deployment.yml @@ -15,6 +15,6 @@ jobs: - 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@v1.26.3 + uses: actions-hub/kubectl@v1.30.0 with: args: --namespace defguard-dev set image deployment/defguard defguard=ghcr.io/defguard/defguard:sha-${{ env.SHORT_SHA }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8bec6c503f..3ab0a3ba40 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ env: jobs: rustdoc: runs-on: [self-hosted, Linux] - container: rust:1.75 + container: rust:1.77 services: postgres: image: postgres:15-alpine @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9f2f31734f..bc3f1180ba 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,11 +7,11 @@ jobs: test: runs-on: [self-hosted, Linux] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: - config-inline: | + buildkitd-config-inline: | [registry."docker.io"] mirrors = ["dockerhub-proxy.teonite.net"] - name: Login to GitHub container registry @@ -21,12 +21,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: "./e2e/.nvmrc" - name: Install pnpm id: pnpm-install - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 with: version: 8 run_install: false @@ -36,7 +36,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} diff --git a/.github/workflows/lint-e2e.yml b/.github/workflows/lint-e2e.yml index 8c6c71200b..f3cadcbed3 100644 --- a/.github/workflows/lint-e2e.yml +++ b/.github/workflows/lint-e2e.yml @@ -14,8 +14,8 @@ jobs: lint-e2e: runs-on: [self-hosted, Linux] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 19 - name: install deps diff --git a/.github/workflows/lint-web.yml b/.github/workflows/lint-web.yml index b7e510d478..98ab096400 100644 --- a/.github/workflows/lint-web.yml +++ b/.github/workflows/lint-web.yml @@ -14,10 +14,10 @@ jobs: lint-web: runs-on: [self-hosted, Linux] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: 'recursive' - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 19 - name: install deps diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3611c30d9e..eabc89476b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: [self-hosted, Linux] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Docker meta @@ -28,7 +28,7 @@ jobs: - name: Set up Docker BuildX uses: docker/setup-buildx-action@v3 with: - config-inline: | + buildkitd-config-inline: | [registry."docker.io"] mirrors = ["dockerhub-proxy.teonite.net"] - name: Login to GitHub container registry @@ -94,7 +94,7 @@ jobs: echo "VERSION=$VERSION" >> $GITHUB_ENV - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -108,10 +108,30 @@ jobs: - name: Set up Docker BuildX uses: docker/setup-buildx-action@v3 with: - config-inline: | + buildkitd-config-inline: | [registry."docker.io"] mirrors = ["dockerhub-proxy.teonite.net"] + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: ./web/pnpm-lock.yaml + + - name: Install frontend dependencies + run: pnpm install --ignore-scripts --frozen-lockfile + working-directory: web + + - name: Build frontend + run: pnpm build + working-directory: web + - name: Build release binary uses: actions-rs/cargo@v1 with: @@ -139,3 +159,39 @@ jobs: asset_path: defguard-${{ github.ref_name }}-${{ matrix.target }}.tar.gz asset_name: defguard-${{ github.ref_name }}-${{ matrix.target }}.tar.gz asset_content_type: application/octet-stream + + - name: Build DEB package + if: matrix.build == 'linux' + uses: bpicode/github-action-fpm@master + with: + fpm_args: "defguard-${{ github.ref_name }}-${{ matrix.target }}=/usr/bin/defguard defguard.service=/usr/lib/systemd/system/defguard.service .env=/etc/defguard/core.conf" + fpm_opts: "--debug --output-type deb --version ${{ env.VERSION }} --package defguard-${{ env.VERSION }}-${{ matrix.target }}.deb" + + - name: Upload DEB + if: matrix.build == 'linux' + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: defguard-${{ env.VERSION }}-${{ matrix.target }}.deb + asset_name: defguard-${{ env.VERSION }}-${{ matrix.target }}.deb + asset_content_type: application/octet-stream + + - name: Build RPM package + if: matrix.build == 'linux' + uses: bpicode/github-action-fpm@master + with: + fpm_args: "defguard-${{ github.ref_name }}-${{ matrix.target }}=/usr/bin/defguard defguard.service=/usr/lib/systemd/system/defguard.service .env=/etc/defguard/core.conf" + fpm_opts: "--debug --output-type rpm --version ${{ env.VERSION }} --package defguard-${{ env.VERSION }}-${{ matrix.target }}.rpm" + + - name: Upload RPM + if: matrix.build == 'linux' + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: defguard-${{ env.VERSION }}-${{ matrix.target }}.rpm + asset_name: defguard-${{ env.VERSION }}-${{ matrix.target }}.rpm + asset_content_type: application/octet-stream diff --git a/.sqlx/query-0ddcc26ec82294f650ad1afc5b6c2ea8a9b8825a1f9e19c1795c564117d8e24e.json b/.sqlx/query-0ddcc26ec82294f650ad1afc5b6c2ea8a9b8825a1f9e19c1795c564117d8e24e.json new file mode 100644 index 0000000000..7389f53fea --- /dev/null +++ b/.sqlx/query-0ddcc26ec82294f650ad1afc5b6c2ea8a9b8825a1f9e19c1795c564117d8e24e.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM session WHERE user_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "0ddcc26ec82294f650ad1afc5b6c2ea8a9b8825a1f9e19c1795c564117d8e24e" +} diff --git a/.sqlx/query-198f1ff51b4777d0727dae0613f642a4ef429df77c70bdf5e2ca50f30af3eda1.json b/.sqlx/query-198f1ff51b4777d0727dae0613f642a4ef429df77c70bdf5e2ca50f30af3eda1.json new file mode 100644 index 0000000000..c90fbf57c7 --- /dev/null +++ b/.sqlx/query-198f1ff51b4777d0727dae0613f642a4ef429df77c70bdf5e2ca50f30af3eda1.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wn.name FROM wireguard_network wn JOIN wireguard_network_allowed_group wnag ON wn.id = wnag.network_id WHERE wnag.group_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "198f1ff51b4777d0727dae0613f642a4ef429df77c70bdf5e2ca50f30af3eda1" +} diff --git a/.sqlx/query-8389fe286dc72ef17a37015b9de4a0b53c03359b79b57106c14ac819ddf333df.json b/.sqlx/query-1b29a6b1d3741ede2d85271f0f0e07069048ba01f8c3c5988f044e788200f9f8.json similarity index 89% rename from .sqlx/query-8389fe286dc72ef17a37015b9de4a0b53c03359b79b57106c14ac819ddf333df.json rename to .sqlx/query-1b29a6b1d3741ede2d85271f0f0e07069048ba01f8c3c5988f044e788200f9f8.json index d85df17816..ee2196861b 100644 --- a/.sqlx/query-8389fe286dc72ef17a37015b9de4a0b53c03359b79b57106c14ac819ddf333df.json +++ b/.sqlx/query-1b29a6b1d3741ede2d85271f0f0e07069048ba01f8c3c5988f044e788200f9f8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes FROM \"user\" WHERE email = $1", + "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\" WHERE username = $1", "describe": { "columns": [ { @@ -85,6 +85,11 @@ "ordinal": 13, "name": "recovery_codes", "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "is_active", + "type_info": "Bool" } ], "parameters": { @@ -106,8 +111,9 @@ true, true, false, + false, false ] }, - "hash": "8389fe286dc72ef17a37015b9de4a0b53c03359b79b57106c14ac819ddf333df" + "hash": "1b29a6b1d3741ede2d85271f0f0e07069048ba01f8c3c5988f044e788200f9f8" } diff --git a/.sqlx/query-e8b8eec04b6b8d93aa508344c1a84adf68be8946253f9b42599f55c2a58646ad.json b/.sqlx/query-2b9b3aa210fa00d43bece1acbd3ae490fea37010d98f3eff2ba15f50e8a6d7a9.json similarity index 84% rename from .sqlx/query-e8b8eec04b6b8d93aa508344c1a84adf68be8946253f9b42599f55c2a58646ad.json rename to .sqlx/query-2b9b3aa210fa00d43bece1acbd3ae490fea37010d98f3eff2ba15f50e8a6d7a9.json index f09d62528c..c3182f082b 100644 --- a/.sqlx/query-e8b8eec04b6b8d93aa508344c1a84adf68be8946253f9b42599f55c2a58646ad.json +++ b/.sqlx/query-2b9b3aa210fa00d43bece1acbd3ae490fea37010d98f3eff2ba15f50e8a6d7a9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\" WHERE id = $1", + "query": "SELECT id \"id?\", \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\" WHERE id = $1", "describe": { "columns": [ { @@ -45,26 +45,31 @@ }, { "ordinal": 8, - "name": "totp_enabled", + "name": "is_active", "type_info": "Bool" }, { "ordinal": 9, - "name": "email_mfa_enabled", + "name": "totp_enabled", "type_info": "Bool" }, { "ordinal": 10, + "name": "email_mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, "name": "totp_secret", "type_info": "Bytea" }, { - "ordinal": 11, + "ordinal": 12, "name": "email_mfa_secret", "type_info": "Bytea" }, { - "ordinal": 12, + "ordinal": 13, "name": "mfa_method: _", "type_info": { "Custom": { @@ -82,7 +87,7 @@ } }, { - "ordinal": 13, + "ordinal": 14, "name": "recovery_codes: _", "type_info": "TextArray" } @@ -103,11 +108,12 @@ false, false, false, + false, true, true, false, false ] }, - "hash": "e8b8eec04b6b8d93aa508344c1a84adf68be8946253f9b42599f55c2a58646ad" + "hash": "2b9b3aa210fa00d43bece1acbd3ae490fea37010d98f3eff2ba15f50e8a6d7a9" } diff --git a/.sqlx/query-30342cf1a832d4ecffd1f77b6ca3d936a05fa60bae4a903ef7d2d37121b177d8.json b/.sqlx/query-3063672b53bf0bada26d6d1b0adbebfbeb42e6a4949a10b2f23ea4d968aeb736.json similarity index 71% rename from .sqlx/query-30342cf1a832d4ecffd1f77b6ca3d936a05fa60bae4a903ef7d2d37121b177d8.json rename to .sqlx/query-3063672b53bf0bada26d6d1b0adbebfbeb42e6a4949a10b2f23ea4d968aeb736.json index 6f43c0d659..e4f3bf2de0 100644 --- a/.sqlx/query-30342cf1a832d4ecffd1f77b6ca3d936a05fa60bae4a903ef7d2d37121b177d8.json +++ b/.sqlx/query-3063672b53bf0bada26d6d1b0adbebfbeb42e6a4949a10b2f23ea4d968aeb736.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"user\" SET \"username\" = $2,\"password_hash\" = $3,\"last_name\" = $4,\"first_name\" = $5,\"email\" = $6,\"phone\" = $7,\"mfa_enabled\" = $8,\"totp_enabled\" = $9,\"email_mfa_enabled\" = $10,\"totp_secret\" = $11,\"email_mfa_secret\" = $12,\"mfa_method\" = $13,\"recovery_codes\" = $14 WHERE id = $1", + "query": "UPDATE \"user\" SET \"username\" = $2,\"password_hash\" = $3,\"last_name\" = $4,\"first_name\" = $5,\"email\" = $6,\"phone\" = $7,\"mfa_enabled\" = $8,\"is_active\" = $9,\"totp_enabled\" = $10,\"email_mfa_enabled\" = $11,\"totp_secret\" = $12,\"email_mfa_secret\" = $13,\"mfa_method\" = $14,\"recovery_codes\" = $15 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -15,6 +15,7 @@ "Bool", "Bool", "Bool", + "Bool", "Bytea", "Bytea", { @@ -36,5 +37,5 @@ }, "nullable": [] }, - "hash": "30342cf1a832d4ecffd1f77b6ca3d936a05fa60bae4a903ef7d2d37121b177d8" + "hash": "3063672b53bf0bada26d6d1b0adbebfbeb42e6a4949a10b2f23ea4d968aeb736" } diff --git a/.sqlx/query-fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312.json b/.sqlx/query-393cacddf87582e0a72156580765dfb15404030a4f28406cbaa344ab279300d7.json similarity index 83% rename from .sqlx/query-fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312.json rename to .sqlx/query-393cacddf87582e0a72156580765dfb15404030a4f28406cbaa344ab279300d7.json index dd54ba7fc4..6fa96fee85 100644 --- a/.sqlx/query-fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312.json +++ b/.sqlx/query-393cacddf87582e0a72156580765dfb15404030a4f28406cbaa344ab279300d7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN \"user\" u ON d.user_id = u.id JOIN group_user gu ON u.id = gu.user_id JOIN \"group\" g ON gu.group_id = g.id WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[]))\n ORDER BY d.id ASC", + "query": "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN \"user\" u ON d.user_id = u.id JOIN group_user gu ON u.id = gu.user_id JOIN \"group\" g ON gu.group_id = g.id WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[]))\n AND u.is_active = true\n ORDER BY d.id ASC", "describe": { "columns": [ { @@ -42,5 +42,5 @@ false ] }, - "hash": "fdbb9308a58ade3fd1cba272fe3979ed4bafb1ee079fd8b23ac9c2ef95db2312" + "hash": "393cacddf87582e0a72156580765dfb15404030a4f28406cbaa344ab279300d7" } diff --git a/.sqlx/query-f7ca16ebd3aae0812c44eca690638f235ad660495ec529dd79d97c38f3336174.json b/.sqlx/query-43ac4b3a375a4756d77b015c0bb871f6ccc4f5ecc7ec32f0b3a9f64398f3686e.json similarity index 85% rename from .sqlx/query-f7ca16ebd3aae0812c44eca690638f235ad660495ec529dd79d97c38f3336174.json rename to .sqlx/query-43ac4b3a375a4756d77b015c0bb871f6ccc4f5ecc7ec32f0b3a9f64398f3686e.json index 8deb500b49..0e0a82ef1b 100644 --- a/.sqlx/query-f7ca16ebd3aae0812c44eca690638f235ad660495ec529dd79d97c38f3336174.json +++ b/.sqlx/query-43ac4b3a375a4756d77b015c0bb871f6ccc4f5ecc7ec32f0b3a9f64398f3686e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\"", + "query": "SELECT id \"id?\", \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\"", "describe": { "columns": [ { @@ -45,26 +45,31 @@ }, { "ordinal": 8, - "name": "totp_enabled", + "name": "is_active", "type_info": "Bool" }, { "ordinal": 9, - "name": "email_mfa_enabled", + "name": "totp_enabled", "type_info": "Bool" }, { "ordinal": 10, + "name": "email_mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, "name": "totp_secret", "type_info": "Bytea" }, { - "ordinal": 11, + "ordinal": 12, "name": "email_mfa_secret", "type_info": "Bytea" }, { - "ordinal": 12, + "ordinal": 13, "name": "mfa_method: _", "type_info": { "Custom": { @@ -82,7 +87,7 @@ } }, { - "ordinal": 13, + "ordinal": 14, "name": "recovery_codes: _", "type_info": "TextArray" } @@ -101,11 +106,12 @@ false, false, false, + false, true, true, false, false ] }, - "hash": "f7ca16ebd3aae0812c44eca690638f235ad660495ec529dd79d97c38f3336174" + "hash": "43ac4b3a375a4756d77b015c0bb871f6ccc4f5ecc7ec32f0b3a9f64398f3686e" } diff --git a/.sqlx/query-96af08182e72ed1798480d4bba8063c7c185be78e72b633208a3ef2436e373b1.json b/.sqlx/query-52c659862cef5cd45bbe7bdc58cf70c93c46740cc778825583e060cdee73a212.json similarity index 86% rename from .sqlx/query-96af08182e72ed1798480d4bba8063c7c185be78e72b633208a3ef2436e373b1.json rename to .sqlx/query-52c659862cef5cd45bbe7bdc58cf70c93c46740cc778825583e060cdee73a212.json index 6a61f1a3a6..72a1d6e0f5 100644 --- a/.sqlx/query-96af08182e72ed1798480d4bba8063c7c185be78e72b633208a3ef2436e373b1.json +++ b/.sqlx/query-52c659862cef5cd45bbe7bdc58cf70c93c46740cc778825583e060cdee73a212.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id WHERE group_user.group_id = $1", + "query": "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id WHERE group_user.group_id = $1", "describe": { "columns": [ { @@ -85,6 +85,11 @@ "ordinal": 13, "name": "recovery_codes", "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "is_active", + "type_info": "Bool" } ], "parameters": { @@ -106,8 +111,9 @@ false, true, false, + false, false ] }, - "hash": "96af08182e72ed1798480d4bba8063c7c185be78e72b633208a3ef2436e373b1" + "hash": "52c659862cef5cd45bbe7bdc58cf70c93c46740cc778825583e060cdee73a212" } diff --git a/.sqlx/query-59ff9c1093cb41a5f902cf42f92afbac37e0c581e96285340196841504ed3cf0.json b/.sqlx/query-59ff9c1093cb41a5f902cf42f92afbac37e0c581e96285340196841504ed3cf0.json new file mode 100644 index 0000000000..1f364cea31 --- /dev/null +++ b/.sqlx/query-59ff9c1093cb41a5f902cf42f92afbac37e0c581e96285340196841504ed3cf0.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created FROM device d JOIN \"user\" u ON d.user_id = u.id WHERE u.is_active = true ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "wireguard_pubkey", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "59ff9c1093cb41a5f902cf42f92afbac37e0c581e96285340196841504ed3cf0" +} diff --git a/.sqlx/query-50e79d640f2c5c55a78f125e11e31e5cf05383f2fe8eabbc8ce3af668152f3d7.json b/.sqlx/query-6fc45edc38cf3f590daec8f930a4117ae77dd226c42ee175544f0ba5eccb315d.json similarity index 89% rename from .sqlx/query-50e79d640f2c5c55a78f125e11e31e5cf05383f2fe8eabbc8ce3af668152f3d7.json rename to .sqlx/query-6fc45edc38cf3f590daec8f930a4117ae77dd226c42ee175544f0ba5eccb315d.json index cfeeb2f163..256dd6ac1c 100644 --- a/.sqlx/query-50e79d640f2c5c55a78f125e11e31e5cf05383f2fe8eabbc8ce3af668152f3d7.json +++ b/.sqlx/query-6fc45edc38cf3f590daec8f930a4117ae77dd226c42ee175544f0ba5eccb315d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes FROM \"user\" WHERE username = $1", + "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\" WHERE email = $1", "describe": { "columns": [ { @@ -85,6 +85,11 @@ "ordinal": 13, "name": "recovery_codes", "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "is_active", + "type_info": "Bool" } ], "parameters": { @@ -106,8 +111,9 @@ true, true, false, + false, false ] }, - "hash": "50e79d640f2c5c55a78f125e11e31e5cf05383f2fe8eabbc8ce3af668152f3d7" + "hash": "6fc45edc38cf3f590daec8f930a4117ae77dd226c42ee175544f0ba5eccb315d" } diff --git a/.sqlx/query-caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d.json b/.sqlx/query-b773bf99f9e3aafcade8bf57c6f9a49107df5e8d1452796c0bb4f2f657e2ec77.json similarity index 75% rename from .sqlx/query-caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d.json rename to .sqlx/query-b773bf99f9e3aafcade8bf57c6f9a49107df5e8d1452796c0bb4f2f657e2ec77.json index 72c3f6bd63..3c480bc095 100644 --- a/.sqlx/query-caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d.json +++ b/.sqlx/query-b773bf99f9e3aafcade8bf57c6f9a49107df5e8d1452796c0bb4f2f657e2ec77.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey as pubkey, preshared_key, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) ORDER BY d.id ASC", + "query": "SELECT d.wireguard_pubkey as pubkey, preshared_key, array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) AND u.is_active = true ORDER BY d.id ASC", "describe": { "columns": [ { @@ -31,5 +31,5 @@ null ] }, - "hash": "caf97c3a058eac0f9deb4e474b0d76a2d14a75d04f4ea5474e06adc9466a544d" + "hash": "b773bf99f9e3aafcade8bf57c6f9a49107df5e8d1452796c0bb4f2f657e2ec77" } diff --git a/.sqlx/query-84ed9302c404a5e6b056988b345d8e3d6e2cb2b3df71dc5ead7d03f22ba46683.json b/.sqlx/query-c9773715f70d267a2bc1e23b86d06c9dd358479e5791b672369d3c0496d51269.json similarity index 75% rename from .sqlx/query-84ed9302c404a5e6b056988b345d8e3d6e2cb2b3df71dc5ead7d03f22ba46683.json rename to .sqlx/query-c9773715f70d267a2bc1e23b86d06c9dd358479e5791b672369d3c0496d51269.json index a959958ca1..7111120273 100644 --- a/.sqlx/query-84ed9302c404a5e6b056988b345d8e3d6e2cb2b3df71dc5ead7d03f22ba46683.json +++ b/.sqlx/query-c9773715f70d267a2bc1e23b86d06c9dd358479e5791b672369d3c0496d51269.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"user\" (\"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\",\"recovery_codes\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING id", + "query": "INSERT INTO \"user\" (\"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\",\"recovery_codes\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id", "describe": { "columns": [ { @@ -20,6 +20,7 @@ "Bool", "Bool", "Bool", + "Bool", "Bytea", "Bytea", { @@ -43,5 +44,5 @@ false ] }, - "hash": "84ed9302c404a5e6b056988b345d8e3d6e2cb2b3df71dc5ead7d03f22ba46683" + "hash": "c9773715f70d267a2bc1e23b86d06c9dd358479e5791b672369d3c0496d51269" } diff --git a/.sqlx/query-d89f6eae4627862933a8f148f54f9eb916a2b11c62fb06b735b22324cd35f6eb.json b/.sqlx/query-d89f6eae4627862933a8f148f54f9eb916a2b11c62fb06b735b22324cd35f6eb.json deleted file mode 100644 index 5188e8beff..0000000000 --- a/.sqlx/query-d89f6eae4627862933a8f148f54f9eb916a2b11c62fb06b735b22324cd35f6eb.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT g.name as name, COALESCE(ARRAY_AGG(u.username) FILTER (WHERE u.username IS NOT NULL), '{}') as members FROM \"group\" g LEFT JOIN \"group_user\" gu ON gu.group_id = g.id LEFT JOIN \"user\" u ON u.id = gu.user_id GROUP BY g.name", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "members", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - null - ] - }, - "hash": "d89f6eae4627862933a8f148f54f9eb916a2b11c62fb06b735b22324cd35f6eb" -} diff --git a/.sqlx/query-467f699c41d75683c6e07b8367cf2899b47213b9b4c301898d548ce643516a49.json b/.sqlx/query-e253723a52a1632fd190ad5404cc0a6dfaf336b5416dafb8d634b521d3dce8d3.json similarity index 83% rename from .sqlx/query-467f699c41d75683c6e07b8367cf2899b47213b9b4c301898d548ce643516a49.json rename to .sqlx/query-e253723a52a1632fd190ad5404cc0a6dfaf336b5416dafb8d634b521d3dce8d3.json index 2dee9d17f3..98488e7959 100644 --- a/.sqlx/query-467f699c41d75683c6e07b8367cf2899b47213b9b4c301898d548ce643516a49.json +++ b/.sqlx/query-e253723a52a1632fd190ad5404cc0a6dfaf336b5416dafb8d634b521d3dce8d3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes FROM \"user\"\n INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id\n INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id\n WHERE \"group\".name = $1", + "query": "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\"\n INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id\n INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id\n WHERE \"group\".name = $1", "describe": { "columns": [ { @@ -85,6 +85,11 @@ "ordinal": 13, "name": "recovery_codes", "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "is_active", + "type_info": "Bool" } ], "parameters": { @@ -106,8 +111,9 @@ false, true, false, + false, false ] }, - "hash": "467f699c41d75683c6e07b8367cf2899b47213b9b4c301898d548ce643516a49" + "hash": "e253723a52a1632fd190ad5404cc0a6dfaf336b5416dafb8d634b521d3dce8d3" } diff --git a/.sqlx/query-eb753d506ce15b17b7aca1f5dd3dd03b382a667f0f0bc506e0cbc45118eea293.json b/.sqlx/query-eb753d506ce15b17b7aca1f5dd3dd03b382a667f0f0bc506e0cbc45118eea293.json new file mode 100644 index 0000000000..f23df5d702 --- /dev/null +++ b/.sqlx/query-eb753d506ce15b17b7aca1f5dd3dd03b382a667f0f0bc506e0cbc45118eea293.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT g.name as name, COALESCE(ARRAY_AGG(DISTINCT u.username) FILTER (WHERE u.username IS NOT NULL), '{}') as \"members!\", COALESCE(ARRAY_AGG(DISTINCT wn.name) FILTER (WHERE wn.name IS NOT NULL), '{}') as \"vpn_locations!\" FROM \"group\" g LEFT JOIN \"group_user\" gu ON gu.group_id = g.id LEFT JOIN \"user\" u ON u.id = gu.user_id LEFT JOIN \"wireguard_network_allowed_group\" wnag ON wnag.group_id = g.id LEFT JOIN \"wireguard_network\" wn ON wn.id = wnag.network_id GROUP BY g.name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "members!", + "type_info": "TextArray" + }, + { + "ordinal": 2, + "name": "vpn_locations!", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + null, + null + ] + }, + "hash": "eb753d506ce15b17b7aca1f5dd3dd03b382a667f0f0bc506e0cbc45118eea293" +} diff --git a/.sqlx/query-e2b78727c2ce57810cc9631a353e226b7dd03ad5917ac35a497d86977835aa4a.json b/.sqlx/query-f5ed6899054f1915882741843733fc196d59c356d69c0d06e9782cb1bc73e6f3.json similarity index 89% rename from .sqlx/query-e2b78727c2ce57810cc9631a353e226b7dd03ad5917ac35a497d86977835aa4a.json rename to .sqlx/query-f5ed6899054f1915882741843733fc196d59c356d69c0d06e9782cb1bc73e6f3.json index 7930f4a4c1..d4dfe1b6b1 100644 --- a/.sqlx/query-e2b78727c2ce57810cc9631a353e226b7dd03ad5917ac35a497d86977835aa4a.json +++ b/.sqlx/query-f5ed6899054f1915882741843733fc196d59c356d69c0d06e9782cb1bc73e6f3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes FROM \"user\" WHERE id = ANY($1)", + "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\" WHERE id = ANY($1)", "describe": { "columns": [ { @@ -85,6 +85,11 @@ "ordinal": 13, "name": "recovery_codes", "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "is_active", + "type_info": "Bool" } ], "parameters": { @@ -106,8 +111,9 @@ true, true, false, + false, false ] }, - "hash": "e2b78727c2ce57810cc9631a353e226b7dd03ad5917ac35a497d86977835aa4a" + "hash": "f5ed6899054f1915882741843733fc196d59c356d69c0d06e9782cb1bc73e6f3" } diff --git a/.sqlx/query-cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d.json b/.sqlx/query-fd950a860fe104136816e1605ed080d70a714e7a02f15e8a1d7b165814cf85d1.json similarity index 80% rename from .sqlx/query-cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d.json rename to .sqlx/query-fd950a860fe104136816e1605ed080d70a714e7a02f15e8a1d7b165814cf85d1.json index 26232ef8b7..07b07f5449 100644 --- a/.sqlx/query-cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d.json +++ b/.sqlx/query-fd950a860fe104136816e1605ed080d70a714e7a02f15e8a1d7b165814cf85d1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, mfa_method as \"mfa_method: MFAMethod\", password_hash FROM \"user\"", + "query": "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, mfa_method as \"mfa_method: MFAMethod\", password_hash, is_active FROM \"user\"", "describe": { "columns": [ { @@ -45,6 +45,11 @@ "ordinal": 5, "name": "password_hash", "type_info": "Text" + }, + { + "ordinal": 6, + "name": "is_active", + "type_info": "Bool" } ], "parameters": { @@ -56,8 +61,9 @@ false, false, false, - true + true, + false ] }, - "hash": "cbe6cdf1b9dd1d13bbb460726e33001ee45dea8486fb02e8c375d7b513ac0d6d" + "hash": "fd950a860fe104136816e1605ed080d70a714e7a02f15e8a1d7b165814cf85d1" } diff --git a/Cargo.lock b/Cargo.lock index 14cc67abc7..5a575a1e30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,18 +67,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "argon2" @@ -225,18 +225,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "async-trait" -version = "0.1.78" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -256,14 +256,14 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "axum" @@ -287,7 +287,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", @@ -295,9 +295,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core 0.4.3", @@ -306,7 +306,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-util", "itoa", "matchit", @@ -319,7 +319,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", "tower", "tower-layer", @@ -333,7 +333,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e7c467bdcd2bd982ce5c8742a1a178aba7b03db399fd18f5d5d438f5aa91cb4" dependencies = [ - "axum 0.7.4", + "axum 0.7.5", "forwarded-header-value", "serde", ] @@ -370,7 +370,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", "tracing", @@ -378,14 +378,14 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" dependencies = [ - "axum 0.7.4", + "axum 0.7.5", "axum-core 0.4.3", "bytes", - "cookie 0.18.0", + "cookie 0.18.1", "futures-util", "headers", "http 1.1.0", @@ -397,13 +397,14 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -438,6 +439,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "base64ct" version = "1.6.0" @@ -512,9 +519,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byte-slice-cast" @@ -530,18 +537,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.0.90" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" [[package]] name = "cfg-if" @@ -551,9 +558,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -561,7 +568,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -617,9 +624,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -634,19 +641,19 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim 0.11.1", ] [[package]] name = "clap_derive" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -736,12 +743,12 @@ dependencies = [ [[package]] name = "cookie" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "aes-gcm", - "base64 0.21.7", + "base64 0.22.0", "percent-encoding", "rand", "subtle", @@ -793,9 +800,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -912,7 +919,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -936,7 +943,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -947,7 +954,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -958,11 +965,11 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "defguard" -version = "0.10.0" +version = "0.11.0" dependencies = [ "anyhow", "argon2", - "axum 0.7.4", + "axum 0.7.5", "axum-client-ip", "axum-extra", "base64 0.21.7", @@ -979,6 +986,7 @@ dependencies = [ "lettre", "matches", "md4", + "mime_guess", "model_derive", "openidconnect", "otpauth", @@ -990,6 +998,7 @@ dependencies = [ "regex", "reqwest", "rsa", + "rust-embed", "rust-ini", "secp256k1", "secrecy", @@ -1023,9 +1032,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -1071,9 +1080,9 @@ dependencies = [ [[package]] name = "deunicode" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" +checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e" [[package]] name = "digest" @@ -1095,7 +1104,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -1159,9 +1168,9 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" dependencies = [ "serde", ] @@ -1189,11 +1198,11 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" dependencies = [ - "base64 0.21.7", + "base64 0.22.0", "memchr", ] @@ -1205,9 +1214,9 @@ checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1322,9 +1331,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "ff" @@ -1338,9 +1347,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" +checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" [[package]] name = "finl_unicode" @@ -1500,7 +1509,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -1555,9 +1564,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", @@ -1592,7 +1601,7 @@ dependencies = [ "bstr", "log", "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -1619,9 +1628,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1629,26 +1638,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.5", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 1.1.0", - "indexmap 2.2.5", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1874,7 +1864,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.25", + "h2", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1890,14 +1880,13 @@ dependencies = [ [[package]] name = "hyper" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.3", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -1917,7 +1906,7 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.28", - "rustls 0.21.10", + "rustls 0.21.11", "tokio", "tokio-rustls 0.24.1", ] @@ -1957,7 +1946,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.2.0", + "hyper 1.3.1", "pin-project-lite", "socket2", "tokio", @@ -2079,9 +2068,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -2121,15 +2110,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -2141,9 +2121,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -2156,9 +2136,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.2.0" +version = "9.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" dependencies = [ "base64 0.21.7", "js-sys", @@ -2236,12 +2216,12 @@ dependencies = [ [[package]] name = "lettre" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357ff5edb6d8326473a64c82cf41ddf78ab116f89668c50c4fac1b321e5e80f4" +checksum = "47460276655930189e0919e4fbf46e46476b14f934f18a63dd726a5fb7b60e2e" dependencies = [ "async-trait", - "base64 0.21.7", + "base64 0.22.0", "chumsky", "email-encoding", "email_address", @@ -2285,12 +2265,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -2361,9 +2335,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" @@ -2412,14 +2386,14 @@ name = "model_derive" version = "0.1.2" dependencies = [ "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "multimap" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "native-tls" @@ -2557,7 +2531,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -2689,7 +2663,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -2700,9 +2674,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2721,9 +2695,9 @@ dependencies = [ [[package]] name = "ordered-multimap" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d6a8c22fc714f0c2373e6091bf6f5e9b37b1bc0b1184874b7e0a4e303d318f" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", "hashbrown 0.14.3", @@ -2861,11 +2835,11 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pem" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.21.7", + "base64 0.22.0", "serde", ] @@ -2886,9 +2860,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.8" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" dependencies = [ "memchr", "thiserror", @@ -2897,9 +2871,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.8" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" dependencies = [ "pest", "pest_generator", @@ -2907,22 +2881,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.8" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "pest_meta" -version = "2.7.8" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" dependencies = [ "once_cell", "pest", @@ -2936,7 +2910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.2.5", + "indexmap 2.2.6", ] [[package]] @@ -2994,14 +2968,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -3038,9 +3012,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "platforms" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" +checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "polyval" @@ -3068,12 +3042,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ "proc-macro2", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -3129,9 +3103,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -3148,15 +3122,15 @@ dependencies = [ "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", "unarray", ] [[package]] name = "prost" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" dependencies = [ "bytes", "prost-derive", @@ -3164,13 +3138,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +checksum = "80b776a1b2dc779f5ee0641f8ade0125bc1298dd41a9a0c16d8bd57b42d222b1" dependencies = [ "bytes", - "heck 0.4.1", - "itertools 0.11.0", + "heck 0.5.0", + "itertools 0.12.1", "log", "multimap", "once_cell", @@ -3179,29 +3153,28 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.53", + "syn 2.0.60", "tempfile", - "which", ] [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" dependencies = [ "prost", ] @@ -3245,9 +3218,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -3314,14 +3287,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -3341,7 +3314,7 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -3352,15 +3325,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.26" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", "bytes", @@ -3369,7 +3342,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.25", + "h2", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", @@ -3384,12 +3357,12 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", + "rustls 0.21.11", "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -3498,6 +3471,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.60", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.20.0" @@ -3540,9 +3548,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" dependencies = [ "bitflags 2.5.0", "errno", @@ -3553,9 +3561,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" dependencies = [ "log", "ring 0.17.8", @@ -3565,14 +3573,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.2" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki 0.102.3", "subtle", "zeroize", ] @@ -3584,7 +3592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.1", + "rustls-pemfile 2.1.2", "rustls-pki-types", "schannel", "security-framework", @@ -3601,19 +3609,19 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.21.7", + "base64 0.22.0", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" [[package]] name = "rustls-webpki" @@ -3627,9 +3635,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -3638,9 +3646,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "ryu" @@ -3659,9 +3667,9 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.11.0" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef2175c2907e7c8bc0a9c3f86aeb5ec1f3b275300ad58a44d0c3ae379a5e52e" +checksum = "7c453e59a955f81fb62ee5d596b450383d699f152d350e9d23a0db2adb78e4c0" dependencies = [ "cfg-if", "derive_more", @@ -3671,9 +3679,9 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.11.0" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b8eb8fd61c5cdd3390d9b2132300a7e7618955b98b8416f118c1b4e144f" +checksum = "18cf6c6447f813ef19eb450e985bcce6705f9ce7660db221b59093d15c79c4b7" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", @@ -3751,9 +3759,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3764,9 +3772,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -3780,9 +3788,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] @@ -3809,20 +3817,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -3881,7 +3889,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_derive", "serde_json", @@ -3898,19 +3906,20 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "serde_yaml" -version = "0.8.26" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.2.6", + "itoa", "ryu", "serde", - "yaml-rust", + "unsafe-libyaml", ] [[package]] @@ -4014,9 +4023,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -4099,7 +4108,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.5", + "indexmap 2.2.6", "ipnetwork", "log", "memchr", @@ -4292,9 +4301,9 @@ dependencies = [ [[package]] name = "ssh-key" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b71299a724c8d84956caaf8fc3b3ea57c3587fe2d0b800cd0dc1f3599905d7e" +checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc" dependencies = [ "p256", "p384", @@ -4348,9 +4357,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "struct-patch" @@ -4369,7 +4378,7 @@ checksum = "f14a349c27ebe59faba22f933c9c734d428da7231e88a247e9d8c61eea964ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -4391,7 +4400,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -4413,9 +4422,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.53" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -4428,6 +4437,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "synstructure" version = "0.12.6" @@ -4503,22 +4518,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -4533,9 +4548,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -4554,9 +4569,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -4588,9 +4603,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -4622,7 +4637,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -4641,7 +4656,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.10", + "rustls 0.21.11", "tokio", ] @@ -4651,7 +4666,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.2", + "rustls 0.22.4", "rustls-pki-types", "tokio", ] @@ -4693,7 +4708,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "toml_datetime", "winnow", ] @@ -4704,7 +4719,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "toml_datetime", "winnow", ] @@ -4715,7 +4730,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "toml_datetime", "winnow", ] @@ -4732,7 +4747,7 @@ dependencies = [ "base64 0.21.7", "bytes", "flate2", - "h2 0.3.25", + "h2", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", @@ -4741,7 +4756,7 @@ dependencies = [ "pin-project", "prost", "rustls-native-certs", - "rustls-pemfile 2.1.1", + "rustls-pemfile 2.1.2", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", @@ -4762,7 +4777,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -4842,7 +4857,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -4898,9 +4913,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uaparser" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf694e7b0434d4fad6c879e984e8fdc3a62f5533c3d421762244f9e9d03f6927" +checksum = "2a4d8fcdf9685cad74cecf1553af084ab4c494e833c47d3c50ca32cba8035545" dependencies = [ "derive_more", "lazy_static", @@ -5048,6 +5063,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.7.1" @@ -5164,7 +5185,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -5198,7 +5219,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5307,18 +5328,6 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - [[package]] name = "whoami" version = "1.5.1" @@ -5366,7 +5375,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -5384,7 +5393,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -5404,17 +5413,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -5425,9 +5435,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -5437,9 +5447,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -5449,9 +5459,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -5461,9 +5477,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -5473,9 +5489,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -5485,9 +5501,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -5497,9 +5513,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -5559,15 +5575,6 @@ dependencies = [ "time", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "zerocopy" version = "0.7.32" @@ -5585,7 +5592,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -5605,5 +5612,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] diff --git a/Cargo.toml b/Cargo.toml index 810ad3d1b8..37e5d1915f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard" -version = "0.10.0" +version = "0.11.0" edition = "2021" license = "Apache-2.0" homepage = "https://defguard.net/" @@ -9,7 +9,6 @@ repository = "https://github.com/DefGuard/defguard" [workspace] [dependencies] -model_derive = { path = "model-derive" } anyhow = "1.0" argon2 = { version = "0.5", features = ["std"] } axum = { version = "0.7" } @@ -35,16 +34,19 @@ jsonwebtoken = "9.2" ldap3 = { version = "0.11", default-features = false, features = ["tls"] } lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } md4 = "0.10" -otpauth = "0.4" +mime_guess = "2.0" +model_derive = { path = "model-derive" } openidconnect = { version = "3.4", default-features = false, optional = true } -pulldown-cmark = "0.9" +otpauth = "0.4" prost = "0.12" +pulldown-cmark = "0.9" rand = "0.8" rand_core = { version = "0.6", default-features = false, features = [ "getrandom", ] } reqwest = { version = "0.11", features = ["json"] } rsa = { version = "0.9", features = ["pem"] } +rust-embed = { version = "8.4", features = ["include-exclude"] } rust-ini = "0.20" secp256k1 = { version = "0.28", features = [ "recovery", diff --git a/Cross.toml b/Cross.toml index 2f0e1f5147..499cd583a9 100644 --- a/Cross.toml +++ b/Cross.toml @@ -7,7 +7,7 @@ pre-build = [ "apt-get update && apt-get install --assume-yes libssl-dev unzip", "PB_REL='https://github.com/protocolbuffers/protobuf/releases'", "PB_VERSION='3.20.0' && curl -LO $PB_REL/download/v$PB_VERSION/protoc-$PB_VERSION-linux-x86_64.zip", - "unzip -o protoc-$PB_VERSION-linux-x86_64.zip bin/protoc include/google/* -d /usr" + "unzip -o protoc-$PB_VERSION-linux-x86_64.zip bin/protoc include/google/* -d /usr", ] [target.armv7-unknown-linux-gnueabihf] @@ -17,7 +17,7 @@ pre-build = [ "apt-get update && apt-get install --assume-yes libssl-dev libssl-dev:$CROSS_DEB_ARCH unzip", "PB_REL='https://github.com/protocolbuffers/protobuf/releases'", "PB_VERSION='3.20.0' && curl -LO $PB_REL/download/v$PB_VERSION/protoc-$PB_VERSION-linux-x86_64.zip", - "unzip -o protoc-$PB_VERSION-linux-x86_64.zip bin/protoc include/google/* -d /usr" + "unzip -o protoc-$PB_VERSION-linux-x86_64.zip bin/protoc include/google/* -d /usr", ] [target.aarch64-unknown-linux-gnu] @@ -27,7 +27,7 @@ pre-build = [ "apt-get update && apt-get install --assume-yes libssl-dev libssl-dev:$CROSS_DEB_ARCH unzip", "PB_REL='https://github.com/protocolbuffers/protobuf/releases'", "PB_VERSION='3.20.0' && curl -LO $PB_REL/download/v$PB_VERSION/protoc-$PB_VERSION-linux-x86_64.zip", - "unzip -o protoc-$PB_VERSION-linux-x86_64.zip bin/protoc include/google/* -d /usr" + "unzip -o protoc-$PB_VERSION-linux-x86_64.zip bin/protoc include/google/* -d /usr", ] @@ -37,5 +37,5 @@ pre-build = [ "apt-get update && apt-get install --assume-yes libssl-dev unzip", "PB_REL='https://github.com/protocolbuffers/protobuf/releases'", "PB_VERSION='3.20.0' && curl -LO $PB_REL/download/v$PB_VERSION/protoc-$PB_VERSION-linux-x86_64.zip", - "unzip -o protoc-$PB_VERSION-linux-x86_64.zip bin/protoc include/google/* -d /usr" + "unzip -o protoc-$PB_VERSION-linux-x86_64.zip bin/protoc include/google/* -d /usr", ] diff --git a/Dockerfile b/Dockerfile index cf8e819f6e..8762ea0045 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,14 @@ -FROM rust:1.75 as chef +FROM node:20-alpine as web + +WORKDIR /app +COPY web/package.json web/pnpm-lock.yaml web/.npmrc . +RUN npm i -g pnpm +RUN pnpm install --ignore-scripts --frozen-lockfile +COPY web/ . +RUN pnpm run generate-translation-types +RUN pnpm build + +FROM rust:1.77 as chef WORKDIR /build @@ -20,6 +30,9 @@ COPY --from=planner /build/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json # build project +COPY --from=web /app/dist ./web/dist +COPY web/src/shared/images/svg ./web/src/shared/images/svg +COPY user_agent_header_regexes.yaml /build/user_agent_header_regexes.yaml RUN apt-get update && apt-get -y install protobuf-compiler libprotobuf-dev COPY Cargo.toml Cargo.lock build.rs ./ COPY .sqlx .sqlx @@ -30,26 +43,11 @@ COPY proto proto COPY migrations migrations RUN cargo install --locked --path . --root /build -FROM node:20.5-alpine3.17 as web - -WORKDIR /app -COPY web/package.json . -COPY web/pnpm-lock.yaml . -COPY web/.npmrc . -RUN npm i -g pnpm -RUN pnpm install --ignore-scripts --frozen-lockfile -COPY web/ . -RUN pnpm run generate-translation-types -RUN pnpm build - # run FROM debian:bookworm-slim as runtime RUN apt-get update -y && \ apt-get install --no-install-recommends -y ca-certificates libssl-dev && \ rm -rf /var/lib/apt/lists/* -COPY user_agent_header_regexes.yaml /app/user_agent_header_regexes.yaml WORKDIR /app COPY --from=builder /build/bin/defguard . -COPY --from=web /app/dist ./web/dist -COPY web/src/shared/images/svg ./web/src/shared/images/svg ENTRYPOINT ["./defguard"] diff --git a/Dockerfile.ci b/Dockerfile.ci deleted file mode 100644 index 0f77ae45b9..0000000000 --- a/Dockerfile.ci +++ /dev/null @@ -1,18 +0,0 @@ -FROM node:20.5-alpine3.17 as web -WORKDIR /app -COPY web/package.json . -COPY web/pnpm-lock.yaml . -COPY web/.npmrc . -RUN npm i -g pnpm -RUN pnpm i --frozen-lockfile --ignore-scripts -COPY web/ . -RUN pnpm build - -FROM debian:bullseye-slim -RUN apt-get update -y && \ - apt-get install --no-install-recommends -y ca-certificates && \ - rm -rf /var/lib/apt/lists/* -COPY build/bin/defguard . -COPY --from=web /app/dist ./web -USER 1000 -ENTRYPOINT ["./defguard"] diff --git a/defguard.service b/defguard.service new file mode 100644 index 0000000000..49e5721234 --- /dev/null +++ b/defguard.service @@ -0,0 +1,23 @@ +[Unit] +Description=defguard core service +Documentation=https://defguard.gitbook.io/defguard/ +Wants=network-online.target +After=network-online.target + +[Service] +DynamicUser=yes +User=defguard +ExecReload=/bin/kill -HUP $MAINPID +EnvironmentFile=/etc/defguard/core.conf +ExecStart=/usr/bin/defguard +KillMode=process +KillSignal=SIGINT +LimitNOFILE=65536 +LimitNPROC=infinity +Restart=on-failure +RestartSec=2 +TasksMax=infinity +OOMScoreAdjust=-1000 + +[Install] +WantedBy=multi-user.target diff --git a/e2e/tests/auth.spec.ts b/e2e/tests/auth.spec.ts index 7477aee07a..f985b777e2 100644 --- a/e2e/tests/auth.spec.ts +++ b/e2e/tests/auth.spec.ts @@ -10,8 +10,10 @@ import { logout } from '../utils/controllers/logout'; import { enableEmailMFA } from '../utils/controllers/mfa/enableEmail'; import { enableTOTP } from '../utils/controllers/mfa/enableTOTP'; import { changePassword, changePasswordByAdmin } from '../utils/controllers/profile'; +import { disableUser } from '../utils/controllers/toggleUserState'; import { dockerDown, dockerRestart } from '../utils/docker'; import { waitForBase } from '../utils/waitForBase'; +import { waitForPromise } from '../utils/waitForPromise'; import { waitForRoute } from '../utils/waitForRoute'; test.describe('Test user authentication', () => { @@ -84,6 +86,37 @@ test.describe('Test user authentication', () => { await page.locator('button[type="submit"]').click(); await waitForRoute(page, routes.me); }); + + test('Login as disabled user', async ({ page, browser }) => { + await waitForBase(page); + await createUser(browser, testUser); + await disableUser(browser, testUser); + await page.goto(routes.base); + await waitForRoute(page, routes.auth.login); + await page.getByTestId('login-form-username').type(testUser.username); + await page.getByTestId('login-form-password').type(testUser.password); + const responsePromise = page.waitForResponse('**/auth'); + await page.getByTestId('login-form-submit').click(); + const response = await responsePromise; + expect(response.status()).toBe(401); + expect(page.url()).toBe(routes.base + routes.auth.login); + }); + + test('Logout when disabled', async ({ page, browser }) => { + await waitForBase(page); + await createUser(browser, testUser); + await loginBasic(page, testUser); + await waitForRoute(page, routes.me); + expect(page.url()).toBe(routes.base + routes.me); + await disableUser(browser, testUser); + // The user should be logged out when the admin disables him + await waitForPromise(2000); + const responsePromise = page.waitForResponse('**/user/' + testUser.username); + await page.locator('a[href="/me"]').click(); + const response = await responsePromise; + expect(response.status()).toBe(401); + expect(page.url()).toBe(routes.base + routes.auth.login); + }); }); test.describe('Test password change', () => { diff --git a/e2e/tests/enrollment.spec.ts b/e2e/tests/enrollment.spec.ts index 2e6725c7c5..265ccf53f4 100644 --- a/e2e/tests/enrollment.spec.ts +++ b/e2e/tests/enrollment.spec.ts @@ -13,6 +13,7 @@ import { validateData, } from '../utils/controllers/enrollment'; import { loginBasic } from '../utils/controllers/login'; +import { disableUser, enableUser } from '../utils/controllers/toggleUserState'; import { createNetwork } from '../utils/controllers/vpn/createNetwork'; import { dockerDown, dockerRestart } from '../utils/docker'; import { waitForBase } from '../utils/waitForBase'; @@ -40,6 +41,46 @@ test.describe('Create user with enrollment enabled', () => { dockerDown(); }); + test('Try to complete enrollment with disabled user', async ({ page, browser }) => { + expect(token).toBeDefined(); + await waitForBase(page); + await disableUser(browser, user); + await page.goto(testsConfig.ENROLLMENT_URL); + await waitForPromise(2000); + // Test if we can send the token + await selectEnrollment(page); + const startResponse = page.waitForResponse('**/start'); + await setToken(token, page); + expect((await startResponse).status()).toBe(403); + // Check if we are still on the token page + expect(page.url()).toBe(`${testsConfig.ENROLLMENT_URL}/token`); + + // Test other enrollment steps + await enableUser(browser, user); + await page.reload(); + await setToken(token, page); + // Welcome page + await page.getByTestId('enrollment-next').click(); + // Data validation + await validateData(user, page); + await page.getByTestId('enrollment-next').click(); + await disableUser(browser, user); + // Set password + await setPassword(page); + // VPN + await page.getByTestId('enrollment-next').click(); + + // Test if we can create a device configuration, if the admin has disabled us after the token validation + const deviceResponse = page.waitForResponse('**/create_device'); + await createDevice(page); + expect((await deviceResponse).status()).toBe(400); + + // Activating the user should fail with a 400 error + const userResponse = page.waitForResponse('**/activate_user'); + await page.getByTestId('enrollment-next').click({ timeout: 2000 }); + expect((await userResponse).status()).toBe(400); + }); + test('Complete enrollment with created user', async ({ page }) => { expect(token).toBeDefined(); await waitForBase(page); diff --git a/e2e/tests/passwordReset.spec.ts b/e2e/tests/passwordReset.spec.ts index 351405ef4b..814d78c320 100644 --- a/e2e/tests/passwordReset.spec.ts +++ b/e2e/tests/passwordReset.spec.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { testsConfig, testUserTemplate } from '../config'; import { User } from '../types'; @@ -10,6 +10,7 @@ import { setEmail, setPassword, } from '../utils/controllers/passwordReset'; +import { disableUser } from '../utils/controllers/toggleUserState'; import { getPasswordResetToken } from '../utils/db/getPasswordResetToken'; import { dockerDown, dockerRestart } from '../utils/docker'; import { waitForBase } from '../utils/waitForBase'; @@ -48,4 +49,29 @@ test.describe('Reset password', () => { await loginBasic(page, { ...user, password: newPassword }); await logout(page); }); + + test('Reset disabled user password', async ({ page, browser }) => { + await waitForBase(page); + await page.goto(testsConfig.ENROLLMENT_URL); + await waitForPromise(2000); + await selectPasswordReset(page); + await setEmail(user.mail, page); + await waitForPromise(2000); + const token = await getPasswordResetToken(user.mail); + await disableUser(browser, user); + await page.goto(`${testsConfig.ENROLLMENT_URL}/password-reset/?token=${token}`); + await waitForPromise(2000); + + // A message should be displayed that the code is invalid + const message = await page.locator('.message').textContent(); + expect(message).toBe( + 'The entered code is invalid. Please start the process from the beginning.' + ); + + // The password input should not be visible + const passwordInputVisible = await page + .locator('[data-testid="field-password"]') + .isVisible(); + expect(passwordInputVisible).toBe(false); + }); }); diff --git a/e2e/utils/controllers/toggleUserState.ts b/e2e/utils/controllers/toggleUserState.ts new file mode 100644 index 0000000000..50e2d085a4 --- /dev/null +++ b/e2e/utils/controllers/toggleUserState.ts @@ -0,0 +1,32 @@ +import { Browser } from 'playwright'; + +import { defaultUserAdmin, routes } from '../../config'; +import { User } from '../../types'; +import { waitForBase } from '../waitForBase'; +import { loginBasic } from './login'; + +export const enableUser = async (browser: Browser, user: User): Promise => { + const context = await browser.newContext(); + const page = await context.newPage(); + await waitForBase(page); + await loginBasic(page, defaultUserAdmin); + await page.goto(routes.base + '/admin/users/' + user.username); + await page.getByTestId('edit-user').click(); + await page.getByTestId('status-select').locator('.select-container').click(); + await page.locator('.select-option:has-text("Active")').click(); + await page.getByTestId('user-edit-save').click(); + await context.close(); +}; + +export const disableUser = async (browser: Browser, user: User): Promise => { + const context = await browser.newContext(); + const page = await context.newPage(); + await waitForBase(page); + await loginBasic(page, defaultUserAdmin); + await page.goto(routes.base + '/admin/users/' + user.username); + await page.getByTestId('edit-user').click(); + await page.getByTestId('status-select').locator('.select-container').click(); + await page.locator('.select-option:has-text("Disabled")').click(); + await page.getByTestId('user-edit-save').click(); + await context.close(); +}; diff --git a/justfile b/justfile index 81597462b2..b510d5c899 100644 --- a/justfile +++ b/justfile @@ -1,9 +1,11 @@ # build release binary build: cargo build --release + # remove test databases drop-test-dbs: ./drop_test_dbs.sh + # move tag to current commit move-tag TAG: # remove local tag @@ -16,3 +18,11 @@ move-tag TAG: git push # push new tag to remote git push origin {{TAG}} + +# format Rust project +format: + cargo +nightly --locked fmt --all # use nightly toolchain for better import handling + +# lint Rust project +lint: + cargo clippy --all-targets --all-features diff --git a/migrations/20240604104038_add_user_active_flag.down.sql b/migrations/20240604104038_add_user_active_flag.down.sql new file mode 100644 index 0000000000..bb48e798c1 --- /dev/null +++ b/migrations/20240604104038_add_user_active_flag.down.sql @@ -0,0 +1 @@ +ALTER TABLE "user" DROP COLUMN is_active; diff --git a/migrations/20240604104038_add_user_active_flag.up.sql b/migrations/20240604104038_add_user_active_flag.up.sql new file mode 100644 index 0000000000..4cbe454f87 --- /dev/null +++ b/migrations/20240604104038_add_user_active_flag.up.sql @@ -0,0 +1 @@ +ALTER TABLE "user" ADD COLUMN is_active boolean NOT NULL DEFAULT true; diff --git a/proto b/proto index 29898d9cb5..c71f378472 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 29898d9cb502ef5e7bb1771b63e6a66debf31e7d +Subproject commit c71f37847279ee23220fcf9e0e45d2c365b3b8ee diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6d833ff506..fcc85b9ecb 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.75" +channel = "1.77" diff --git a/src/assets.rs b/src/assets.rs new file mode 100644 index 0000000000..0ae3ddc743 --- /dev/null +++ b/src/assets.rs @@ -0,0 +1,48 @@ +use axum::{ + http::{header, StatusCode, Uri}, + response::{IntoResponse, Response}, +}; +use rust_embed::Embed; + +pub async fn web_asset(uri: Uri) -> impl IntoResponse { + let mut path = uri.path().trim_start_matches('/').to_string(); + // Rewrite the path to match the structure of the embedded files + path.insert_str(0, "dist/"); + StaticFile(path) +} + +pub async fn index() -> impl IntoResponse { + web_asset(Uri::from_static("/index.html")).await +} + +pub async fn svg(uri: Uri) -> impl IntoResponse { + let mut path = uri.path().trim_start_matches('/').to_string(); + // Rewrite the path to match the structure of the embedded files + path.insert_str(0, "src/shared/images/"); + StaticFile(path) +} + +#[derive(Embed)] +#[folder = "web/"] +#[include = "dist/*"] +#[include = "src/shared/images/*"] +struct WebAsset; + +pub struct StaticFile(pub T); + +impl IntoResponse for StaticFile +where + T: Into, +{ + fn into_response(self) -> Response { + let path = self.0.into(); + + match WebAsset::get(path.as_str()) { + Some(content) => { + let mime = mime_guess::from_path(path).first_or_octet_stream(); + ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() + } + None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), + } + } +} diff --git a/src/db/models/device.rs b/src/db/models/device.rs index f21751e8e9..62583415e8 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -366,12 +366,21 @@ impl Device { } None => String::new(), }; - let allowed_ips = network - .allowed_ips - .iter() - .map(IpNetwork::to_string) - .collect::>() - .join(","); + + let allowed_ips = if network.allowed_ips.is_empty() { + String::new() + } else { + format!( + "AllowedIPs = {}\n", + network + .allowed_ips + .iter() + .map(IpNetwork::to_string) + .collect::>() + .join(",") + ) + }; + format!( "[Interface]\n\ PrivateKey = YOUR_PRIVATE_KEY\n\ @@ -380,7 +389,7 @@ impl Device { \n\ [Peer]\n\ PublicKey = {}\n\ - AllowedIPs = {allowed_ips}\n\ + {allowed_ips}\ Endpoint = {}:{}\n\ PersistentKeepalive = 300", wireguard_network_device.wireguard_ip, network.pubkey, network.endpoint, network.port, diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 767af4fe9f..40d4f10719 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -35,6 +35,8 @@ pub enum TokenError { TokenUsed, #[error("Enrollment user not found")] UserNotFound, + #[error("Enrollment user is disabled")] + UserDisabled, #[error("Enrollment admin not found")] AdminNotFound, #[error("User account is already activated")] @@ -58,6 +60,7 @@ impl From for Status { TokenError::DbError(_) | TokenError::AdminNotFound | TokenError::UserNotFound + | TokenError::UserDisabled | TokenError::NotificationError(_) | TokenError::WelcomeMsgNotConfigured | TokenError::WelcomeEmailNotConfigured @@ -380,6 +383,14 @@ impl User { return Err(TokenError::AlreadyActive); } + if !self.is_active { + warn!( + "Can't create enrollment token for disabled user {}", + self.username + ); + return Err(TokenError::UserDisabled); + } + let user_id = self.id.expect("User without ID"); let admin_id = admin.id.expect("Admin user without ID"); @@ -454,6 +465,14 @@ impl User { let user_id = self.id.expect("User without ID"); let admin_id = admin.id.expect("Admin user without ID"); + if !self.is_active { + warn!( + "Can't create desktop configuration enrollment token for disabled user {}", + self.username + ); + return Err(TokenError::UserDisabled); + } + self.clear_unused_enrollment_tokens(&mut *transaction) .await?; diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 33bf950bbe..73f4476188 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -60,7 +60,7 @@ impl Group { User, "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ - mfa_method \"mfa_method: _\", recovery_codes \ + mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" \ JOIN group_user ON \"user\".id = group_user.user_id \ WHERE group_user.group_id = $1", @@ -72,6 +72,26 @@ impl Group { Ok(Vec::new()) } } + + /// Fetches a list of VPN locations where a given group is explicitly allowed. + /// This does not include VPN locations where all groups are implicitly allowed (admin group), + /// because no access control in configured. + pub async fn allowed_vpn_locations<'e, E>(&self, executor: E) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { + if let Some(id) = self.id { + query_scalar!( + "SELECT wn.name FROM wireguard_network wn JOIN wireguard_network_allowed_group wnag ON wn.id = wnag.network_id \ + WHERE wnag.group_id = $1", + id + ) + .fetch_all(executor) + .await + } else { + Ok(Vec::new()) + } + } } impl WireguardNetwork { @@ -125,7 +145,7 @@ impl WireguardNetwork { transaction: &mut PgConnection, allowed_groups: Vec, ) -> Result<(), ModelError> { - info!("Setting allowed groups for network {self}"); + info!("Setting allowed groups for network {self} to : {allowed_groups:?}"); if allowed_groups.is_empty() { return self.clear_allowed_groups(transaction).await; } diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index e60dfdfe99..fe7b8d4dd8 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -76,6 +76,7 @@ pub struct UserInfo { pub mfa_method: MFAMethod, pub authorized_apps: Vec, pub is_active: bool, + pub enrolled: bool, } impl UserInfo { @@ -96,10 +97,32 @@ impl UserInfo { groups, mfa_method: user.mfa_method.clone(), authorized_apps, - is_active: user.has_password(), + is_active: user.is_active, + enrolled: user.has_password(), }) } + /// Copy status to [`User`]. This function should be used by administrators. + /// + /// Return `true` if status was changed, `false` otherwise. + /// If status was changed to inactive, all user sessions will be invalidated. + pub(crate) async fn handle_status_change( + &self, + transaction: &mut PgConnection, + user: &mut User, + ) -> Result { + if self.is_active != user.is_active { + if !self.is_active { + user.logout_all_sessions(&mut *transaction).await?; + } + user.is_active = self.is_active; + user.save(&mut *transaction).await?; + Ok(true) + } else { + Ok(false) + } + } + /// Copy groups to [`User`]. This function should be used by administrators. /// /// Return `true` if groups were changed, `false` otherwise. diff --git a/src/db/models/session.rs b/src/db/models/session.rs index 79a9062096..db80fcbb68 100644 --- a/src/db/models/session.rs +++ b/src/db/models/session.rs @@ -192,4 +192,14 @@ impl Session { .await?; Ok(()) } + + pub async fn delete_all_for_user<'e, E>(executor: E, user_id: i64) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { + query!("DELETE FROM session WHERE user_id = $1", user_id) + .execute(executor) + .await?; + Ok(()) + } } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 65cf433465..2ea623127a 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -21,6 +21,7 @@ use super::{ }; use crate::{ auth::TOTP_CODE_VALIDITY_PERIOD, + db::Session, error::WebError, random::{gen_alphanumeric, gen_totp_secret}, server_config, @@ -59,6 +60,7 @@ pub struct UserDiagnostic { pub email_mfa_enabled: bool, pub mfa_method: MFAMethod, pub is_active: bool, + pub enrolled: bool, } #[derive(Model, PartialEq, Serialize, Clone, Debug)] @@ -71,6 +73,7 @@ pub struct User { pub email: String, pub phone: Option, pub mfa_enabled: bool, + pub is_active: bool, // secret has been verified and TOTP can be used pub(crate) totp_enabled: bool, pub(crate) email_mfa_enabled: bool, @@ -116,6 +119,7 @@ impl User { email_mfa_secret: None, mfa_method: MFAMethod::None, recovery_codes: Vec::new(), + is_active: true, } } @@ -447,7 +451,7 @@ impl User { ) -> Result, SqlxError> { let users = query!( "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, \ - mfa_method as \"mfa_method: MFAMethod\", password_hash \ + mfa_method as \"mfa_method: MFAMethod\", password_hash, is_active \ FROM \"user\"" ) .fetch_all(pool) @@ -460,7 +464,8 @@ impl User { email_mfa_enabled: u.email_mfa_enabled, mfa_enabled: u.mfa_enabled, id: u.id, - is_active: u.password_hash.is_some(), + is_active: u.is_active, + enrolled: u.password_hash.is_some(), }) .collect(); Ok(res) @@ -476,7 +481,7 @@ impl User { "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ - mfa_method \"mfa_method: _\", recovery_codes \ + mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id @@ -567,7 +572,7 @@ impl User { Self, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" WHERE username = $1", username ) @@ -583,7 +588,7 @@ impl User { Self, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" WHERE email = $1", email ) @@ -793,6 +798,16 @@ impl User { Ok(()) } + + pub async fn logout_all_sessions<'e, E>(&self, executor: E) -> Result<(), SqlxError> + where + E: PgExecutor<'e>, + { + if let Some(id) = self.id { + Session::delete_all_for_user(executor, id).await?; + } + Ok(()) + } } #[cfg(test)] diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index f030284bdf..5721cb32ee 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -185,12 +185,14 @@ impl WireguardNetwork { // run sync_allowed_devices on all wireguard networks pub async fn sync_all_networks(app: &AppState) -> Result<(), WireguardNetworkError> { + info!("Syncing allowed devices for all WireGuard locations"); let mut transaction = app.pool.begin().await?; let networks = Self::all(&mut *transaction).await?; for network in networks { let gateway_events = network.sync_allowed_devices(&mut transaction, None).await?; app.send_multiple_wireguard_events(gateway_events); } + transaction.commit().await?; Ok(()) } @@ -326,15 +328,23 @@ impl WireguardNetwork { JOIN group_user gu ON u.id = gu.user_id \ JOIN \"group\" g ON gu.group_id = g.id \ WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[])) + AND u.is_active = true ORDER BY d.id ASC", &allowed_groups ) .fetch_all(&mut *transaction) .await? }, - // all devices are allowed + // all devices of enabled users are allowed None => { - Device::all(&mut *transaction).await? + query_as!( + Device, + "SELECT d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created \ + FROM device d \ + JOIN \"user\" u ON d.user_id = u.id \ + WHERE u.is_active = true \ + ORDER BY d.id ASC" + ).fetch_all(&mut *transaction).await? } }; diff --git a/src/error.rs b/src/error.rs index 3c003b0f99..00db0dee6d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -134,9 +134,10 @@ impl From for WebError { TokenError::NotFound | TokenError::UserNotFound | TokenError::AdminNotFound => { WebError::ObjectNotFound(err.to_string()) } - TokenError::TokenExpired | TokenError::SessionExpired | TokenError::TokenUsed => { - WebError::Authorization(err.to_string()) - } + TokenError::TokenExpired + | TokenError::SessionExpired + | TokenError::TokenUsed + | TokenError::UserDisabled => WebError::Authorization(err.to_string()), TokenError::AlreadyActive => WebError::BadRequest(err.to_string()), TokenError::NotificationError(_) | TokenError::WelcomeMsgNotConfigured diff --git a/src/grpc/desktop_client_mfa.rs b/src/grpc/desktop_client_mfa.rs index 153fb91085..b10c0ccdce 100644 --- a/src/grpc/desktop_client_mfa.rs +++ b/src/grpc/desktop_client_mfa.rs @@ -73,7 +73,7 @@ impl ClientMfaServer { &mut self, request: ClientMfaStartRequest, ) -> Result { - info!("Starting desktop client login: {request:?}"); + debug!("Starting desktop client login: {request:?}"); // fetch location let Ok(Some(location)) = WireguardNetwork::find_by_id(&self.pool, request.location_id).await @@ -117,8 +117,9 @@ impl ClientMfaServer { .any(|allowed_group| user_info.groups.contains(allowed_group)) { error!( - "User {} not allowed to connect to location {location}", - user.username + "User {} not allowed to connect to location {location} because he doesn't belong to any of the allowed groups. + User groups: {:?}, allowed groups: {:?}", + user.username, user_info.groups, groups ); return Err(Status::unauthenticated("unauthorized")); } @@ -126,7 +127,7 @@ impl ClientMfaServer { // check if selected method is enabled let method = MfaMethod::try_from(request.method).map_err(|err| { - error!("Invalid MFA method selected: {err}"); + error!("Invalid MFA method selected ({}): {err}", request.method); Status::invalid_argument("invalid MFA method selected") })?; match method { @@ -159,6 +160,11 @@ impl ClientMfaServer { // generate auth token let token = Self::generate_token(&request.pubkey)?; + info!( + "Desktop client MFA login started for {} at location {}", + user.username, location.name + ); + // store login session self.sessions.insert( request.pubkey, @@ -177,7 +183,7 @@ impl ClientMfaServer { &mut self, request: ClientMfaFinishRequest, ) -> Result { - info!("Finishing desktop client login: {request:?}"); + debug!("Finishing desktop client login: {request:?}"); // get pubkey from token let pubkey = self.parse_token(&request.token)?; @@ -261,12 +267,17 @@ impl ClientMfaServer { Status::internal("unexpected error") })?; + info!( + "Desktop client login finished for {} at location {}", + user.username, location.name + ); + // remove login session from map self.sessions.remove(&pubkey); // commit transaction transaction.commit().await.map_err(|_| { - error!("Failed to commit transaction"); + error!("Failed to commit transaction while finishing desktop client login."); Status::internal("unexpected error") })?; diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 6a37bb8c74..16569d0d0b 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -99,6 +99,7 @@ impl EnrollmentServer { let enrollment = Token::find_by_id(&self.pool, token).await?; if enrollment.is_session_valid(server_config().enrollment_session_timeout.as_secs()) { + info!("Enrollment session validated"); Ok(enrollment) } else { error!("Enrollment session expired"); @@ -117,11 +118,13 @@ impl EnrollmentServer { &self, request: EnrollmentStartRequest, ) -> Result { + debug!("Starting enrollment session, request: {request:?}"); // fetch enrollment token let mut enrollment = Token::find_by_id(&self.pool, &request.token).await?; if let Some(token_type) = &enrollment.token_type { if token_type != ENROLLMENT_TOKEN_TYPE { + error!("Invalid token type used while trying to start enrollment: {token_type}"); return Err(Status::permission_denied("invalid token")); } @@ -129,19 +132,25 @@ impl EnrollmentServer { let user = enrollment.fetch_user(&self.pool).await?; let admin = enrollment.fetch_admin(&self.pool).await?; + if !user.is_active { + warn!("Can't start enrollment for disabled user {}", user.username); + return Err(Status::permission_denied("user is disabled")); + }; + let mut transaction = self.pool.begin().await.map_err(|_| { error!("Failed to begin transaction"); Status::internal("unexpected error") })?; // validate token & start session - info!("Starting enrollment session for user {}", user.username); + debug!("Starting enrollment session for user {}", user.username); let session_deadline = enrollment .start_session( &mut transaction, server_config().enrollment_session_timeout.as_secs(), ) .await?; + info!("Enrollment session started for user {}", user.username); let settings = Settings::get_settings(&mut *transaction) .await @@ -165,7 +174,7 @@ impl EnrollmentServer { let response = super::proto::EnrollmentStartResponse { admin: admin_info, user: Some(user_info), - deadline_timestamp: session_deadline.timestamp(), + deadline_timestamp: session_deadline.and_utc().timestamp(), final_page_content: enrollment .get_welcome_page_content(&mut transaction) .await?, @@ -211,12 +220,19 @@ impl EnrollmentServer { // fetch related users let mut user = enrollment.fetch_user(&self.pool).await?; - info!("Activating user account for {}", user.username); if user.has_password() { error!("User {} already activated", user.username); return Err(Status::invalid_argument("user already activated")); } + if !user.is_active { + warn!( + "Can't finalize enrollment for disabled user {}", + user.username + ); + return Err(Status::invalid_argument("user is disabled")); + } + let mut transaction = self.pool.begin().await.map_err(|_| { error!("Failed to begin transaction"); Status::internal("unexpected error") @@ -272,6 +288,8 @@ impl EnrollmentServer { Status::internal("unexpected error") })?; + info!("User {} activated", user.username); + Ok(()) } @@ -287,7 +305,12 @@ impl EnrollmentServer { let user = enrollment.fetch_user(&self.pool).await?; // add device - info!("Adding new device for user {}", user.username); + if !user.is_active { + error!("Can't create device for a disabled user {}", user.username); + return Err(Status::invalid_argument( + "can't add device to disabled user", + )); + } let ip_address; let device_info; @@ -362,6 +385,12 @@ impl EnrollmentServer { device_info.as_deref(), ) .map_err(|_| Status::internal("Failed to render new device added template"))?; + + info!( + "Device {} assigned to user {} and added to all networks.", + device.name, user.username + ); + let response = DeviceConfigResponse { device: Some(device.into()), configs: configs.into_iter().map(Into::into).collect(), @@ -377,6 +406,7 @@ impl EnrollmentServer { &self, request: ExistingDevice, ) -> Result { + debug!("Getting network info for device: {:?}", request.pubkey); let enrollment = self.validate_session(request.token.as_deref()).await?; // get enrollment user @@ -390,7 +420,7 @@ impl EnrollmentServer { let device = Device::find_by_pubkey(&self.pool, &request.pubkey) .await .map_err(|_| { - error!("Failed to get device"); + error!("Failed to get device by its pubkey: {}", request.pubkey); Status::internal("unexpected error") })?; @@ -400,7 +430,7 @@ impl EnrollmentServer { })?; let networks = WireguardNetwork::all(&self.pool).await.map_err(|err| { - error!("Invalid failed to get networks {err}"); + error!("Failed to fetch all networks: {err}"); Status::internal(format!("unexpected error: {err}")) })?; @@ -414,7 +444,7 @@ impl EnrollmentServer { WireguardNetworkDevice::find(&self.pool, device_id, network_id) .await .map_err(|err| { - error!("Invalid failed to get networks {err}"); + error!("Failed to fetch wireguard network device for device {} and network {}: {err}", device_id, network_id); Status::internal(format!("unexpected error: {err}")) })?; if let Some(wireguard_network_device) = wireguard_network_device { @@ -440,6 +470,8 @@ impl EnrollmentServer { } } + info!("Device {} configs fetched", device.name); + let response = DeviceConfigResponse { device: Some(device.into()), configs, @@ -465,7 +497,7 @@ impl From for AdminInfo { impl InitialUserInfo { async fn from_user(pool: &DbPool, user: User) -> Result { - let is_active = user.has_password(); + let is_enrolled = user.has_password(); let devices = user.devices(pool).await?; let device_names = devices.into_iter().map(|dev| dev.device.name).collect(); Ok(Self { @@ -474,8 +506,9 @@ impl InitialUserInfo { login: user.username, email: user.email, phone_number: user.phone, - is_active, + is_active: user.is_active, device_names, + enrolled: is_enrolled, }) } } @@ -510,7 +543,7 @@ impl From for ProtoDevice { name: device.name, pubkey: device.wireguard_pubkey, user_id: device.user_id, - created_at: device.created.timestamp(), + created_at: device.created.and_utc().timestamp(), } } } diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index f60f800732..dd8f53e3c2 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -4,7 +4,7 @@ use std::{ task::{Context, Poll}, }; -use chrono::{NaiveDateTime, Utc}; +use chrono::{DateTime, Utc}; use sqlx::{query, Error as SqlxError, PgExecutor}; use tokio::{ sync::{ @@ -49,7 +49,9 @@ impl WireguardNetwork { array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ + JOIN \"user\" u ON d.user_id = u.id \ WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) \ + AND u.is_active = true \ ORDER BY d.id ASC", self.id, self.mfa_enabled @@ -156,8 +158,9 @@ impl WireguardPeerStats { collected_at: Utc::now().naive_utc(), upload: stats.upload as i64, download: stats.download as i64, - latest_handshake: NaiveDateTime::from_timestamp_opt(stats.latest_handshake as i64, 0) - .unwrap_or_default(), + latest_handshake: DateTime::from_timestamp(stats.latest_handshake as i64, 0) + .unwrap_or_default() + .naive_utc(), allowed_ips: Some(stats.allowed_ips), } } @@ -234,7 +237,9 @@ impl GatewayUpdatesHandler { { Some(network_info) => { if self.network.mfa_enabled && !network_info.is_authorized { - debug!("Created WireGuard device is not authorized to connect to MFA enabled location"); + debug!("Created WireGuard device {} is not authorized to connect to MFA enabled location {}", + device.device.name, self.network.name + ); continue; }; self.send_peer_update( @@ -262,7 +267,9 @@ impl GatewayUpdatesHandler { { Some(network_info) => { if self.network.mfa_enabled && !network_info.is_authorized { - debug!("Modified WireGuard device is not authorized to connect to MFA enabled location"); + debug!("Modified WireGuard device {} is not authorized to connect to MFA enabled location {}", + device.device.name, self.network.name + ); continue; }; self.send_peer_update( @@ -326,11 +333,17 @@ impl GatewayUpdatesHandler { .await { let msg = format!( - "Failed to send network update, network {network}, update type: {update_type}, error: {err}", + "Failed to send network update, network {network}, update type: {update_type} ({}), error: {err}", + if update_type == 0 { + "CREATE" + } else { + "MODIFY" + }, ); error!(msg); return Err(Status::new(Code::Internal, msg)); } + debug!("Network update sent for network {network}"); Ok(()) } @@ -355,12 +368,13 @@ impl GatewayUpdatesHandler { .await { let msg = format!( - "Failed to send network update, network {}, update type: 2, error: {err}", + "Failed to send network update, network {}, update type: 2 (DELETE), error: {err}", self.network, ); error!(msg); return Err(Status::new(Code::Internal, msg)); } + debug!("Network delete command sent for network {}", self.network); Ok(()) } @@ -376,12 +390,18 @@ impl GatewayUpdatesHandler { .await { let msg = format!( - "Failed to send peer update for network {}, update type: {update_type}, error: {err}", - self.network + "Failed to send peer update for network {}, update type: {update_type} ({}), error: {err}", + self.network, + if update_type == 0 { + "CREATE" + } else { + "MODIFY" + }, ); error!(msg); return Err(Status::new(Code::Internal, msg)); } + debug!("Peer update sent for network {}", self.network); Ok(()) } @@ -402,12 +422,13 @@ impl GatewayUpdatesHandler { .await { let msg = format!( - "Failed to send peer update for network {}, peer {peer_pubkey}, update type: 2, error: {err}", + "Failed to send peer update for network {}, peer {peer_pubkey}, update type: 2 (DELETE), error: {err}", self.network, ); error!(msg); return Err(Status::new(Code::Internal, msg)); } + debug!("Peer delete command sent for network {}", self.network); Ok(()) } } @@ -476,8 +497,9 @@ impl gateway_service_server::GatewayService for GatewayServer { let network_id = Self::get_network_id(request.metadata())?; let mut stream = request.into_inner(); while let Some(stats_update) = stream.message().await? { + debug!("Received stats message: {stats_update:?}"); let Some(stats_update::Payload::PeerStats(peer_stats)) = stats_update.payload else { - debug!("Received empty stats message"); + debug!("Received stats message is empty, skipping."); continue; }; let public_key = peer_stats.public_key.clone(); @@ -485,9 +507,15 @@ impl gateway_service_server::GatewayService for GatewayServer { // Get device by public key and fill in stats.device_id // FIXME: keep an in-memory device map to avoid repeated DB requests stats.device_id = match Device::find_by_pubkey(&self.pool, &public_key).await { - Ok(Some(device)) => device - .id - .ok_or_else(|| Status::new(Code::Internal, "Device has no ID"))?, + Ok(Some(device)) => device.id.ok_or_else(|| { + Status::new( + Code::Internal, + format!( + "Device {} (public key: {public_key}) has no ID", + device.name + ), + ) + })?, Ok(None) => { error!("Device with public key {public_key} not found"); return Err(Status::new( @@ -530,9 +558,14 @@ impl gateway_service_server::GatewayService for GatewayServer { error!("Network {network_id} not found"); Status::new(Code::Internal, format!("Failed to retrieve network: {e}")) })? - .ok_or_else(|| Status::new(Code::Internal, "Network not found"))?; + .ok_or_else(|| { + Status::new( + Code::Internal, + format!("Network with id {} not found", network_id), + ) + })?; - info!("Sending configuration to gateway client, network {network}."); + debug!("Sending configuration to gateway client, network {network}."); // store connected gateway in memory { @@ -548,17 +581,19 @@ impl gateway_service_server::GatewayService for GatewayServer { network.connected_at = Some(Utc::now().naive_utc()); if let Err(err) = network.save(&self.pool).await { - error!("Failed to update network {network_id} status: {err}"); + error!("Failed to save updated network {network_id} in the database, status: {err}"); } let peers = network.get_peers(&self.pool).await.map_err(|error| { - error!("Failed to fetch peers for network {network_id}: {error}",); + error!("Failed to fetch peers from the database for network {network_id}: {error}",); Status::new( Code::Internal, - format!("Failed to retrieve peers for network: {network_id}"), + format!("Failed to retrieve peers from the database for network: {network_id}"), ) })?; + info!("Configuration sent to gateway client, network {network}."); + Ok(Response::new(gen_config(&network, peers))) } @@ -569,14 +604,17 @@ impl gateway_service_server::GatewayService for GatewayServer { let Some(network) = WireguardNetwork::find_by_id(&self.pool, gateway_network_id) .await .map_err(|_| { - error!("Failed to fetch network {gateway_network_id}"); + error!("Failed to fetch network {gateway_network_id} from the database"); Status::new( Code::Internal, - format!("Failed to retrieve network {gateway_network_id}"), + format!("Failed to retrieve network {gateway_network_id} from the database"), ) })? else { - return Err(Status::new(Code::Internal, "Network not found")); + return Err(Status::new( + Code::Internal, + format!("Network with id {gateway_network_id} not found"), + )); }; info!("New client connected to updates stream: {hostname}, network {network}",); @@ -587,8 +625,11 @@ impl gateway_service_server::GatewayService for GatewayServer { state .connect_gateway(gateway_network_id, &hostname) .map_err(|err| { - error!("Failed to connect gateway: {err}"); - Status::new(Code::Internal, "Failed to connect gateway") + error!("Failed to connect gateway on network {gateway_network_id}: {err}"); + Status::new( + Code::Internal, + "Failed to connect gateway on network {gateway_network_id}", + ) })?; // clone here before moving into a closure diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 98cd6c088a..11f44a0bfd 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -121,7 +121,7 @@ impl GatewayMap { // remove gateway from map pub fn remove_gateway(&mut self, network_id: i64, uid: Uuid) -> Result<(), GatewayMapError> { - info!("Removing gateway from network {network_id}"); + debug!("Removing gateway from network {network_id}"); if let Some(network_gateway_map) = self.0.get_mut(&network_id) { // find gateway by uuid let hostname = match network_gateway_map @@ -134,6 +134,7 @@ impl GatewayMap { } Some((hostname, state)) => { if state.connected { + error!("Cannot remove. Gateway with UID {uid} is still active"); return Err(GatewayMapError::RemoveActive(uid)); } hostname.clone() @@ -146,6 +147,7 @@ impl GatewayMap { error!("Network {network_id} not found in gateway map"); return Err(GatewayMapError::NetworkNotFound(network_id)); }; + info!("Gateway with UID {uid} removed from network {network_id}"); Ok(()) } @@ -156,12 +158,16 @@ impl GatewayMap { network_id: i64, hostname: &str, ) -> Result<(), GatewayMapError> { - info!("Connecting gateway {hostname} in network {network_id}"); + debug!("Connecting gateway {hostname} in network {network_id}"); if let Some(network_gateway_map) = self.0.get_mut(&network_id) { if let Some(state) = network_gateway_map.get_mut(hostname) { state.connected = true; state.disconnected_at = None; state.connected_at = Some(Utc::now().naive_utc()); + debug!( + "Gateway {hostname} found in gateway map, current state: {:#?}", + state + ); } else { error!("Gateway {hostname} not found in gateway map for network {network_id}"); return Err(GatewayMapError::NotFound(network_id, hostname.into())); @@ -171,6 +177,7 @@ impl GatewayMap { error!("Network {network_id} not found in gateway map"); return Err(GatewayMapError::NetworkNotFound(network_id)); }; + info!("Gateway {hostname} connected in network {network_id}"); Ok(()) } @@ -181,12 +188,17 @@ impl GatewayMap { hostname: String, pool: &DbPool, ) -> Result<(), GatewayMapError> { - info!("Disconnecting gateway {hostname} in network {network_id}"); + debug!("Disconnecting gateway {hostname} in network {network_id}"); if let Some(network_gateway_map) = self.0.get_mut(&network_id) { if let Some(state) = network_gateway_map.get_mut(&hostname) { state.connected = false; state.disconnected_at = Some(Utc::now().naive_utc()); state.send_disconnect_notification(pool)?; + debug!( + "Gateway {hostname} found in gateway map, current state: {:#?}", + state + ); + info!("Gateway {hostname} disconnected in network {network_id}"); return Ok(()); }; }; @@ -279,6 +291,7 @@ impl GatewayState { /// Send gateway disconnected notification /// Sends notification only if last notification time is bigger than specified in config fn send_disconnect_notification(&mut self, pool: &DbPool) -> Result<(), GatewayMapError> { + debug!("Sending gateway disconnect email notification"); // Clone here because self doesn't live long enough let name = self.name.clone(); let mail_tx = self.mail_tx.clone(); @@ -304,10 +317,12 @@ impl GatewayState { .await { error!("Failed to send gateway disconnect notification: {e}"); + } else { + info!("Gateway {hostname} disconnected. Email notification sent",); } }); } else { - debug!( + info!( "Gateway {hostname} disconnected. Email notification not sent. Last notification was at {:?}", self.last_email_notification ); @@ -361,17 +376,18 @@ pub async fn run_grpc_bidi_stream( }; loop { - info!("Connecting to proxy"); + debug!("Connecting to proxy at {}", endpoint.uri()); let mut client = ProxyClient::new(endpoint.connect_lazy()); let (tx, rx) = mpsc::unbounded_channel(); let Ok(response) = client.bidi(UnboundedReceiverStream::new(rx)).await else { - info!("Failed to connect to proxy, retrying in 10s"); + error!("Failed to connect to proxy, retrying in 10s"); sleep(TEN_SECS).await; continue; }; + info!("Connected to proxy at {}", endpoint.uri()); let mut resp_stream = response.into_inner(); while let Some(received) = resp_stream.next().await { - info!("received message"); + info!("Received message from proxy"); match received { Ok(received) => { let payload = match received.payload { @@ -529,7 +545,7 @@ pub async fn run_grpc_server( ); // Run gRPC server let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), server_config().grpc_port); - info!("Starting gRPC services"); + debug!("Starting gRPC services"); let builder = if let (Some(cert), Some(key)) = (grpc_cert, grpc_key) { let identity = Identity::from_pem(cert, key); Server::builder().tls_config(ServerTlsConfig::new().identity(identity))? @@ -545,6 +561,7 @@ pub async fn run_grpc_server( #[cfg(feature = "worker")] let router = router.add_service(worker_service); router.serve(addr).await?; + info!("gRPC server started on {addr}"); Ok(()) } diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index 12a523341e..d94c25c7c1 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -40,6 +40,7 @@ impl PasswordResetServer { // check if token provided with request corresponds to a valid enrollment session async fn validate_session(&self, token: Option<&str>) -> Result { + debug!("Validating enrollment session"); let Some(token) = token else { error!("Missing authorization header in request"); return Err(Status::unauthenticated("Missing authorization header")); @@ -49,6 +50,10 @@ impl PasswordResetServer { let enrollment = Token::find_by_id(&self.pool, token).await?; if enrollment.is_session_valid(server_config().enrollment_session_timeout.as_secs()) { + info!( + "Enrollment session validated for user {}.", + enrollment.user_id + ); Ok(enrollment) } else { error!("Enrollment session expired"); @@ -79,17 +84,22 @@ impl PasswordResetServer { let user = User::find_by_email(&self.pool, email.to_string().as_str()) .await .map_err(|_| { - error!("Failed to fetch user by email"); + error!("Failed to fetch user by email: {email}"); Status::internal("unexpected error") })?; let Some(user) = user else { // Do not return information whether user exists + debug!("Password reset skipped for non-existing user {email}"); return Ok(()); }; - // Do not allow password change if user is not active - if !user.has_password() { + // Do not allow password change if user is disabled or not enrolled + if !user.has_password() || !user.is_active { + debug!( + "Password reset skipped for disabled or not enrolled user {} ({email})", + user.username + ); return Ok(()); } @@ -127,6 +137,11 @@ impl PasswordResetServer { Some(&user_agent), )?; + info!( + "Finished processing password reset request for user {}.", + user.username + ); + Ok(()) } @@ -139,13 +154,23 @@ impl PasswordResetServer { let mut enrollment = Token::find_by_id(&self.pool, &request.token).await?; if enrollment.token_type != Some("PASSWORD_RESET".to_string()) { + error!( + "Invalid token type ({:?}) for password reset session", + enrollment.token_type + ); return Err(Status::permission_denied("invalid token")); } let user = enrollment.fetch_user(&self.pool).await?; - if !user.has_password() { - return Err(Status::permission_denied("user inactive")); + if !user.has_password() || !user.is_active { + error!( + "Can't start password reset for a disabled or not enrolled user {}.", + user.username + ); + return Err(Status::permission_denied( + "user disabled or not yet enrolled", + )); } let mut transaction = self.pool.begin().await.map_err(|_| { @@ -161,7 +186,7 @@ impl PasswordResetServer { .await?; let response = PasswordResetStartResponse { - deadline_timestamp: session_deadline.timestamp(), + deadline_timestamp: session_deadline.and_utc().timestamp(), }; transaction.commit().await.map_err(|_| { @@ -169,6 +194,11 @@ impl PasswordResetServer { Status::internal("unexpected error") })?; + info!( + "Finished processing password reset session for user {}.", + user.username + ); + Ok(response) } @@ -197,6 +227,14 @@ impl PasswordResetServer { let mut user = enrollment.fetch_user(&self.pool).await?; + if !user.is_active { + error!( + "Can't reset password for a disabled user {}.", + user.username + ); + return Err(Status::permission_denied("user disabled")); + } + let mut transaction = self.pool.begin().await.map_err(|_| { error!("Failed to begin transaction"); Status::internal("unexpected error") diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index a4a1cbffb7..d5c076aac1 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -59,7 +59,14 @@ pub async fn authenticate( let user = match User::find_by_username(&appstate.pool, &username).await { Ok(Some(user)) => match user.verify_password(&data.password) { - Ok(()) => user, + Ok(()) => { + if user.is_active { + user + } else { + info!("Failed to authenticate user {username}: user is disabled"); + return Err(WebError::Authorization("user not found".into())); + } + } Err(err) => { info!("Failed to authenticate user {username}: {err}"); log_failed_login_attempt(&appstate.failed_logins, &username); @@ -91,7 +98,11 @@ pub async fn authenticate( let agent = parse_user_agent(&appstate.user_agent_parser, &user_agent_string); let device_info = agent.clone().map(|v| get_user_agent_device(&v)); + debug!("Cleaning up expired sessions..."); Session::delete_expired(&appstate.pool).await?; + debug!("Expired sessions cleaned up"); + + debug!("Creating new session for user {username}"); let session = Session::new( user.id.unwrap(), SessionState::PasswordVerified, @@ -99,6 +110,7 @@ pub async fn authenticate( device_info, ); session.save(&appstate.pool).await?; + debug!("New session created for user {username}"); let max_age = Duration::seconds(server_config().auth_cookie_timeout.as_secs() as i64); let config = server_config(); @@ -140,6 +152,7 @@ pub async fn authenticate( }, )) } else { + error!("Couldn't fetch MFA info for user {username} with MFA enabled"); Err(WebError::DbError("MFA info read error".into())) } } else { @@ -203,7 +216,7 @@ pub async fn logout( /// Enable MFA pub async fn mfa_enable( cookies: CookieJar, - session: Session, + _session: Session, session_info: SessionInfo, State(appstate): State, ) -> Result<(CookieJar, ApiResponse), WebError> { @@ -213,9 +226,9 @@ pub async fn mfa_enable( if user.mfa_enabled { info!("Enabled MFA for user {}", user.username); let cookies = cookies.remove(Cookie::from("defguard_sesssion")); - session.delete(&appstate.pool).await?; + user.logout_all_sessions(&appstate.pool).await?; debug!( - "Removed auth session for user {} after enabling MFA", + "Removed auth sessions for user {} after enabling MFA", user.username ); Ok((cookies, ApiResponse::default())) diff --git a/src/handlers/group.rs b/src/handlers/group.rs index 64e708ead4..748bdc5388 100644 --- a/src/handlers/group.rs +++ b/src/handlers/group.rs @@ -5,7 +5,7 @@ use axum::{ use serde_json::json; use sqlx::query_as; -use super::{ApiResponse, GroupInfo, Username}; +use super::{ApiResponse, EditGroupInfo, GroupInfo, Username}; use crate::{ appstate::AppState, auth::{SessionInfo, UserAdminRole}, @@ -47,7 +47,7 @@ pub(crate) async fn bulk_assign_to_groups( User, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" WHERE id = ANY($1)", &data.users ) @@ -98,10 +98,13 @@ pub(crate) async fn list_groups_info( let q_result = query_as!( GroupInfo, "SELECT g.name as name, \ - COALESCE(ARRAY_AGG(u.username) FILTER (WHERE u.username IS NOT NULL), '{}') as members \ + COALESCE(ARRAY_AGG(DISTINCT u.username) FILTER (WHERE u.username IS NOT NULL), '{}') as \"members!\", \ + COALESCE(ARRAY_AGG(DISTINCT wn.name) FILTER (WHERE wn.name IS NOT NULL), '{}') as \"vpn_locations!\" \ FROM \"group\" g \ LEFT JOIN \"group_user\" gu ON gu.group_id = g.id \ LEFT JOIN \"user\" u ON u.id = gu.user_id \ + LEFT JOIN \"wireguard_network_allowed_group\" wnag ON wnag.group_id = g.id \ + LEFT JOIN \"wireguard_network\" wn ON wn.id = wnag.network_id \ GROUP BY g.name" ) .fetch_all(&appstate.pool) @@ -139,9 +142,10 @@ pub(crate) async fn get_group( debug!("Retrieving group {name}"); if let Some(group) = Group::find_by_name(&appstate.pool, &name).await? { let members = group.member_usernames(&appstate.pool).await?; + let vpn_locations = group.allowed_vpn_locations(&appstate.pool).await?; info!("Retrieved group {name}"); Ok(ApiResponse { - json: json!(GroupInfo::new(name, Some(members))), + json: json!(GroupInfo::new(name, members, vpn_locations)), status: StatusCode::OK, }) } else { @@ -155,7 +159,7 @@ pub(crate) async fn get_group( pub(crate) async fn create_group( _role: UserAdminRole, State(appstate): State, - Json(group_info): Json, + Json(group_info): Json, ) -> Result { debug!("Creating group {}", group_info.name); @@ -167,16 +171,14 @@ pub(crate) async fn create_group( group.save(&appstate.pool).await?; // TODO: create group in LDAP - if let Some(ref members) = group_info.members { - for username in members { - let Some(user) = User::find_by_username(&mut *transaction, username).await? else { - let msg = format!("Failed to find user {username}"); - error!(msg); - return Err(WebError::ObjectNotFound(msg)); - }; - user.add_to_group(&mut *transaction, &group).await?; - // let _result = ldap_add_user_to_group(&mut *transaction, username, &group.name).await; - } + for username in &group_info.members { + let Some(user) = User::find_by_username(&mut *transaction, username).await? else { + let msg = format!("Failed to find user {username}"); + error!(msg); + return Err(WebError::ObjectNotFound(msg)); + }; + user.add_to_group(&mut *transaction, &group).await?; + // TODO: update LDAP } transaction.commit().await?; @@ -195,7 +197,7 @@ pub(crate) async fn modify_group( _role: UserAdminRole, State(appstate): State, Path(name): Path, - Json(group_info): Json, + Json(group_info): Json, ) -> Result { debug!("Modifying group {}", group_info.name); let Some(mut group) = Group::find_by_name(&appstate.pool, &name).await? else { @@ -211,38 +213,34 @@ pub(crate) async fn modify_group( if group.name != group_info.name { group.name = group_info.name; group.save(&mut *transaction).await?; - // let _result = ldap_modify_group(&mut *transaction, &group.name, &group).await; + // TODO: update LDAP } // Modify group members. - if let Some(ref members) = group_info.members { - let mut current_members = group.members(&mut *transaction).await?; - for username in members { - if let Some(index) = current_members - .iter() - .position(|gm| &gm.username == username) - { - // This member is already in the group. - current_members.remove(index); - continue; - } - - // Add new members to the group. - if let Some(user) = User::find_by_username(&mut *transaction, username).await? { - user.add_to_group(&mut *transaction, &group).await?; - // let _result = - // ldap_add_user_to_group(&mut *transaction, username, &group.name).await; - } + let mut current_members = group.members(&mut *transaction).await?; + for username in &group_info.members { + if let Some(index) = current_members + .iter() + .position(|gm| &gm.username == username) + { + // This member is already in the group. + current_members.remove(index); + continue; } - // Remove outstanding members. - for user in current_members { - user.remove_from_group(&mut *transaction, &group).await?; - // let _result = - // ldap_remove_user_from_group(&mut *transaction, &user.username, &group.name).await; + // Add new members to the group. + if let Some(user) = User::find_by_username(&mut *transaction, username).await? { + user.add_to_group(&mut *transaction, &group).await?; + // TODO: update LDAP } } + // Remove outstanding members. + for user in current_members { + user.remove_from_group(&mut *transaction, &group).await?; + // TODO: update LDAP + } + transaction.commit().await?; WireguardNetwork::sync_all_networks(&appstate).await?; @@ -272,6 +270,7 @@ pub(crate) async fn delete_group( group.delete(&appstate.pool).await?; // TODO: delete group from LDAP + // sync allowed devices for all locations WireguardNetwork::sync_all_networks(&appstate).await?; info!("Deleted group {name}"); @@ -325,8 +324,8 @@ pub(crate) async fn remove_group_member( user.username, group.name ); user.remove_from_group(&appstate.pool, &group).await?; - // let _result = - // ldap_remove_user_from_group(&appstate.pool, &user.username, &group.name).await; + // TODO: update LDAP + WireguardNetwork::sync_all_networks(&appstate).await?; info!("Removed user: {} from group: {}", user.username, group.name); Ok(ApiResponse { diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 768256ca3c..0ee11dc383 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -173,19 +173,28 @@ impl AuthCode { #[derive(Deserialize, Serialize)] pub struct GroupInfo { pub name: String, - pub members: Option>, + pub members: Vec, + pub vpn_locations: Vec, } impl GroupInfo { #[must_use] - pub fn new>(name: S, members: Option>) -> Self { + pub fn new>(name: S, members: Vec, vpn_locations: Vec) -> Self { Self { name: name.into(), members, + vpn_locations, } } } +/// Dedicated `GroupInfo` variant for group modification operations. +#[derive(Deserialize, Serialize)] +pub struct EditGroupInfo { + pub name: String, + pub members: Vec, +} + #[derive(Deserialize, Serialize)] pub struct Username { pub username: String, diff --git a/src/handlers/openid_flow.rs b/src/handlers/openid_flow.rs index 7676d03e13..aa46c59b6d 100644 --- a/src/handlers/openid_flow.rs +++ b/src/handlers/openid_flow.rs @@ -40,7 +40,7 @@ use crate::{ auth::{AccessUserInfo, SessionInfo}, db::{ models::{auth_code::AuthCode, oauth2client::OAuth2Client}, - DbPool, OAuth2AuthorizedApp, OAuth2Token, Session, User, + DbPool, OAuth2AuthorizedApp, OAuth2Token, Session, SessionState, User, }, error::WebError, handlers::{mail::send_new_device_ocid_login_email, SIGN_IN_COOKIE_NAME}, @@ -402,6 +402,22 @@ pub async fn authorization( let _result = session.delete(&appstate.pool).await; login_redirect(&data, private_cookies).await } else { + let user = User::find_by_id(&appstate.pool, session.user_id) + .await? + .ok_or(WebError::Authorization("User not found".into()))?; + + // Session exists even if user hasn't completed MFA verification yet, + // thus we need to check if MFA is enabled and the verification is done. + if user.mfa_enabled + && session.state != SessionState::MultiFactorVerified + { + info!( + "MFA not verified for user id {}, redirecting to login", + session.user_id + ); + return login_redirect(&data, private_cookies).await; + } + // If session is present check if app is in user authorized apps. // If yes return auth code and state else redirect to consent form. if let Some(app) = diff --git a/src/handlers/ssh_authorized_keys.rs b/src/handlers/ssh_authorized_keys.rs index d7fa096897..fd2f69f963 100644 --- a/src/handlers/ssh_authorized_keys.rs +++ b/src/handlers/ssh_authorized_keys.rs @@ -174,6 +174,7 @@ pub async fn add_authentication_key( // authorize request let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; let Some(user_id) = user.id else { + error!("Model returned user ({}) without ID", user.username); return Err(WebError::ModelError("Model returned without ID".into())); }; @@ -184,6 +185,7 @@ pub async fn add_authentication_key( AuthenticationKeyType::Ssh => { let parsed = trimmed_key.parse::(); if parsed.is_err() { + error!("User {username} tried to insert invalid SSH key: {data:?}"); return Err(WebError::BadRequest("SSH key failed verification.".into())); } } @@ -230,6 +232,7 @@ pub async fn fetch_authentication_keys( ) -> ApiResult { let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; let Some(user_id) = user.id else { + error!("Model returned user ({}) without ID", user.username); return Err(WebError::ModelError( "Model returned user without ID".into(), )); @@ -258,6 +261,7 @@ pub async fn delete_authentication_key( } key.delete(&appstate.pool).await?; } else { + error!("Key with id {} not found", key_id); return Err(WebError::BadRequest("Key not found".into())); } Ok(ApiResponse { @@ -283,14 +287,26 @@ pub async fn rename_authentication_key( .ok_or(WebError::DbError("Returned user had no ID".into()))?; if let Some(mut key) = AuthenticationKey::find_by_id(&appstate.pool, key_id).await? { if key.yubikey_id.is_some() { + warn!( + "User {} tried to rename authentication key instead of yubikey", + username + ); return Err(WebError::BadRequest("Rename yubikey instead.".into())); } if !session.is_admin && user_id != key.user_id { + warn!( + "User {} tried to rename key ({}) of another user with id {}", + username, key_id, key.user_id + ); return Err(WebError::Forbidden(String::new())); } key.name = Some(data.name); key.save(&appstate.pool).await?; } else { + error!( + "User {} tried to rename non-existing key with id {}", + username, key_id + ); return Err(WebError::ObjectNotFound(String::new())); } diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 78803fe551..44608a054c 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -126,7 +126,7 @@ pub async fn add_user( // check username if let Err(err) = check_username(&username) { - debug!("{err}"); + debug!("Username {username} rejected: {err}"); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -165,8 +165,8 @@ pub async fn add_user( let user_info = UserInfo::from_user(&appstate.pool, &user).await?; appstate.trigger_action(AppEvent::UserCreated(user_info.clone())); info!("User {} added user {username}", session.user.username); - if !user.has_password() { - warn!("User {username} is not active yet. Please proceed with enrollment."); + if !user_info.enrolled { + warn!("User {username} hasn't been enrolled yet. Please proceed with enrollment."); }; Ok(ApiResponse { json: json!(&user_info), @@ -189,12 +189,17 @@ pub async fn start_enrollment( // validate request if data.send_enrollment_notification && data.email.is_none() { + error!( + "Email notification is enabled for user {}, but email was not provided", + session.user.username + ); return Err(WebError::BadRequest( "Email notification is enabled, but email was not provided".into(), )); } let Some(user) = User::find_by_username(&appstate.pool, &username).await? else { + error!("User {username} couldn't be found, enrollment aborted"); return Err(WebError::ObjectNotFound(format!( "user {username} not found" ))); @@ -217,6 +222,11 @@ pub async fn start_enrollment( transaction.commit().await?; + info!( + "User {} started enrollment for user {username}", + session.user.username + ); + Ok(ApiResponse { json: json!({"enrollment_token": enrollment_token, "enrollment_url": config.enrollment_url.to_string()}), status: StatusCode::CREATED, @@ -259,6 +269,11 @@ pub async fn start_remote_desktop_configuration( transaction.commit().await?; + info!( + "User {} started enrollment for user {username}", + session.user.username + ); + Ok(ApiResponse { json: json!({"enrollment_token": enrollment_token, "enrollment_url": config.enrollment_url.to_string()}), status: StatusCode::CREATED, @@ -271,14 +286,17 @@ pub async fn username_available( Json(data): Json, ) -> ApiResult { if let Err(err) = check_username(&data.username) { - debug!("{err}"); + debug!("Username {} rejected: {err}", data.username); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, }); }; let status = match User::find_by_username(&appstate.pool, &data.username).await? { - Some(_) => StatusCode::BAD_REQUEST, + Some(_) => { + debug!("Username {} is not available", data.username); + StatusCode::BAD_REQUEST + } None => StatusCode::OK, }; Ok(ApiResponse { @@ -296,7 +314,7 @@ pub async fn modify_user( debug!("User {} updating user {username}", session.user.username); let mut user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; if let Err(err) = check_username(&user_info.username) { - debug!("Failed to check username {} {err}", user_info.username); + debug!("Username {} rejected: {err}", user_info.username); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -322,16 +340,33 @@ pub async fn modify_user( .await?; } if session.is_admin { - // update VPN gateway config if groups have changed + // prevent admin from disabling himself + if session.user.username == username && !user_info.is_active { + debug!("Admin {username} attempted to disable himself"); + return Ok(ApiResponse { + json: json!({}), + status: StatusCode::BAD_REQUEST, + }); + } + + // update VPN gateway config if user status or groups have changed if user_info .handle_user_groups(&mut transaction, &mut user) .await? + || user_info + .handle_status_change(&mut transaction, &mut user) + .await? { + debug!( + "User {} changed {username} groups or status, syncing allowed network devices", + session.user.username + ); let networks = WireguardNetwork::all(&mut *transaction).await?; for network in networks { let gateway_events = network.sync_allowed_devices(&mut transaction, None).await?; appstate.send_multiple_wireguard_events(gateway_events); } + info!("Allowed network devices of {username} synced"); }; user_info.into_user_all_fields(&mut user)?; } else { @@ -339,6 +374,7 @@ pub async fn modify_user( } user.save(&mut *transaction).await?; + // TODO: Reflect user status (active/disabled) modification in ldap let _result = ldap_modify_user(&appstate.pool, &username, &user).await; let user_info = UserInfo::from_user(&appstate.pool, &user).await?; appstate.trigger_action(AppEvent::UserModified(user_info)); @@ -382,6 +418,7 @@ pub async fn change_self_password( State(appstate): State, Json(data): Json, ) -> ApiResult { + debug!("User {} is changing his password.", session.user.username); let mut user = session.user; if user.verify_password(&data.old_password).is_err() { return Ok(ApiResponse { @@ -390,7 +427,8 @@ pub async fn change_self_password( }); } - if check_password_strength(&data.new_password).is_err() { + if let Err(err) = check_password_strength(&data.new_password) { + debug!("User {} password change failed: {err}", user.username); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -402,7 +440,7 @@ pub async fn change_self_password( let _ = ldap_change_password(&appstate.pool, &user.username, &data.new_password).await; - info!("User {} changed password.", &user.username); + info!("User {} changed his password.", &user.username); Ok(ApiResponse { json: json!({}), @@ -423,7 +461,7 @@ pub async fn change_password( ); if session.user.username == username { - debug!("Cannot change own password with this endpoint."); + debug!("Cannot change own ({username}) password with this endpoint."); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -431,14 +469,14 @@ pub async fn change_password( } if let Err(err) = check_password_strength(&data.new_password) { - debug!("Pasword not strong enough: {err}"); + debug!("Password for user {username} not strong enough: {err}"); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, }); } if let Err(err) = check_username(&username) { - debug!("Invalid Username: {err}"); + debug!("Invalid username ({username}): {err}"); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -457,7 +495,7 @@ pub async fn change_password( ); Ok(ApiResponse::default()) } else { - debug!("User not found"); + debug!("Can't change password for user {username}, user not found"); Ok(ApiResponse { json: json!({}), status: StatusCode::NOT_FOUND, @@ -472,12 +510,12 @@ pub async fn reset_password( Path(username): Path, ) -> ApiResult { debug!( - "Admin {} changing password for user {username}", + "Admin {} resetting password for user {username}", session.user.username, ); if session.user.username == username { - debug!("Cannot change own password with this endpoint."); + debug!("Cannot reset own ({username}) password with this endpoint."); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -522,11 +560,13 @@ pub async fn reset_password( match &appstate.mail_tx.send(mail) { Ok(()) => { - info!("Password reset email sent to {to}"); + info!("Password reset email for {username} sent to {to}"); Ok(()) } Err(err) => { - error!("Failed to send password reset email to {to} with error:\n{err}"); + error!( + "Failed to send password reset email for {username} to {to} with error: {err}" + ); Err(WebError::Serialization(format!( "Could not send password reset email to user {username}" ))) @@ -536,12 +576,12 @@ pub async fn reset_password( transaction.commit().await?; info!( - "Admin {} changed password for user {username}", + "Admin {} reset password for user {username}", session.user.username ); Ok(ApiResponse::default()) } else { - debug!("User not found"); + debug!("Can't reset password for user {username}, user not found"); Ok(ApiResponse { json: json!({}), status: StatusCode::NOT_FOUND, @@ -575,6 +615,10 @@ pub async fn wallet_challenge( .await? { if wallet.validation_timestamp.is_some() { + error!( + "Can't generate wallet challange for user {username}, the wallet {} is already validated", + wallet_info.address + ); return Err(WebError::ObjectNotFound("wrong address".into())); } wallet @@ -583,6 +627,7 @@ pub async fn wallet_challenge( if let Some(settings) = Settings::find_by_id(&appstate.pool, 1).await? { Wallet::format_challenge(&wallet_info.address, &settings.challenge_template) } else { + error!("Cannot retrieve settings"); return Err(WebError::DbError("cannot retrieve settings".into())); }; let mut wallet = Wallet::new_for_user( @@ -634,9 +679,17 @@ pub async fn set_wallet( ); Ok(ApiResponse::default()) } else { + error!( + "User {} failed to set wallet signature for user {username}, wallet ({}) signature {} is invalid", + session.user.username, wallet_info.address, wallet_info.signature + ); Err(WebError::ObjectNotFound("wrong address".into())) } } else { + error!( + "User {} failed to set wallet signature for user {username}, address {} not found", + session.user.username, wallet_info.address + ); Err(WebError::ObjectNotFound("wallet not found".into())) } } @@ -695,9 +748,17 @@ pub async fn update_wallet( ); Ok(ApiResponse::default()) } else { + error!( + "User {} failed to update wallet {address} for user {username} (id: {:?}), the owner id is {}", + session.user.username, user.id, wallet.user_id + ); Err(WebError::ObjectNotFound("wrong wallet".into())) } } else { + error!( + "User {} failed to update wallet {address} for user {username}, wallet not found", + session.user.username + ); Err(WebError::ObjectNotFound("wallet not found".into())) } } @@ -725,9 +786,17 @@ pub async fn delete_wallet( ); Ok(ApiResponse::default()) } else { + error!( + "User {} failed to delete wallet {address} for user {username} (id: {:?}), the owner id is {}", + session.user.username, user.id, wallet.user_id + ); Err(WebError::ObjectNotFound("wrong wallet".into())) } } else { + error!( + "User {} failed to delete wallet {address} for user {username}, wallet not found", + session.user.username + ); Err(WebError::ObjectNotFound("wallet not found".into())) } } @@ -752,9 +821,17 @@ pub async fn delete_security_key( ); Ok(ApiResponse::default()) } else { + error!( + "User {} failed to delete security key {id} for user {username} (id: {:?}), the owner id is {}", + session.user.username, user.id, webauthn.user_id + ); Err(WebError::ObjectNotFound("wrong security key".into())) } } else { + error!( + "User {} failed to delete security key {id} for user {username}, security key not found", + session.user.username + ); Err(WebError::ObjectNotFound("security key not found".into())) } } @@ -793,9 +870,17 @@ pub async fn delete_authorized_app( ); Ok(ApiResponse::default()) } else { + error!( + "User {} failed to delete OAuth2 client {oauth2client_id} for user {username} (id: {:?}), the app owner id is {}", + session.user.username, user.id, app.user_id + ); Err(WebError::ObjectNotFound("Wrong app".into())) } } else { + error!( + "User {} failed to delete OAuth2 client {oauth2client_id} for user {username}, authorized app not found", + session.user.username + ); Err(WebError::ObjectNotFound("Authorized app not found".into())) } } diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 9350061aa6..b545e3c575 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -124,7 +124,7 @@ pub async fn create_network( .send_wireguard_event(GatewayEvent::NetworkCreated(*network_id, network.clone())); } None => { - error!("Network {} ID was not created during network creation, gateway event was not send!", network.name); + error!("Network {} ID was not created during network creation, gateway event was not sent!", network.name); return Ok(ApiResponse { json: json!({}), status: StatusCode::INTERNAL_SERVER_ERROR, @@ -193,7 +193,7 @@ pub async fn modify_network( } &None => { error!( - "Network {} id not found, gateway update not send!", + "Network {} id not found, gateway update not sent!", network.name ); } @@ -309,6 +309,7 @@ pub async fn gateway_status( let gateway_state = gateway_state .lock() .expect("Failed to acquire gateway state lock"); + debug!("Displayed gateway status for network {network_id}"); Ok(ApiResponse { json: json!(gateway_state.get_network_gateway_status(network_id)), @@ -321,7 +322,7 @@ pub async fn remove_gateway( _role: VpnRole, Extension(gateway_state): Extension>>, ) -> ApiResult { - info!("Removing gateway {gateway_id} in network {network_id}"); + debug!("Removing gateway {gateway_id} in network {network_id}"); let mut gateway_state = gateway_state .lock() .expect("Failed to acquire gateway state lock"); @@ -332,6 +333,8 @@ pub async fn remove_gateway( .map_err(|_| WebError::Http(StatusCode::INTERNAL_SERVER_ERROR))?, )?; + info!("Removed gateway {gateway_id} in network {network_id}"); + Ok(ApiResponse { json: Value::Null, status: StatusCode::OK, @@ -343,7 +346,7 @@ pub async fn import_network( State(appstate): State, Json(data): Json, ) -> ApiResult { - info!("Importing network from config file"); + debug!("Importing network from config file"); let (mut network, imported_devices) = parse_wireguard_config(&data.config).map_err(|error| { error!("{error}"); @@ -379,14 +382,17 @@ pub async fn import_network( appstate.send_multiple_wireguard_events(gateway_events); // assign IPs for other existing devices - info!("Assigning IPs in imported network for remaining existing devices"); + debug!("Assigning IPs in imported network for remaining existing devices"); let gateway_events = network .sync_allowed_devices(&mut transaction, Some(&reserved_ips)) .await?; appstate.send_multiple_wireguard_events(gateway_events); + debug!("Assigned IPs in imported network for remaining existing devices"); transaction.commit().await?; + info!("Imported network {network} with {} devices", devices.len()); + Ok(ApiResponse { json: json!(ImportedNetworkData { network, devices }), status: StatusCode::CREATED, @@ -405,8 +411,14 @@ pub async fn add_user_devices( let user = session.user; let device_count = mapped_devices.len(); + debug!( + "User {} mapping {device_count} devices for network {network_id}", + user.username, + ); + // finish early if no devices were provided in request if mapped_devices.is_empty() { + debug!("No devices provided in request, skipping mapping"); return Ok(ApiResponse { json: json!({}), status: StatusCode::NO_CONTENT, @@ -415,11 +427,6 @@ pub async fn add_user_devices( match WireguardNetwork::find_by_id(&appstate.pool, network_id).await? { Some(network) => { - info!( - "User {} mapping {device_count} devices for network {network_id}", - user.username, - ); - // wrap loop in transaction to abort if a device is invalid let mut transaction = appstate.pool.begin().await?; let events = network @@ -438,9 +445,12 @@ pub async fn add_user_devices( status: StatusCode::CREATED, }) } - None => Err(WebError::ObjectNotFound(format!( - "Network {network_id} not found" - ))), + None => { + error!("Failed to map devices, network {network_id} not found"); + Err(WebError::ObjectNotFound(format!( + "Network {network_id} not found" + ))) + } } } @@ -458,9 +468,20 @@ pub async fn add_device( ); let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; + + // Let admins manage devices for disabled users + if !user.is_active && !session.is_admin { + info!( + "User {} tried to add a device for a disabled user {username}", + session.user.username + ); + + return Err(WebError::Forbidden("User is disabled.".into())); + } + let networks = WireguardNetwork::all(&appstate.pool).await?; if networks.is_empty() { - error!("No network found, can't add device"); + error!("Failed to add device {device_name}, no networks found"); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -471,6 +492,10 @@ pub async fn add_device( // save device let Some(user_id) = user.id else { + error!( + "Failed to add device {device_name}, user {} has no id", + user.username + ); return Err(WebError::ModelError("User has no id".to_string())); }; let mut device = Device::new(add_device.name, add_device.wireguard_pubkey, user_id); @@ -550,7 +575,7 @@ pub async fn modify_device( let networks = WireguardNetwork::all(&appstate.pool).await?; if networks.is_empty() { - error!("No network found can't modify device"); + error!("Failed to update device {device_id}, no networks found"); return Ok(ApiResponse { json: json!({}), status: StatusCode::BAD_REQUEST, @@ -560,6 +585,7 @@ pub async fn modify_device( // check pubkeys for network in &networks { if network.pubkey == data.wireguard_pubkey { + error!("Failed to update device {device_id}, device's pubkey must be different from server's pubkey"); return Ok(ApiResponse { json: json!({"msg": "device's pubkey must be different from server's pubkey"}), status: StatusCode::BAD_REQUEST, @@ -609,6 +635,7 @@ pub async fn get_device( ) -> ApiResult { debug!("Retrieving device with id: {device_id}"); let device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?; + debug!("Retrieved device with id: {device_id}"); Ok(ApiResponse { json: json!(device), status: StatusCode::OK, @@ -633,7 +660,7 @@ pub async fn delete_device( pub async fn list_devices(_role: VpnRole, State(appstate): State) -> ApiResult { debug!("Listing devices"); let devices = Device::all(&appstate.pool).await?; - info!("Listed devices"); + info!("Listed {} devices", devices.len()); Ok(ApiResponse { json: json!(devices), @@ -648,10 +675,15 @@ pub async fn list_user_devices( ) -> ApiResult { // only allow for admin or user themselves if !session.is_admin && session.user.username != username { + warn!( + "User {} tried to list devices for user {username}, but is not an admin", + session.user.username + ); return Err(WebError::Forbidden("Admin access required".into())); }; debug!("Listing devices for user: {username}"); let devices = Device::all_for_username(&appstate.pool, &username).await?; + info!("Listed {} devices for user: {username}", devices.len()); Ok(ApiResponse { json: json!(devices), @@ -664,11 +696,13 @@ pub async fn download_config( State(appstate): State, Path((network_id, device_id)): Path<(i64, i64)>, ) -> Result { + debug!("Creating config for device {device_id} in network {network_id}"); let network = find_network(network_id, &appstate.pool).await?; let device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?; let wireguard_network_device = WireguardNetworkDevice::find(&appstate.pool, device_id, network_id).await?; if let Some(wireguard_network_device) = wireguard_network_device { + info!("Created config for device {}({device_id})", device.name); Ok(device.create_config(&network, &wireguard_network_device)) } else { let device_id = if let Some(id) = device.id { @@ -676,6 +710,10 @@ pub async fn download_config( } else { String::new() }; + error!( + "Failed to create config, no IP address found for device: {}({device_id})", + device.name + ); Err(WebError::ObjectNotFound(format!( "No IP address found for device: {}({device_id})", device.name @@ -688,7 +726,7 @@ pub async fn create_network_token( State(appstate): State, Path(network_id): Path, ) -> ApiResult { - info!("Generating a new token for network ID {network_id}"); + debug!("Generating a new token for network ID {network_id}"); let network = find_network(network_id, &appstate.pool).await?; let token = Claims::new( ClaimsType::Gateway, @@ -698,11 +736,13 @@ pub async fn create_network_token( ) .to_jwt() .map_err(|_| { + error!("Failed to create token for gateway {}", network.name); WebError::Authorization(format!( "Failed to create token for gateway {}", network.name )) })?; + info!("Generated a new token for network ID {network_id}"); Ok(ApiResponse { json: json!({"token": token, "grpc_url": server_config().grpc_url.to_string()}), status: StatusCode::OK, @@ -743,7 +783,7 @@ pub async fn user_stats( Path(network_id): Path, Query(query_from): Query, ) -> ApiResult { - debug!("Displaying WireGuard user stats"); + debug!("Displaying WireGuard user stats for network {network_id}"); let Some(network) = WireguardNetwork::find_by_id(&appstate.pool, network_id).await? else { return Err(WebError::ObjectNotFound(format!( "Requested network ({network_id}) not found", @@ -754,7 +794,7 @@ pub async fn user_stats( let stats = network .user_stats(&appstate.pool, &from, &aggregation) .await?; - debug!("Displayed WireGuard user stats"); + debug!("Displayed WireGuard user stats for network {network_id}"); Ok(ApiResponse { json: json!(stats), @@ -768,7 +808,7 @@ pub async fn network_stats( Path(network_id): Path, Query(query_from): Query, ) -> ApiResult { - debug!("Displaying WireGuard network stats"); + debug!("Displaying WireGuard network stats for network {network_id}"); let Some(network) = WireguardNetwork::find_by_id(&appstate.pool, network_id).await? else { return Err(WebError::ObjectNotFound(format!( "Requested network ({network_id}) not found" @@ -779,7 +819,7 @@ pub async fn network_stats( let stats = network .network_stats(&appstate.pool, &from, &aggregation) .await?; - debug!("Displayed WireGuard network stats"); + debug!("Displayed WireGuard network stats for network {network_id}"); Ok(ApiResponse { json: json!(stats), diff --git a/src/handlers/worker.rs b/src/handlers/worker.rs index 54eeb229eb..86254df911 100644 --- a/src/handlers/worker.rs +++ b/src/handlers/worker.rs @@ -46,6 +46,10 @@ pub async fn create_job( Some(user) => { // only admins should be able to create jobs for other users if user != session.user && !session.is_admin { + warn!( + "User {} cannot schedule jobs for other users", + session.user.username + ); return Err(WebError::Forbidden( "Cannot schedule jobs for other users.".into(), )); @@ -69,10 +73,13 @@ pub async fn create_job( status: StatusCode::CREATED, }) } - None => Err(WebError::ObjectNotFound(format!( - "user {} not found", - job_data.username - ))), + None => { + error!("Failed to create job, user {} not found", job_data.username); + Err(WebError::ObjectNotFound(format!( + "user {} not found", + job_data.username + ))) + } } } @@ -96,8 +103,10 @@ pub async fn list_workers( _admin: AdminRole, Extension(worker_state): Extension>>, ) -> ApiResult { + debug!("Listing workers"); let state = worker_state.lock().unwrap(); let workers = state.list_workers(); + debug!("Listed workers"); Ok(ApiResponse { json: json!(workers), status: StatusCode::OK, @@ -128,21 +137,34 @@ pub async fn job_status( Extension(worker_state): Extension>>, Path(id): Path, ) -> ApiResult { + debug!( + "User {} fetching job status for job {id}", + session.user.username + ); let state = worker_state.lock().unwrap(); let job_response = state.get_job_status(id); if let Some(response) = job_response { // prevent non-admin users from accessing other users' jobs status if !session.is_admin && response.username != session.user.username { + warn!( + "User {} cannot fetch job status for other users' jobs", + session.user.username + ); return Err(WebError::Forbidden( "Cannot fetch job status for other users' jobs.".into(), )); } if response.success { + debug!("Fetched job status for job {id}"); Ok(ApiResponse { json: json!(job_response), status: StatusCode::OK, }) } else { + error!( + "Failed to fetch job status for job {id}: {}", + response.error + ); Ok(ApiResponse { json: json!(JobResponseError { message: response.error.clone() @@ -151,6 +173,7 @@ pub async fn job_status( }) } } else { + debug!("Fetched job status for job {id}"); Ok(ApiResponse { json: json!(job_response), status: StatusCode::OK, diff --git a/src/handlers/yubikey.rs b/src/handlers/yubikey.rs index 66c8a7cb72..aab077d88c 100644 --- a/src/handlers/yubikey.rs +++ b/src/handlers/yubikey.rs @@ -19,9 +19,14 @@ pub async fn delete_yubikey( .id .ok_or(WebError::DbError("Returned user had no ID".into()))?; let Some(yubikey) = YubiKey::find_by_id(&appstate.pool, key_id).await? else { + error!("Yubikey with id {key_id} not found"); return Err(WebError::ObjectNotFound("YubiKey not found".into())); }; if !session.is_admin && yubikey.user_id != user_id { + warn!( + "User {user_id} tried to delete yubikey {key_id} of user {} without being an admin.", + yubikey.user_id + ); return Err(WebError::Forbidden("Not allowed to delete YubiKey".into())); } yubikey.delete(&appstate.pool).await?; @@ -49,10 +54,11 @@ pub async fn rename_yubikey( .ok_or(WebError::DbError("Returned user had no ID".into()))?; debug!("User {} attempts to rename yubikey {}", user_id, key_id); let Some(mut yubikey) = YubiKey::find_by_id(&appstate.pool, key_id).await? else { + error!("Yubikey with id {key_id} not found"); return Err(WebError::ObjectNotFound("YubiKey not found".into())); }; if !session.is_admin && yubikey.user_id != user_id { - info!( + warn!( "User {user_id}, tried to rename yubikey {key_id} of user {} without being an admin.", yubikey.user_id ); diff --git a/src/headers.rs b/src/headers.rs index 8dc6ace92a..e20a816bc0 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -12,9 +12,10 @@ use crate::{ #[must_use] pub fn create_user_agent_parser() -> Arc { + let regexes = include_bytes!("../user_agent_header_regexes.yaml"); Arc::new( UserAgentParser::builder() - .build_from_yaml("user_agent_header_regexes.yaml") + .build_from_bytes(regexes) .expect("Parser creation failed"), ) } diff --git a/src/lib.rs b/src/lib.rs index 85155852d1..275d08d498 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,12 +6,12 @@ use std::{ use anyhow::anyhow; use axum::{ - handler::HandlerWithoutStateExt, http::{Request, StatusCode}, routing::{delete, get, patch, post, put}, serve, Extension, Router, }; +use assets::{index, svg, web_asset}; use handlers::ssh_authorized_keys::{ add_authentication_key, delete_authentication_key, fetch_authentication_keys, }; @@ -30,10 +30,7 @@ use tokio::{ OnceCell, }, }; -use tower_http::{ - services::{ServeDir, ServeFile}, - trace::{DefaultOnResponse, TraceLayer}, -}; +use tower_http::trace::{DefaultOnResponse, TraceLayer}; use tracing::Level; use uaparser::UserAgentParser; @@ -108,6 +105,7 @@ use self::{ }; pub mod appstate; +pub mod assets; pub mod auth; pub mod config; pub mod db; @@ -164,10 +162,15 @@ pub fn build_webapp( user_agent_parser: Arc, failed_logins: Arc>, ) -> Router { - let serve_web_dir = ServeDir::new("web/dist").fallback(ServeFile::new("web/dist/index.html")); - let serve_images = - ServeDir::new("web/src/shared/images/svg").not_found_service(handle_404.into_service()); - let webapp = Router::new().nest( + let webapp: Router = Router::new() + .route("/", get(index)) + .route("/*path", get(index)) + .route("/fonts/*path", get(web_asset)) + .route("/assets/*path", get(web_asset)) + .route("/svg/*path", get(svg)) + .fallback_service(get(handle_404)); + + let webapp = webapp.nest( "/api/v1", Router::new() .route("/health", get(health_check)) @@ -343,8 +346,6 @@ pub fn build_webapp( ); webapp - .nest_service("/svg", serve_images) - .nest_service("/", serve_web_dir) .with_state(AppState::new( pool, webhook_tx, diff --git a/tests/auth.rs b/tests/auth.rs index fe82f16efc..1789441384 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -4,9 +4,12 @@ use std::{str::FromStr, time::SystemTime}; use chrono::NaiveDateTime; use claims::assert_err; +use common::fetch_user_details; use defguard::{ auth::TOTP_CODE_VALIDITY_PERIOD, - db::{models::wallet::keccak256, DbPool, MFAInfo, MFAMethod, Settings, UserDetails, Wallet}, + db::{ + models::wallet::keccak256, DbPool, MFAInfo, MFAMethod, Settings, User, UserDetails, Wallet, + }, handlers::{Auth, AuthCode, AuthResponse, AuthTotp, WalletChallenge}, hex::to_lower_hex, secret::SecretString, @@ -131,6 +134,46 @@ async fn test_login_bruteforce() { } } +#[tokio::test] +async fn test_login_disabled() { + let client = make_client().await; + + let user_auth = Auth::new("hpotter", "pass123"); + let admin_auth = Auth::new("admin", "pass123"); + + let response = client.post("/api/v1/auth").json(&admin_auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + let mut user_details = fetch_user_details(&client, "hpotter").await; + user_details.user.is_active = false; + let response = client + .put("/api/v1/user/hpotter") + .json(&user_details.user) + .send() + .await; + + assert_eq!(response.status(), StatusCode::OK); + + let response = client.post("/api/v1/auth").json(&user_auth).send().await; + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + client.post("/api/v1/auth").json(&admin_auth).send().await; + let mut user_details = fetch_user_details(&client, "hpotter").await; + user_details.user.is_active = true; + let response = client + .put("/api/v1/user/hpotter") + .json(&user_details.user) + .send() + .await; + + assert_eq!(response.status(), StatusCode::OK); + + let response = client.post("/api/v1/auth").json(&user_auth).send().await; + + assert_eq!(response.status(), StatusCode::OK); +} + #[tokio::test] async fn test_cannot_enable_mfa() { let client = make_client().await; @@ -1103,3 +1146,26 @@ async fn test_session_cookie() { let auth_cookie = response.cookies().find(|c| c.name() == SESSION_COOKIE_NAME); assert!(auth_cookie.is_none()); } + +#[tokio::test] +async fn test_all_session_logout() { + let (client, pool) = make_client_with_db().await; + + let auth = Auth::new("hpotter", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Disable the user, effectively logging them out + let user = User::find_by_username(&pool, "hpotter") + .await + .unwrap() + .unwrap(); + + user.logout_all_sessions(&pool).await.unwrap(); + + let response = client.get("/api/v1/me").send().await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + let auth_cookie = response.cookies().find(|c| c.name() == SESSION_COOKIE_NAME); + assert!(auth_cookie.is_none()); +} diff --git a/tests/enrollment.rs b/tests/enrollment.rs index 1561e990b8..ec34283d53 100644 --- a/tests/enrollment.rs +++ b/tests/enrollment.rs @@ -1,5 +1,6 @@ mod common; +use common::fetch_user_details; use defguard::{ db::{models::enrollment::Token, DbPool}, handlers::{AddUserData, Auth}, @@ -84,3 +85,41 @@ async fn test_initialize_enrollment() { assert_eq!(enrollment.admin_id, Some(1)); assert_eq!(enrollment.used_at, None); } + +#[tokio::test] +async fn test_enroll_disabled_user() { + let (client, _) = make_client().await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + let new_user = AddUserData { + username: "adumbledore".into(), + last_name: "Dumbledore".into(), + first_name: "Albus".into(), + email: "a.dumbledore@hogwart.edu.uk".into(), + phone: Some("1234".into()), + password: None, + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + let mut user_details = fetch_user_details(&client, "adumbledore").await; + user_details.user.is_active = false; + let response = client + .put(format!("/api/v1/user/{}", "adumbledore")) + .json(&user_details.user) + .send() + .await; + + assert_eq!(response.status(), StatusCode::OK); + + // enrollment should fail, because user is disabled + let response = client + .post("/api/v1/user/adumbledore/start_enrollment") + .json(&json!({})) + .send() + .await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); +} diff --git a/tests/group.rs b/tests/group.rs index dc62ff9d2a..31040e0a9e 100644 --- a/tests/group.rs +++ b/tests/group.rs @@ -2,6 +2,7 @@ mod common; use defguard::handlers::{Auth, GroupInfo}; use reqwest::StatusCode; +use serde_json::json; use self::common::make_test_client; @@ -15,7 +16,7 @@ async fn test_create_group() { assert_eq!(response.status(), StatusCode::OK); // Create new group. - let data = GroupInfo::new("hogwards", Some(vec!["hpotter".into()])); + let data = GroupInfo::new("hogwards", vec!["hpotter".into()], Vec::new()); let response = client.post("/api/v1/group").json(&data).send().await; assert_eq!(response.status(), StatusCode::CREATED); @@ -42,12 +43,12 @@ async fn test_modify_group() { assert_eq!(response.status(), StatusCode::OK); // Create new group. - let data = GroupInfo::new("hogwards", Some(vec!["hpotter".into()])); + let data = GroupInfo::new("hogwards", vec!["hpotter".into()], Vec::new()); let response = client.post("/api/v1/group").json(&data).send().await; assert_eq!(response.status(), StatusCode::CREATED); // Rename group. - let data = GroupInfo::new("gryffindor", None); + let data = GroupInfo::new("gryffindor", Vec::new(), Vec::new()); let response = client .put("/api/v1/group/hogwards") .json(&data) @@ -76,7 +77,7 @@ async fn test_modify_group_members() { assert_eq!(response.status(), StatusCode::OK); // Create new group. - let data = GroupInfo::new("hogwards", Some(vec!["hpotter".into()])); + let data = GroupInfo::new("hogwards", vec!["hpotter".into()], Vec::new()); let response = client.post("/api/v1/group").json(&data).send().await; assert_eq!(response.status(), StatusCode::CREATED); @@ -84,10 +85,10 @@ async fn test_modify_group_members() { let response = client.get("/api/v1/group/hogwards").send().await; assert_eq!(response.status(), StatusCode::OK); let group_info: GroupInfo = response.json().await; - assert_eq!(group_info.members.unwrap(), vec!["hpotter".to_string()]); + assert_eq!(group_info.members, vec!["hpotter".to_string()]); // Change group members. - let data = GroupInfo::new("hogwards", Some(Vec::new())); + let data = GroupInfo::new("hogwards", Vec::new(), Vec::new()); let response = client .put("/api/v1/group/hogwards") .json(&data) @@ -99,5 +100,51 @@ async fn test_modify_group_members() { let response = client.get("/api/v1/group/hogwards").send().await; assert_eq!(response.status(), StatusCode::OK); let group_info: GroupInfo = response.json().await; - assert!(group_info.members.unwrap().is_empty()); + assert!(group_info.members.is_empty()); +} + +#[tokio::test] +async fn test_modify_group_no_locations_in_request() { + let (client, _) = make_test_client().await; + + // Authorize as an administrator. + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create new group. + let data = json!({ + "name": "hogwards", + "members": [ + "hpotter", + "admin" + ] + }); + let response = client.post("/api/v1/group").json(&data).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // Rename group. + let data = json!({ + "name": "gryffindor", + "members": [ + "hpotter", + ] + }); + let response = client + .put("/api/v1/group/hogwards") + .json(&data) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Try to get the group by its old name. + let response = client.get("/api/v1/group/hogwards").send().await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + // Get group info. + let response = client.get("/api/v1/group/gryffindor").send().await; + assert_eq!(response.status(), StatusCode::OK); + let group_info: GroupInfo = response.json().await; + assert_eq!(group_info.name, "gryffindor"); + assert_eq!(group_info.members, vec!["hpotter"]); } diff --git a/tests/user.rs b/tests/user.rs index aa4e31368a..fb2f4b677c 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -707,3 +707,50 @@ async fn test_user_add_device() { .content .contains("Device type: iPhone, OS: iOS 17.1, Mobile Safari")); } + +#[tokio::test] +async fn test_disable() { + let client = make_client().await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // get yourself + let mut user_details = fetch_user_details(&client, "admin").await; + user_details.user.is_active = false; + + // disable yourself + let response = client + .put("/api/v1/user/admin") + .json(&user_details.user) + .send() + .await; + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // create user + let new_user = AddUserData { + username: "adumbledore".into(), + last_name: "Dumbledore".into(), + first_name: "Albus".into(), + email: "a.dumbledore@hogwart.edu.uk".into(), + phone: Some("1234".into()), + password: Some("Password1234543$!".into()), + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // get user + let mut user_details = fetch_user_details(&client, "adumbledore").await; + assert_eq!(user_details.user.first_name, "Albus"); + + // disable user + user_details.user.is_active = false; + let response = client + .put("/api/v1/user/adumbledore") + .json(&user_details.user) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); +} diff --git a/tests/wireguard.rs b/tests/wireguard.rs index 59446a48a0..5da65cf49a 100644 --- a/tests/wireguard.rs +++ b/tests/wireguard.rs @@ -8,7 +8,7 @@ use defguard::{ }, Device, GatewayEvent, WireguardNetwork, }, - handlers::{wireguard::WireguardNetworkData, Auth}, + handlers::{wireguard::WireguardNetworkData, Auth, GroupInfo}, }; use matches::assert_matches; use reqwest::StatusCode; @@ -53,6 +53,11 @@ async fn test_network() { let event = wg_rx.try_recv().unwrap(); assert_matches!(event, GatewayEvent::NetworkCreated(..)); + // check vpn locations for `admin` group + let response = client.get("/api/v1/group/admin").send().await; + let group_info: GroupInfo = response.json().await; + assert!(group_info.vpn_locations.is_empty()); + // modify network let network_data = WireguardNetworkData { name: "my network".into(), @@ -61,7 +66,7 @@ async fn test_network() { port: 55555, allowed_ips: Some("10.1.1.0/24".into()), dns: None, - allowed_groups: vec![], + allowed_groups: vec!["admin".into()], mfa_enabled: false, keepalive_interval: DEFAULT_KEEPALIVE_INTERVAL, peer_disconnect_threshold: DEFAULT_DISCONNECT_THRESHOLD, @@ -75,6 +80,12 @@ async fn test_network() { let event = wg_rx.try_recv().unwrap(); assert_matches!(event, GatewayEvent::NetworkModified(..)); + // check vpn locations for `admin` group + let response = client.get("/api/v1/group/admin").send().await; + assert_eq!(response.status(), StatusCode::OK); + let group_info: GroupInfo = response.json().await; + assert_eq!(group_info.vpn_locations, vec!["my network"]); + // list networks let response = client.get("/api/v1/network").send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/tests/wireguard_network_allowed_groups.rs b/tests/wireguard_network_allowed_groups.rs index f387d81040..484e612a48 100644 --- a/tests/wireguard_network_allowed_groups.rs +++ b/tests/wireguard_network_allowed_groups.rs @@ -577,3 +577,55 @@ async fn test_modify_user() { assert_eq!(peers[0].pubkey, devices[0].wireguard_pubkey); assert_eq!(peers[1].pubkey, devices[3].wireguard_pubkey); } + +#[tokio::test] +async fn test_delete_only_allowed_group() { + let (client, client_state) = make_test_client().await; + let (_users, devices) = setup_test_users(&client_state.pool).await; + + let mut wg_rx = client_state.wireguard_rx; + + let auth = Auth::new("admin", "pass123"); + let response = &client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // create network with an allowed group + let response = client + .post("/api/v1/network") + .json(&json!({ + "name": "network", + "address": "10.1.1.1/24", + "port": 55555, + "endpoint": "192.168.4.14", + "allowed_ips": "10.1.1.0/24", + "dns": "1.1.1.1", + "allowed_groups": ["allowed group"], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 180 + })) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let network: WireguardNetwork = response.json().await; + assert_eq!(network.name, "network"); + let event = wg_rx.try_recv().unwrap(); + assert_matches!(event, GatewayEvent::NetworkCreated(..)); + + let peers = network.get_peers(&client_state.pool).await.unwrap(); + assert_eq!(peers.len(), 2); + assert_eq!(peers[0].pubkey, devices[0].wireguard_pubkey); + assert_eq!(peers[1].pubkey, devices[1].wireguard_pubkey); + + // remove an allowed group + let response = client.delete("/api/v1/group/allowed%20group").send().await; + assert_eq!(response.status(), StatusCode::OK); + + // network configuration was created for all devices + let peers = network.get_peers(&client_state.pool).await.unwrap(); + assert_eq!(peers.len(), 4); + assert_eq!(peers[0].pubkey, devices[0].wireguard_pubkey); + assert_eq!(peers[1].pubkey, devices[1].wireguard_pubkey); + assert_eq!(peers[2].pubkey, devices[2].wireguard_pubkey); + assert_eq!(peers[3].pubkey, devices[3].wireguard_pubkey); +} diff --git a/web/src/components/Navigation/components/NavigationBar/NavigationBar.tsx b/web/src/components/Navigation/components/NavigationBar/NavigationBar.tsx index 462c868de9..da92d62a41 100644 --- a/web/src/components/Navigation/components/NavigationBar/NavigationBar.tsx +++ b/web/src/components/Navigation/components/NavigationBar/NavigationBar.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { useMemo } from 'react'; import { useI18nContext } from '../../../../i18n/i18n-react'; -import SvgDefguadNavLogoCollapsed from '../../../../shared/components/svg/DefguadNavLogoCollapsed'; +import SvgDefguardNavLogoCollapsed from '../../../../shared/components/svg/DefguardNavLogoCollapsed'; import SvgIconNavLogout from '../../../../shared/components/svg/IconNavLogout'; import { useAppStore } from '../../../../shared/hooks/store/useAppStore'; import { NavigationItems } from '../../types'; @@ -33,7 +33,7 @@ export const NavigationBar = ({ navItems, onLogout, isOpen }: Props) => {