From c98656e3fed9dc4a584522b6a3ec450c651f085f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 24 Sep 2025 11:13:15 +0200 Subject: [PATCH 1/2] Release/1.5.1 (#1611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix password reset grpc sending unparsed user agent (#1546) Co-authored-by: Filip Ślęzak * Fixes pentest issue DG25-10 from 2025-09-02 (#1579) * validate phone number during enrollment * also check phone numbers in core API endpoints * Do not display sensitive data from protos (#1580) * Don't send empty strings when phone number is not provided (#1583) * don't send empty strings when phone number is not providecleand * use zod trim() instead of trimObjectStrings helper * Fixes pentest issue DG25-17 from 2025-09-02 (#1581) * fix open redirect pentest issue * add tests and handling of get requests, allow redirects if url is allowed for the client * compare the whole url, not just domain * cargo clippy fixes * wip fix openid flow tests * fix panic in the contains_redirect_url method * cleanup eprintln statements * bring back the other openid flow test * state-based fallback url in openid test * ensure openid client names don't contain HTML (#1587) * ensure login responses don't allow login enumeration (#1588) * Fixes pentest issue DG25-24 from 2025-09-02 (#1585) * put mail handler into a separate crate (#1590) * put random & secret modules into a common crate * move DB setup code to common crate * move version to common crate * move id types to common crate * move AuthCode model into common crate * move auth key model * move biometric auth model * move device login model * remove unnecessary feature flags * move global value macro * move model error * move server config * move hex module * move protos to a separate crate * put mailer into a separate crate * update query data * remove commented out code * add new crates * update flake inputs * move AsCsv trait * fix failing test * move claims struct * Cleanup and revive OpenID login test (#1591) * use default subject as fallback (#1593) * Fixes pentest issue DG25-25 and DG25-20 from 2025-09-02 (#1574) * Fixes pentest issue DG25-32 from 2025-09-02 (#1597) * custom Debug implementation for Settings struct to avoid exposing license key in logs * cargo update * fix document links (#1599) * fix links in readme * fix frontend links * bump version to 1.5.1 * sanitize branch name for docker cache * don't log settings during partial update * cargo fmt --------- Co-authored-by: Aleksander <170264518+t-aleksander@users.noreply.github.com> Co-authored-by: Maciej Wójcik Co-authored-by: Maciek <19913370+wojcik91@users.noreply.github.com> Co-authored-by: Filip Ślęzak Co-authored-by: Adam --- .github/workflows/build-docker.yml | 7 +- ...b7d7faff6d6327e279362212cda4960caca0.json} | 4 +- ...35ad0e24e4855aa42cdceab2712f8c8dbef4.json} | 4 +- ...6a96461ecb5edf023ab88d71f9887fc0f2f2.json} | 6 +- ...dde95cf576aee8744697f1acf957e3fdd1552.json | 59 ++ Cargo.lock | 574 ++++++++++++---- Cargo.toml | 13 +- README.md | 54 +- crates/defguard/Cargo.toml | 2 + crates/defguard/src/main.rs | 22 +- crates/defguard_common/Cargo.toml | 39 ++ crates/defguard_common/build.rs | 10 + crates/defguard_common/src/auth/claims.rs | 98 +++ crates/defguard_common/src/auth/mod.rs | 1 + .../src/config.rs | 13 +- crates/defguard_common/src/csv.rs | 17 + crates/defguard_common/src/db/mod.rs | 47 ++ .../src/db/models/auth_code.rs | 43 +- .../src/db/models/authentication_key.rs | 9 +- .../src/db/models/biometric_auth.rs | 15 +- .../src/db/models/device_login.rs | 6 +- .../src/db/models/error.rs | 0 crates/defguard_common/src/db/models/mod.rs | 15 + .../src/db/models/settings.rs | 126 +++- crates/defguard_common/src/db/models/user.rs | 30 + .../src/globals.rs | 0 .../src/hex.rs | 0 crates/defguard_common/src/lib.rs | 11 + .../src/random.rs | 4 +- .../src/secret.rs | 0 crates/defguard_core/Cargo.toml | 31 +- crates/defguard_core/build.rs | 27 +- crates/defguard_core/src/appstate.rs | 4 +- crates/defguard_core/src/auth/mod.rs | 178 +---- crates/defguard_core/src/db/mod.rs | 42 +- .../src/db/models/activity_log/metadata.rs | 30 +- .../src/db/models/activity_log/mod.rs | 2 +- crates/defguard_core/src/db/models/device.rs | 17 +- .../defguard_core/src/db/models/enrollment.rs | 52 +- crates/defguard_core/src/db/models/group.rs | 6 +- crates/defguard_core/src/db/models/mod.rs | 27 +- .../src/db/models/oauth2authorizedapp.rs | 2 +- .../src/db/models/oauth2client.rs | 64 +- .../src/db/models/oauth2token.rs | 3 +- .../src/db/models/polling_token.rs | 6 +- crates/defguard_core/src/db/models/session.rs | 12 +- crates/defguard_core/src/db/models/user.rs | 97 +-- .../defguard_core/src/db/models/webauthn.rs | 3 +- crates/defguard_core/src/db/models/webhook.rs | 2 +- .../defguard_core/src/db/models/wireguard.rs | 23 +- .../src/db/models/wireguard_peer_stats.rs | 2 +- crates/defguard_core/src/db/models/yubikey.rs | 2 +- .../activity_log_stream/http_stream.rs | 8 +- .../src/enterprise/db/models/acl.rs | 6 +- .../src/enterprise/db/models/acl/tests.rs | 3 +- .../db/models/activity_log_stream.rs | 4 +- .../src/enterprise/db/models/api_tokens.rs | 2 +- .../enterprise/db/models/openid_provider.rs | 2 +- .../src/enterprise/db/models/snat.rs | 6 +- .../src/enterprise/directory_sync/mod.rs | 3 +- .../src/enterprise/directory_sync/tests.rs | 17 +- .../src/enterprise/firewall/mod.rs | 13 +- .../src/enterprise/firewall/tests.rs | 12 +- .../src/enterprise/grpc/desktop_client_mfa.rs | 8 +- .../src/enterprise/grpc/polling.rs | 9 +- .../src/enterprise/handlers/acl.rs | 2 +- .../handlers/activity_log_stream.rs | 2 +- .../src/enterprise/handlers/api_tokens.rs | 2 +- .../src/enterprise/handlers/openid_login.rs | 171 ++--- .../enterprise/handlers/openid_providers.rs | 57 +- .../src/enterprise/ldap/client.rs | 6 +- .../defguard_core/src/enterprise/ldap/hash.rs | 3 +- .../defguard_core/src/enterprise/ldap/mod.rs | 29 +- .../src/enterprise/ldap/model.rs | 7 +- .../defguard_core/src/enterprise/ldap/sync.rs | 30 +- .../src/enterprise/ldap/tests.rs | 7 +- .../src/enterprise/ldap/utils.rs | 3 +- .../defguard_core/src/enterprise/license.rs | 10 +- crates/defguard_core/src/enterprise/limits.rs | 3 +- .../src/enterprise/snat/handlers.rs | 3 +- crates/defguard_core/src/error.rs | 10 +- crates/defguard_core/src/events.rs | 28 +- crates/defguard_core/src/grpc/auth.rs | 9 +- crates/defguard_core/src/grpc/client_mfa.rs | 41 +- crates/defguard_core/src/grpc/enrollment.rs | 239 +++++-- .../src/grpc/gateway/client_state.rs | 3 +- crates/defguard_core/src/grpc/gateway/map.rs | 3 +- crates/defguard_core/src/grpc/gateway/mod.rs | 17 +- .../defguard_core/src/grpc/gateway/state.rs | 4 +- crates/defguard_core/src/grpc/interceptor.rs | 10 +- crates/defguard_core/src/grpc/mod.rs | 91 +-- .../defguard_core/src/grpc/password_reset.rs | 32 +- crates/defguard_core/src/grpc/utils.rs | 20 +- crates/defguard_core/src/grpc/worker.rs | 12 +- .../src/handlers/activity_log.rs | 7 +- crates/defguard_core/src/handlers/app_info.rs | 4 +- crates/defguard_core/src/handlers/auth.rs | 32 +- crates/defguard_core/src/handlers/group.rs | 3 +- crates/defguard_core/src/handlers/mail.rs | 33 +- crates/defguard_core/src/handlers/mod.rs | 10 +- .../src/handlers/network_devices.rs | 6 +- .../src/handlers/openid_clients.rs | 20 + .../defguard_core/src/handlers/openid_flow.rs | 231 +++++-- crates/defguard_core/src/handlers/settings.rs | 24 +- .../src/handlers/ssh_authorized_keys.rs | 9 +- crates/defguard_core/src/handlers/user.rs | 44 +- .../defguard_core/src/handlers/wireguard.rs | 6 +- crates/defguard_core/src/handlers/worker.rs | 3 +- crates/defguard_core/src/headers.rs | 17 +- crates/defguard_core/src/lib.rs | 96 +-- crates/defguard_core/src/support.rs | 7 +- crates/defguard_core/src/updates.rs | 8 +- crates/defguard_core/src/utility_thread.rs | 3 +- crates/defguard_core/src/version.rs | 4 +- crates/defguard_core/src/wg_config.rs | 2 +- .../src/wireguard_peer_disconnect.rs | 4 +- .../tests/integration/api/acl.rs | 11 +- .../tests/integration/api/api_tokens.rs | 4 +- .../tests/integration/api/auth.rs | 72 +- .../tests/integration/api/common/client.rs | 2 - .../tests/integration/api/common/mod.rs | 32 +- .../tests/integration/api/forward_auth.rs | 3 +- .../tests/integration/api/mod.rs | 2 + .../tests/integration/api/oauth.rs | 3 +- .../tests/integration/api/openid.rs | 645 ++++++++++++++++-- .../tests/integration/api/openid_login.rs | 324 ++++----- .../tests/integration/api/settings.rs | 10 +- .../tests/integration/api/snat.rs | 2 +- .../tests/integration/api/user.rs | 10 +- .../tests/integration/api/webhook.rs | 6 +- .../tests/integration/api/wireguard.rs | 4 +- .../api/wireguard_network_allowed_groups.rs | 4 +- .../api/wireguard_network_devices.rs | 3 +- .../api/wireguard_network_stats.rs | 16 +- .../tests/integration/api/worker.rs | 6 +- .../defguard_core/tests/integration/common.rs | 7 +- .../integration/grpc/common/mock_gateway.rs | 10 +- .../tests/integration/grpc/common/mod.rs | 5 +- .../tests/integration/grpc/gateway.rs | 17 +- crates/defguard_event_logger/Cargo.toml | 17 +- crates/defguard_event_logger/src/lib.rs | 38 +- crates/defguard_event_logger/src/message.rs | 8 +- crates/defguard_event_router/Cargo.toml | 1 + crates/defguard_event_router/src/lib.rs | 2 +- crates/defguard_mail/Cargo.toml | 26 + .../src/mail.rs => defguard_mail/src/lib.rs} | 4 +- .../src/templates.rs | 63 +- .../templates/base.tera | 2 +- .../templates/macros.tera | 0 .../templates/mail_desktop_start.tera | 0 .../templates/mail_email_mfa_activation.tera | 0 .../templates/mail_email_mfa_code.tera | 0 .../mail_enrollment_admin_notification.tera | 0 .../templates/mail_enrollment_start.tera | 0 .../templates/mail_enrollment_welcome.tera | 0 .../templates/mail_gateway_disconnected.tera | 0 .../templates/mail_gateway_reconnected.tera | 0 .../templates/mail_mfa_configured.tera | 0 .../templates/mail_new_device_added.tera | 0 .../templates/mail_new_device_login.tera | 0 .../templates/mail_new_device_ocid_login.tera | 0 .../templates/mail_password_reset_start.tera | 0 .../mail_password_reset_success.tera | 0 .../templates/mail_support_data.tera | 0 .../templates/mail_test.tera | 0 crates/defguard_proto/Cargo.toml | 17 + crates/defguard_proto/build.rs | 37 + crates/defguard_proto/src/lib.rs | 66 ++ deny.toml | 9 + flake.lock | 12 +- web/src/i18n/en/index.ts | 12 +- web/src/i18n/i18n-types.ts | 24 +- web/src/i18n/ko/index.ts | 6 +- web/src/i18n/pl/index.ts | 10 +- .../ProfileDetailsForm/ProfileDetailsForm.tsx | 7 +- .../components/AddUserForm/AddUserForm.tsx | 23 +- 176 files changed, 3297 insertions(+), 1666 deletions(-) rename .sqlx/{query-796641fb2d79872ae5de64c8b9181e5890a3a122ea5e37ad2fa216c9db47f0a3.json => query-04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0.json} (96%) rename .sqlx/{query-321ce9355db39c3d93d8b915ba7888357f5403c2e9e97fa17e502223bbcc918a.json => query-27f3c5ea4443993df10ea337195635ad0e24e4855aa42cdceab2712f8c8dbef4.json} (82%) rename .sqlx/{query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json => query-4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2.json} (94%) create mode 100644 .sqlx/query-ef66255ab254e8d0981226b2e9fdde95cf576aee8744697f1acf957e3fdd1552.json create mode 100644 crates/defguard_common/Cargo.toml create mode 100644 crates/defguard_common/build.rs create mode 100644 crates/defguard_common/src/auth/claims.rs create mode 100644 crates/defguard_common/src/auth/mod.rs rename crates/{defguard_core => defguard_common}/src/config.rs (97%) create mode 100644 crates/defguard_common/src/csv.rs create mode 100644 crates/defguard_common/src/db/mod.rs rename crates/{defguard_core => defguard_common}/src/db/models/auth_code.rs (50%) rename crates/{defguard_core => defguard_common}/src/db/models/authentication_key.rs (94%) rename crates/{defguard_core => defguard_common}/src/db/models/biometric_auth.rs (95%) rename crates/{defguard_core => defguard_common}/src/db/models/device_login.rs (93%) rename crates/{defguard_core => defguard_common}/src/db/models/error.rs (100%) create mode 100644 crates/defguard_common/src/db/models/mod.rs rename crates/{defguard_core => defguard_common}/src/db/models/settings.rs (75%) create mode 100644 crates/defguard_common/src/db/models/user.rs rename crates/{defguard_core => defguard_common}/src/globals.rs (100%) rename crates/{defguard_core => defguard_common}/src/hex.rs (100%) create mode 100644 crates/defguard_common/src/lib.rs rename crates/{defguard_core => defguard_common}/src/random.rs (77%) rename crates/{defguard_core => defguard_common}/src/secret.rs (100%) create mode 100644 crates/defguard_mail/Cargo.toml rename crates/{defguard_core/src/mail.rs => defguard_mail/src/lib.rs} (98%) rename crates/{defguard_core => defguard_mail}/src/templates.rs (93%) rename crates/{defguard_core => defguard_mail}/templates/base.tera (99%) rename crates/{defguard_core => defguard_mail}/templates/macros.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_desktop_start.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_email_mfa_activation.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_email_mfa_code.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_enrollment_admin_notification.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_enrollment_start.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_enrollment_welcome.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_gateway_disconnected.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_gateway_reconnected.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_mfa_configured.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_new_device_added.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_new_device_login.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_new_device_ocid_login.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_password_reset_start.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_password_reset_success.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_support_data.tera (100%) rename crates/{defguard_core => defguard_mail}/templates/mail_test.tera (100%) create mode 100644 crates/defguard_proto/Cargo.toml create mode 100644 crates/defguard_proto/build.rs create mode 100644 crates/defguard_proto/src/lib.rs diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index c17f985182..f25226c1b7 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -55,6 +55,9 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Sanitize branch name + run: echo "SAFE_REF=${GITHUB_REF_NAME//\//-}" >> $GITHUB_ENV + - name: Build container uses: docker/build-push-action@v6 with: @@ -65,8 +68,8 @@ jobs: tags: "${{ env.GHCR_REPO }}:${{ github.sha }}-${{ matrix.tag }}" cache-from: | type=registry,ref=${{ env.GHCR_REPO }}:cache-${{ matrix.tag }} - type=registry,ref=${{ env.GHCR_REPO }}:cache-${{ matrix.tag }}-${{ github.ref_name }} - cache-to: type=registry,mode=max,ref=${{ env.GHCR_REPO }}:cache-${{ matrix.tag }}-${{ github.ref_name }} + type=registry,ref=${{ env.GHCR_REPO }}:cache-${{ matrix.tag }}-${{ env.SAFE_REF }} + cache-to: type=registry,mode=max,ref=${{ env.GHCR_REPO }}:cache-${{ matrix.tag }}-${{ env.SAFE_REF }} - name: Scan image with Trivy uses: aquasecurity/trivy-action@0.32.0 diff --git a/.sqlx/query-796641fb2d79872ae5de64c8b9181e5890a3a122ea5e37ad2fa216c9db47f0a3.json b/.sqlx/query-04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0.json similarity index 96% rename from .sqlx/query-796641fb2d79872ae5de64c8b9181e5890a3a122ea5e37ad2fa216c9db47f0a3.json rename to .sqlx/query-04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0.json index 65b55605ac..7396561c7f 100644 --- a/.sqlx/query-796641fb2d79872ae5de64c8b9181e5890a3a122ea5e37ad2fa216c9db47f0a3.json +++ b/.sqlx/query-04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT 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, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE openid_sub = $1 LIMIT 1", + "query": "SELECT 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, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE openid_sub = $1", "describe": { "columns": [ { @@ -144,5 +144,5 @@ true ] }, - "hash": "796641fb2d79872ae5de64c8b9181e5890a3a122ea5e37ad2fa216c9db47f0a3" + "hash": "04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0" } diff --git a/.sqlx/query-321ce9355db39c3d93d8b915ba7888357f5403c2e9e97fa17e502223bbcc918a.json b/.sqlx/query-27f3c5ea4443993df10ea337195635ad0e24e4855aa42cdceab2712f8c8dbef4.json similarity index 82% rename from .sqlx/query-321ce9355db39c3d93d8b915ba7888357f5403c2e9e97fa17e502223bbcc918a.json rename to .sqlx/query-27f3c5ea4443993df10ea337195635ad0e24e4855aa42cdceab2712f8c8dbef4.json index c4bb584637..00ccd67e94 100644 --- a/.sqlx/query-321ce9355db39c3d93d8b915ba7888357f5403c2e9e97fa17e502223bbcc918a.json +++ b/.sqlx/query-27f3c5ea4443993df10ea337195635ad0e24e4855aa42cdceab2712f8c8dbef4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, user_id, client_id, code, redirect_uri, scope, auth_time, nonce, code_challenge FROM authorization_code WHERE code = $1", + "query": "DELETE FROM authorization_code WHERE code = $1 RETURNING id, user_id, client_id, code, redirect_uri, scope, auth_time, nonce, code_challenge", "describe": { "columns": [ { @@ -66,5 +66,5 @@ true ] }, - "hash": "321ce9355db39c3d93d8b915ba7888357f5403c2e9e97fa17e502223bbcc918a" + "hash": "27f3c5ea4443993df10ea337195635ad0e24e4855aa42cdceab2712f8c8dbef4" } diff --git a/.sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json b/.sqlx/query-4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2.json similarity index 94% rename from .sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json rename to .sqlx/query-4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2.json index 9f60163eac..fa1068e52b 100644 --- a/.sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json +++ b/.sqlx/query-4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: SyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" FROM \"settings\" WHERE id = 1", + "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" FROM \"settings\" WHERE id = 1", "describe": { "columns": [ { @@ -206,7 +206,7 @@ }, { "ordinal": 38, - "name": "ldap_sync_status: SyncStatus", + "name": "ldap_sync_status: LdapSyncStatus", "type_info": { "Custom": { "name": "ldap_sync_status", @@ -330,5 +330,5 @@ false ] }, - "hash": "7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10" + "hash": "4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2" } diff --git a/.sqlx/query-ef66255ab254e8d0981226b2e9fdde95cf576aee8744697f1acf957e3fdd1552.json b/.sqlx/query-ef66255ab254e8d0981226b2e9fdde95cf576aee8744697f1acf957e3fdd1552.json new file mode 100644 index 0000000000..8b4597bc46 --- /dev/null +++ b/.sqlx/query-ef66255ab254e8d0981226b2e9fdde95cf576aee8744697f1acf957e3fdd1552.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT c.id, c.client_id, c.client_secret, c.redirect_uri, c.scope, c.name, c.enabled FROM oauth2client c JOIN oauth2authorizedapp a ON a.oauth2client_id = c.id JOIN oauth2token t ON t.oauth2authorizedapp_id = a.id WHERE t.access_token = $1 OR t.refresh_token = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "client_id", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "client_secret", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "redirect_uri", + "type_info": "TextArray" + }, + { + "ordinal": 4, + "name": "scope", + "type_info": "TextArray" + }, + { + "ordinal": 5, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "ef66255ab254e8d0981226b2e9fdde95cf576aee8744697f1acf957e3fdd1552" +} diff --git a/Cargo.lock b/Cargo.lock index 57464586f5..0bd268902d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,19 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ammonia" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" +dependencies = [ + "cssparser", + "html5ever", + "maplit", + "tendril", + "url", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -150,9 +163,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" @@ -569,9 +582,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.36" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ "find-msvc-tools", "jobserver", @@ -664,9 +677,9 @@ checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" [[package]] name = "clap" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -674,9 +687,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -921,6 +934,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ctr" version = "0.9.2" @@ -980,8 +1016,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -998,13 +1044,38 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn", ] @@ -1030,9 +1101,11 @@ version = "0.0.0" dependencies = [ "anyhow", "bytes", + "defguard_common", "defguard_core", "defguard_event_logger", "defguard_event_router", + "defguard_mail", "defguard_version", "dotenvy", "secrecy", @@ -1040,10 +1113,41 @@ dependencies = [ "tracing", ] +[[package]] +name = "defguard_common" +version = "1.5.1" +dependencies = [ + "anyhow", + "base64 0.22.1", + "chrono", + "clap", + "ed25519-dalek", + "humantime", + "ipnetwork", + "jsonwebtoken", + "matches", + "model_derive", + "openidconnect", + "rand 0.8.5", + "reqwest", + "rsa", + "secrecy", + "serde", + "sqlx", + "struct-patch", + "thiserror 2.0.16", + "tonic", + "tracing", + "utoipa", + "uuid", + "vergen-git2", +] + [[package]] name = "defguard_core" -version = "1.5.0" +version = "0.0.0" dependencies = [ + "ammonia", "anyhow", "argon2", "axum", @@ -1054,10 +1158,11 @@ dependencies = [ "bytes", "chrono", "claims", - "clap", + "defguard_common", + "defguard_mail", + "defguard_proto", "defguard_version", "defguard_web_ui", - "ed25519-dalek", "humantime", "hyper-util", "ipnetwork", @@ -1073,7 +1178,6 @@ dependencies = [ "paste", "pgp", "prost", - "pulldown-cmark", "rand 0.8.5", "regex", "reqwest", @@ -1112,7 +1216,6 @@ dependencies = [ "utoipa", "utoipa-swagger-ui", "uuid", - "vergen-git2", "webauthn-authenticator-rs", "webauthn-rs", "webauthn-rs-proto", @@ -1125,6 +1228,7 @@ version = "0.0.0" dependencies = [ "bytes", "chrono", + "defguard_common", "defguard_core", "serde_json", "sqlx", @@ -1139,11 +1243,42 @@ version = "0.0.0" dependencies = [ "defguard_core", "defguard_event_logger", + "defguard_mail", + "thiserror 2.0.16", + "tokio", + "tracing", +] + +[[package]] +name = "defguard_mail" +version = "0.0.0" +dependencies = [ + "chrono", + "claims", + "defguard_common", + "lettre", + "pulldown-cmark", + "reqwest", + "serde", + "serde_json", + "sqlx", + "tera", "thiserror 2.0.16", "tokio", "tracing", ] +[[package]] +name = "defguard_proto" +version = "0.0.0" +dependencies = [ + "prost", + "serde", + "tonic", + "tonic-prost", + "tonic-prost-build", +] + [[package]] name = "defguard_version" version = "0.0.0" @@ -1230,7 +1365,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn", @@ -1358,6 +1493,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1538,9 +1688,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "fixedbitset" @@ -1622,6 +1772,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.31" @@ -1765,7 +1925,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.5+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -1845,7 +2005,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.1", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -1885,6 +2045,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "hashlink" version = "0.10.0" @@ -1968,6 +2134,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever", + "match_token", +] + [[package]] name = "http" version = "1.3.1" @@ -2031,9 +2208,9 @@ dependencies = [ [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" @@ -2106,9 +2283,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", @@ -2132,9 +2309,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2305,13 +2482,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.1" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -2401,9 +2579,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.78" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -2561,9 +2739,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", @@ -2641,6 +2819,40 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "matchers" version = "0.2.0" @@ -2760,6 +2972,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -3211,9 +3429,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", "thiserror 2.0.16", @@ -3222,9 +3440,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" dependencies = [ "pest", "pest_generator", @@ -3232,9 +3450,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" dependencies = [ "pest", "pest_meta", @@ -3245,9 +3463,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", "sha2", @@ -3260,7 +3478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.11.1", + "indexmap 2.11.4", ] [[package]] @@ -3337,6 +3555,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ + "phf_macros", "phf_shared", ] @@ -3360,6 +3579,19 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_shared" version = "0.11.3" @@ -3430,12 +3662,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.11.1", + "indexmap 2.11.4", "quick-xml", "serde", "time", @@ -3477,6 +3709,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -3498,9 +3736,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -4024,9 +4262,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "log", "once_cell", @@ -4046,7 +4284,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.4.0", + "security-framework 3.5.0", ] [[package]] @@ -4061,9 +4299,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -4170,9 +4408,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" +checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" dependencies = [ "bitflags 2.9.4", "core-foundation 0.10.1", @@ -4193,19 +4431,21 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" dependencies = [ + "serde_core", "serde_derive", ] @@ -4221,11 +4461,12 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -4238,11 +4479,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -4251,37 +4501,39 @@ dependencies = [ [[package]] name = "serde_html_form" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap 2.11.1", + "indexmap 2.11.4", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -4318,15 +4570,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.1", + "indexmap 2.11.4", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -4338,11 +4590,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn", @@ -4354,7 +4606,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.11.4", "itoa", "ryu", "serde", @@ -4604,7 +4856,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.11.1", + "indexmap 2.11.4", "ipnetwork", "log", "memchr", @@ -4831,6 +5083,31 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -4966,6 +5243,17 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "tera" version = "1.20.0" @@ -5039,11 +5327,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.43" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", + "itoa", "libc", "num-conv", "num_threads", @@ -5145,9 +5434,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ "rustls", "tokio", @@ -5180,18 +5469,31 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.11.4", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ "winnow", ] @@ -5299,7 +5601,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.11.1", + "indexmap 2.11.4", "pin-project-lite", "slab", "sync_wrapper", @@ -5599,6 +5901,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5617,7 +5925,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.11.4", "serde", "serde_json", "utoipa-gen", @@ -5757,18 +6065,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.5+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ "wasip2", ] [[package]] name = "wasip2" -version = "1.0.0+wasi-0.2.4" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] @@ -5781,9 +6089,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", @@ -5794,9 +6102,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", @@ -5808,9 +6116,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.51" +version = "0.4.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" dependencies = [ "cfg-if", "js-sys", @@ -5821,9 +6129,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5831,9 +6139,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", @@ -5844,9 +6152,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] @@ -5866,9 +6174,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.78" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -5884,6 +6192,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "webauthn-attestation-ca" version = "0.5.2" @@ -6015,15 +6335,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -6067,8 +6387,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -6080,6 +6400,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -6089,6 +6418,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -6331,9 +6669,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -6515,7 +6853,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.11.1", + "indexmap 2.11.4", "memchr", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index d6405991da..513ecf7cf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,12 @@ resolver = "2" [workspace.dependencies] # internal crates -defguard_core = { path = "./crates/defguard_core", version = "1.5.0" } +defguard_common = { path = "./crates/defguard_common", version = "1.5.0" } +defguard_core = { path = "./crates/defguard_core", version = "0.0.0" } defguard_event_logger = { path = "./crates/defguard_event_logger", version = "0.0.0" } defguard_event_router = { path = "./crates/defguard_event_router", version = "0.0.0" } +defguard_mail = { path = "./crates/defguard_mail", version = "0.0.0" } +defguard_proto = { path = "./crates/defguard_proto", version = "0.0.0" } defguard_version = { path = "./crates/defguard_version", version = "0.0.0" } defguard_web_ui = { path = "./crates/defguard_web_ui", version = "0.0.0" } model_derive = { path = "./crates/model_derive", version = "0.0.0" } @@ -35,6 +38,7 @@ chrono = { version = "0.4", default-features = false, features = [ "clock", "serde", ] } +claims = "0.8" clap = { version = "4.5", features = ["derive", "env"] } humantime = "2.1" # match version used by sqlx @@ -43,10 +47,15 @@ jsonwebkey = { version = "0.3", features = ["pkcs-convert"] } jsonwebtoken = "9.3" ldap3 = { version = "0.11", default-features = false, features = ["tls"] } lettre = { version = "0.11", features = ["tokio1-native-tls"] } +matches = "0.1" md4 = "0.10" +openidconnect = { version = "4.0", default-features = false, features = [ + "reqwest", +] } parse_link_header = "0.4" paste = "1.0" pgp = { version = "0.16", default-features = false } +prost = "0.14" pulldown-cmark = "0.13" # match version used by sqlx rand = "0.8" @@ -93,6 +102,8 @@ tonic = { version = "0.14", features = [ "tls-ring", ] } tonic-health = "0.14" +tonic-prost = "0.14" +tonic-prost-build = "0.14" totp-lite = { version = "2.0" } tower-http = { version = "0.6", features = ["fs", "trace", "set-header"] } tracing = "0.1" diff --git a/README.md b/README.md index 99e5e6a309..a4e0ce6a48 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,17 @@ ### Defguard provides Comprehensive Access Control (a complete security platform): -- **[WireGuard® VPN with 2FA/MFA](https://docs.defguard.net/admin-and-features/wireguard/multi-factor-authentication-mfa-2fa/architecture)** - not 2FA to "access application" like most solutions - - The only solution with [automatic and real-time synchronization](https://docs.defguard.net/enterprise/automatic-real-time-desktop-client-configuration) for users' desktop client settings (including all VPNs/locations). - - Control users [ability to manage devices and VPN options](https://docs.defguard.net/enterprise/behavior-customization) -- [ACLs/Firewall Management](https://docs.defguard.net/enterprise/all-enteprise-features/access-control-list) for Linux and FreeBSD/OPNSense -- [Integrated SSO based on OpenID Connect](https://docs.defguard.net/admin-and-features/openid-connect): +- **[WireGuard® VPN with 2FA/MFA](https://docs.defguard.net/in-depth/architecture/architecture)** - not 2FA to "access application" like most solutions + - The only solution with [automatic and real-time synchronization](https://docs.defguard.net/features/remote-user-enrollment/automatic-real-time-desktop-client-configuration) for users' desktop client settings (including all VPNs/locations). + - Control users [ability to manage devices and VPN options](https://docs.defguard.net/features/wireguard/behavior-customization) +- [ACLs/Firewall Management](https://docs.defguard.net/features/access-control-list) for Linux and FreeBSD/OPNSense +- [Integrated SSO based on OpenID Connect](https://docs.defguard.net/features/openid-connect): - significant cost saving, simplifying deployment and maintenance - enabling features unavailable to VPN platforms relying upon 3rd party SSO integration -- Already using Google/Microsoft or other OpenID Provider? - [external OpenID provider support](https://docs.defguard.net/enterprise/external-openid-providers) -- [Two way Active Directory/LDAP synchronization](https://docs.defguard.net/enterprise/all-enteprise-features/ldap-and-active-directory-integration/two-way-ldap-and-active-directory-synchronization) -- Only solution with [secure remote user Enrollment & Onboarding](https://docs.defguard.net/help/enrollment) -- Yubico YubiKey Hardware [security key management and provisioning](https://docs.defguard.net/admin-and-features/yubikey-provisioning) +- Already using Google/Microsoft or other OpenID Provider? - [external OpenID provider support](https://docs.defguard.net/features/external-openid-providers) +- [Two way Active Directory/LDAP synchronization](https://docs.defguard.net/features/ldap-and-active-directory-integration/two-way-ldap-and-active-directory-synchronization) +- Only solution with [secure remote user Enrollment & Onboarding](https://docs.defguard.net/using-defguard-for-end-users/enrollment) +- Yubico YubiKey Hardware [security key management and provisioning](https://docs.defguard.net/features/yubikey-provisioning) - Secure and robust architecture, featuring components and micro-services seamlessly deployable in diverse network setups (eg. utilizing network segments like Demilitarized Zones, Intranet with no external access, etc), ensuring a secure environment. - Enterprise ready (multiple Locations/Gateways/Kubernetes deployment, etc..) - Built on WireGuard® protocol which is faster than IPSec, and significantly faster than OpenVPN @@ -67,8 +67,8 @@ Better quality video can [be viewed here](https://github.com/DefGuard/docs/raw/d [Desktop client](https://github.com/DefGuard/client): - **2FA / Multi-Factor Authentication** with TOTP or email based tokens & WireGuard PSK -- [automatic and real-time synchronization](https://docs.defguard.net/enterprise/automatic-real-time-desktop-client-configuration) for users' desktop client settings (including all VPNs/locations). -- Control users [ability to manage devices and VPN options](https://docs.defguard.net/enterprise/behavior-customization) +- [automatic and real-time synchronization](https://docs.defguard.net/features/remote-user-enrollment/automatic-real-time-desktop-client-configuration) for users' desktop client settings (including all VPNs/locations). +- Control users [ability to manage devices and VPN options](https://docs.defguard.net/features/wireguard/behavior-customization) - Defguard instances as well as **any WireGuard tunnel** - just import your tunnels - one client for all WireGuard connections - Secure and remote user enrollment - setting up password, automatically configuring the client for all VPN Locations/Networks - Onboarding - displaying custom onboarding messages, with templates, links ... @@ -79,7 +79,7 @@ Better quality video can [be viewed here](https://github.com/DefGuard/docs/raw/d ## Quick start -The easiest way to run your own defguard instance is to use Docker and our [one-line install script](https://docs.defguard.net/features/setting-up-your-instance/one-line-install). +The easiest way to run your own defguard instance is to use Docker and our [one-line install script](https://docs.defguard.net/getting-started/one-line-install). Just run the command below in your shell and follow the prompts: ```bash @@ -96,7 +96,7 @@ Here is a step-by-step video about this process:

-To learn more about the script and available options please see the [documentation](https://docs.defguard.net/features/setting-up-your-instance/one-line-install). +To learn more about the script and available options please see the [documentation](https://docs.defguard.net/getting-started/one-line-install). ### Setup a VPN server in under 5 minutes !? @@ -104,9 +104,9 @@ Just follow [this tutorial](http://bit.ly/defguard-setup) ## Manual deployment examples -- [Standalone system package based install](https://docs.defguard.net/admin-and-features/setting-up-your-instance/standalone-package-based-installation) -- Using [Docker Compose](https://docs.defguard.net/features/setting-up-your-instance/docker-compose) -- Using [Kubernetes](https://docs.defguard.net/features/setting-up-your-instance/kubernetes) +- [Standalone system package based install](https://docs.defguard.net/deployment-strategies/standalone-package-based-installation) +- Using [Docker Compose](https://docs.defguard.net/deployment-strategies/docker-compose) +- Using [Kubernetes](https://docs.defguard.net/deployment-strategies/kubernetes) ## Roadmap & Development backlog @@ -116,27 +116,23 @@ Just follow [this tutorial](http://bit.ly/defguard-setup) Here is a [dedicated view for **good first bugs**](https://github.com/orgs/DefGuard/projects/5/views/5) -## Why? - -The story and motivation behind defguard [can be found here: https://teonite.com/blog/defguard/](https://teonite.com/blog/defguard/) - ## Features * Remote Access: [WireGuard® VPN](https://www.wireguard.com/) server with: - - [Multi-Factor Authentication](https://docs.defguard.net/help/desktop-client/multi-factor-authentication-mfa-2fa) with TOTP/Email & Pre-Shared Session Keys + - [Multi-Factor Authentication](https://docs.defguard.net/features/wireguard/multi-factor-authentication-mfa-2fa) with TOTP/Email & Pre-Shared Session Keys - multiple VPN Locations (networks/sites) - with defined access (all users or only Admin group) - multiple [Gateways](https://github.com/DefGuard/gateway) for each VPN Location (**high availability/failover**) - supported on a cluster of routers/firewalls for Linux, FreeBSD/PFSense/OPNSense - **import your current WireGuard® server configuration (with a wizard!)** - **most beautiful [Desktop Client!](https://github.com/defguard/client)** (in our opinion ;-)) - automatic IP allocation - - [automatic and real-time synchronization](https://docs.defguard.net/enterprise/automatic-real-time-desktop-client-configuration) for users' desktop client settings (including all VPNs/locations). - - control users [ability to manage devices and VPN options](https://docs.defguard.net/enterprise/behavior-customization) + - [automatic and real-time synchronization](https://docs.defguard.net/features/remote-user-enrollment/automatic-real-time-desktop-client-configuration) for users' desktop client settings (including all VPNs/locations). + - control users [ability to manage devices and VPN options](https://docs.defguard.net/features/wireguard/behavior-customization) - kernel (Linux, FreeBSD/OPNSense/PFSense) & userspace WireGuard® support with [our Rust library](https://github.com/defguard/wireguard-rs) - dashboard and statistics overview of connected users/devices for admins - *defguard is not an official WireGuard® project, and WireGuard is a registered trademark of Jason A. Donenfeld.* * Identity & Account Management: - SSO based on OpenID Connect](https://openid.net/developers/how-connect-works/) - - External SSO: [external OpenID provider support](https://docs.defguard.net/enterprise/external-openid-providers) + - External SSO: [external OpenID provider support](https://docs.defguard.net/features/external-openid-providers) - [Multi-Factor/2FA](https://en.wikipedia.org/wiki/Multi-factor_authentication) Authentication: - [Time-based One-Time Password Algorithm](https://en.wikipedia.org/wiki/Time-based_one-time_password) (TOTP - e.g. Google Authenticator) - WebAuthn / FIDO2 - for hardware key authentication support (eg. YubiKey, FaceID, TouchID, ...) @@ -146,12 +142,12 @@ The story and motivation behind defguard [can be found here: https://teonite.com - nice UI to manage users - Users **self-service** (besides typical data management, users can revoke access to granted apps, MFA, WireGuard®, etc.) * Account Lifecycle Management: - - Secure remote (over the Internet) [user enrollment](https://docs.defguard.net/help/remote-user-enrollment) - on public web / Desktop Client - - User [onboarding after enrollment](https://docs.defguard.net/help/remote-user-enrollment/user-onboarding-after-enrollment) -* SSH & GPG public key management in user profile - with [SSH keys authentication for servers](https://docs.defguard.net/admin-and-features/ssh-authentication) + - Secure remote (over the Internet) [user enrollment](https://docs.defguard.net/features/remote-user-enrollment) - on public web / Desktop Client + - User [onboarding after enrollment](https://docs.defguard.net/features/remote-user-enrollment/user-onboarding-after-enrollment) +* SSH & GPG public key management in user profile - with [SSH keys authentication for servers](https://docs.defguard.net/features/ssh-authentication) * [Yubikey hardware keys](https://www.yubico.com/) provisioning for users by *one click* -* [Email/SMTP support](https://docs.defguard.net/help/setting-up-smtp-for-email-notifications) for notifications, remote enrollment and onboarding -* Easy support with [sending debug/support information](https://docs.defguard.net/help/sending-support-info) +* [Email/SMTP support](https://docs.defguard.net/features/notifications/setting-up-smtp-for-email-notifications) for notifications, remote enrollment and onboarding +* Easy support with [sending debug/support information](https://docs.defguard.net/support-1/troubleshooting/sending-support-info) * Webhooks & REST API * Built with [Rust](https://www.rust-lang.org/) for portability, security, and speed * [UI Library](https://github.com/defguard/ui) - our beautiful React/TypeScript UI is a collection of React components: diff --git a/crates/defguard/Cargo.toml b/crates/defguard/Cargo.toml index a9837db037..8399bb0ae5 100644 --- a/crates/defguard/Cargo.toml +++ b/crates/defguard/Cargo.toml @@ -9,9 +9,11 @@ rust-version.workspace = true [dependencies] # internal crates +defguard_common = { workspace = true } defguard_core = { workspace = true } defguard_event_router = { workspace = true } defguard_event_logger = { workspace = true } +defguard_mail = { workspace = true } defguard_version = { workspace = true } # external dependencies diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 84a176a73d..3c7576a2ee 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -4,14 +4,17 @@ use std::{ }; use bytes::Bytes; -use defguard_core::{ - SERVER_CONFIG, VERSION, - auth::failed_login::FailedLoginMap, - config::{Command, DefGuardConfig}, +use defguard_common::{ + VERSION, + config::{Command, DefGuardConfig, SERVER_CONFIG}, db::{ - AppEvent, GatewayEvent, Settings, User, init_db, - models::settings::initialize_current_settings, + init_db, + models::{Settings, settings::initialize_current_settings}, }, +}; +use defguard_core::{ + auth::failed_login::FailedLoginMap, + db::{AppEvent, GatewayEvent, User}, enterprise::{ activity_log_stream::activity_log_stream_manager::run_activity_log_stream_manager, license::{License, run_periodic_license_check, set_cached_license}, @@ -23,9 +26,7 @@ use defguard_core::{ gateway::{client_state::ClientMap, map::GatewayMap}, run_grpc_bidi_stream, run_grpc_server, }, - init_dev_env, init_vpn_location, - mail::{Mail, run_mail_handler}, - run_web_server, + init_dev_env, init_vpn_location, run_web_server, utility_thread::run_utility_thread, version::IncompatibleComponents, wireguard_peer_disconnect::run_periodic_peer_disconnect, @@ -33,6 +34,7 @@ use defguard_core::{ }; use defguard_event_logger::{message::EventLoggerMessage, run_event_logger}; use defguard_event_router::{RouterReceiverSet, run_event_router}; +use defguard_mail::{Mail, run_mail_handler}; use secrecy::ExposeSecret; use tokio::sync::{broadcast, mpsc::unbounded_channel}; @@ -109,7 +111,7 @@ async fn main() -> Result<(), anyhow::Error> { let gateway_state = Arc::new(Mutex::new(GatewayMap::new())); let client_state = Arc::new(Mutex::new(ClientMap::new())); - let incompatible_components: Arc> = Default::default(); + let incompatible_components: Arc> = Arc::default(); // initialize admin user User::init_admin_user(&pool, config.default_admin_password.expose_secret()).await?; diff --git a/crates/defguard_common/Cargo.toml b/crates/defguard_common/Cargo.toml new file mode 100644 index 0000000000..3ab2e155a1 --- /dev/null +++ b/crates/defguard_common/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "defguard_common" +version = "1.5.1" +edition.workspace = true +license-file.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +model_derive.workspace = true + +anyhow.workspace = true +base64.workspace = true +chrono.workspace = true +clap.workspace = true +ed25519-dalek = { version = "2.2", features = ["rand_core"] } +humantime.workspace = true +ipnetwork.workspace = true +jsonwebtoken.workspace = true +openidconnect.workspace = true +rand.workspace = true +reqwest.workspace = true +rsa.workspace = true +secrecy.workspace = true +serde.workspace = true +sqlx.workspace = true +struct-patch.workspace = true +thiserror.workspace = true +tonic.workspace = true +tracing.workspace = true +utoipa.workspace = true +uuid.workspace = true + +[dev-dependencies] +matches.workspace = true + +[build-dependencies] +vergen-git2 = { version = "1.0", features = ["build"] } diff --git a/crates/defguard_common/build.rs b/crates/defguard_common/build.rs new file mode 100644 index 0000000000..d1e71d8bbe --- /dev/null +++ b/crates/defguard_common/build.rs @@ -0,0 +1,10 @@ +use vergen_git2::{Emitter, Git2Builder}; + +fn main() -> Result<(), Box> { + // set VERGEN_GIT_SHA env variable based on git commit hash + let git2 = Git2Builder::default().branch(true).sha(true).build()?; + Emitter::default().add_instructions(&git2)?.emit()?; + + println!("cargo:rerun-if-changed=../../migrations"); + Ok(()) +} diff --git a/crates/defguard_common/src/auth/claims.rs b/crates/defguard_common/src/auth/claims.rs new file mode 100644 index 0000000000..bca84e18e7 --- /dev/null +++ b/crates/defguard_common/src/auth/claims.rs @@ -0,0 +1,98 @@ +use std::{ + env, + time::{Duration, SystemTime}, +}; + +use jsonwebtoken::{ + DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::Error as JWTError, +}; +use serde::{Deserialize, Serialize}; + +pub static JWT_ISSUER: &str = "DefGuard"; +pub static AUTH_SECRET_ENV: &str = "DEFGUARD_AUTH_SECRET"; +pub static GATEWAY_SECRET_ENV: &str = "DEFGUARD_GATEWAY_SECRET"; +pub static YUBIBRIDGE_SECRET_ENV: &str = "DEFGUARD_YUBIBRIDGE_SECRET"; + +#[derive(Clone, Copy, Default)] +pub enum ClaimsType { + #[default] + Auth, + Gateway, + YubiBridge, + DesktopClient, +} + +/// Standard claims: https://www.iana.org/assignments/jwt/jwt.xhtml +#[derive(Deserialize, Serialize)] +pub struct Claims { + #[serde(skip_serializing, skip_deserializing)] + secret: String, + // issuer + pub iss: String, + // subject + pub sub: String, + // client identifier + pub client_id: String, + // expiration time + pub exp: u64, + // not before + pub nbf: u64, +} + +impl Claims { + #[must_use] + pub fn new(claims_type: ClaimsType, sub: String, client_id: String, duration: u64) -> Self { + let now = SystemTime::now(); + let exp = now + .checked_add(Duration::from_secs(duration)) + .expect("valid time") + .duration_since(SystemTime::UNIX_EPOCH) + .expect("valid timestamp") + .as_secs(); + let nbf = now + .duration_since(SystemTime::UNIX_EPOCH) + .expect("valid timestamp") + .as_secs(); + Self { + secret: Self::get_secret(claims_type), + iss: JWT_ISSUER.to_string(), + sub, + client_id, + exp, + nbf, + } + } + + fn get_secret(claims_type: ClaimsType) -> String { + let env_var = match claims_type { + ClaimsType::Auth | ClaimsType::DesktopClient => AUTH_SECRET_ENV, + ClaimsType::Gateway => GATEWAY_SECRET_ENV, + ClaimsType::YubiBridge => YUBIBRIDGE_SECRET_ENV, + }; + env::var(env_var).unwrap_or_default() + } + + /// Convert claims to JWT. + pub fn to_jwt(&self) -> Result { + encode( + &Header::default(), + self, + &EncodingKey::from_secret(self.secret.as_bytes()), + ) + } + + /// Verify JWT and, if successful, convert it to claims. + pub fn from_jwt(claims_type: ClaimsType, token: &str) -> Result { + let secret = Self::get_secret(claims_type); + let mut validation = Validation::default(); + validation.validate_nbf = true; + validation.set_issuer(&[JWT_ISSUER]); + validation.set_required_spec_claims(&["iss", "sub", "exp", "nbf"]); + decode::( + token, + &DecodingKey::from_secret(secret.as_bytes()), + &validation, + ) + .map(|data| data.claims) + } +} diff --git a/crates/defguard_common/src/auth/mod.rs b/crates/defguard_common/src/auth/mod.rs new file mode 100644 index 0000000000..c0e708bb12 --- /dev/null +++ b/crates/defguard_common/src/auth/mod.rs @@ -0,0 +1 @@ +pub mod claims; diff --git a/crates/defguard_core/src/config.rs b/crates/defguard_common/src/config.rs similarity index 97% rename from crates/defguard_core/src/config.rs rename to crates/defguard_common/src/config.rs index e3cf365da5..2549ce610b 100644 --- a/crates/defguard_core/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -1,4 +1,4 @@ -use std::net::IpAddr; +use std::{net::IpAddr, sync::OnceLock}; use clap::{Args, Parser, Subcommand}; use humantime::Duration; @@ -12,6 +12,15 @@ use rsa::{ traits::PublicKeyParts, }; use secrecy::{ExposeSecret, SecretString}; +use serde::Serialize; + +pub static SERVER_CONFIG: OnceLock = OnceLock::new(); + +pub fn server_config() -> &'static DefGuardConfig { + SERVER_CONFIG + .get() + .expect("Server configuration not set yet") +} #[derive(Clone, Parser, Serialize, Debug)] #[command(version)] @@ -281,7 +290,7 @@ impl DefGuardConfig { /// Returns configured URL with "auth/callback" appended to the path. #[must_use] - pub(crate) fn callback_url(&self) -> Url { + pub fn callback_url(&self) -> Url { let mut url = self.url.clone(); // Append "auth/callback" to the URL. if let Ok(mut path_segments) = url.path_segments_mut() { diff --git a/crates/defguard_common/src/csv.rs b/crates/defguard_common/src/csv.rs new file mode 100644 index 0000000000..1695bafaba --- /dev/null +++ b/crates/defguard_common/src/csv.rs @@ -0,0 +1,17 @@ +pub trait AsCsv { + fn as_csv(&self) -> String; +} + +impl AsCsv for I +where + I: ?Sized + std::iter::IntoIterator, + for<'a> &'a I: IntoIterator, + T: ToString, +{ + fn as_csv(&self) -> String { + self.into_iter() + .map(ToString::to_string) + .collect::>() + .join(",") + } +} diff --git a/crates/defguard_common/src/db/mod.rs b/crates/defguard_common/src/db/mod.rs new file mode 100644 index 0000000000..d7ca63d055 --- /dev/null +++ b/crates/defguard_common/src/db/mod.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{ + PgPool, + postgres::{PgConnectOptions, PgPoolOptions}, +}; +use tracing::info; +use utoipa::ToSchema; + +pub mod models; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Eq, Default, Hash)] +pub struct NoId; +pub type Id = i64; + +// helper for easier migration handling with a custom `migration` folder location +// reference: https://docs.rs/sqlx/latest/sqlx/attr.test.html#automatic-migrations-requires-migrate-feature +pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../../migrations"); + +/// Initializes and migrates postgres database. Returns DB pool object. +pub async fn init_db(host: &str, port: u16, name: &str, user: &str, password: &str) -> PgPool { + info!("Initializing DB pool"); + let opts = PgConnectOptions::new() + .host(host) + .port(port) + .username(user) + .password(password) + .database(name); + let pool = PgPool::connect_with(opts) + .await + .expect("Database connection failed"); + MIGRATOR + .run(&pool) + .await + .expect("Cannot run database migrations."); + pool +} + +// Helper function to instantiate pool manually as a workaround for issues with `sqlx::test` macro +// reference: https://github.com/launchbadge/sqlx/issues/2567#issuecomment-2009849261 +pub async fn setup_pool(options: PgConnectOptions) -> PgPool { + let pool = PgPoolOptions::new().connect_with(options).await.unwrap(); + MIGRATOR + .run(&pool) + .await + .expect("Cannot run database migrations."); + pool +} diff --git a/crates/defguard_core/src/db/models/auth_code.rs b/crates/defguard_common/src/db/models/auth_code.rs similarity index 50% rename from crates/defguard_core/src/db/models/auth_code.rs rename to crates/defguard_common/src/db/models/auth_code.rs index 6b2fd73b53..b57774c708 100644 --- a/crates/defguard_core/src/db/models/auth_code.rs +++ b/crates/defguard_common/src/db/models/auth_code.rs @@ -1,15 +1,16 @@ use chrono::Utc; use model_derive::Model; -use sqlx::{Error as SqlxError, PgPool, query_as}; +use sqlx::{PgExecutor, query_as}; use crate::{ db::{Id, NoId}, random::gen_alphanumeric, }; -#[derive(Model, Clone)] +#[derive(Model)] #[table(authorization_code)] pub struct AuthCode { + #[allow(dead_code)] id: I, pub user_id: Id, pub client_id: String, @@ -46,21 +47,41 @@ impl AuthCode { } } +impl From> for AuthCode { + fn from(value: AuthCode) -> Self { + Self { + id: NoId, + user_id: value.user_id, + client_id: value.client_id, + code: value.code, + redirect_uri: value.redirect_uri, + scope: value.scope, + auth_time: value.auth_time, + nonce: value.nonce, + code_challenge: value.code_challenge, + } + } +} + impl AuthCode { /// Find by code. - pub async fn find_code(pool: &PgPool, code: &str) -> Result, SqlxError> { + /// If found, delete `AuthCode` from the database right away, so it can't be reused. + pub async fn find_code<'e, E>( + executor: E, + code: &str, + ) -> Result>, sqlx::Error> + where + E: PgExecutor<'e>, + { query_as!( Self, - "SELECT id, user_id, client_id, code, redirect_uri, scope, auth_time, nonce, \ - code_challenge FROM authorization_code WHERE code = $1", + "DELETE FROM authorization_code WHERE code = $1 \ + RETURNING id, user_id, client_id, code, redirect_uri, scope, auth_time, nonce, \ + code_challenge", code ) - .fetch_optional(pool) + .fetch_optional(executor) .await - } - - // Remove a used authorization_code - pub async fn consume(self, pool: &PgPool) -> Result<(), SqlxError> { - self.delete(pool).await + .map(|inner_option| inner_option.map(Into::into)) } } diff --git a/crates/defguard_core/src/db/models/authentication_key.rs b/crates/defguard_common/src/db/models/authentication_key.rs similarity index 94% rename from crates/defguard_core/src/db/models/authentication_key.rs rename to crates/defguard_common/src/db/models/authentication_key.rs index 84ddd50183..a1bc189586 100644 --- a/crates/defguard_core/src/db/models/authentication_key.rs +++ b/crates/defguard_common/src/db/models/authentication_key.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use model_derive::Model; +use serde::{Deserialize, Serialize}; use sqlx::{Error as SqlxError, PgExecutor, Type, query_as}; use crate::db::{Id, NoId}; @@ -25,11 +26,11 @@ impl Display for AuthenticationKeyType { #[derive(Clone, Debug, Deserialize, Model, Serialize)] #[table(authentication_key)] pub struct AuthenticationKey { - pub(crate) id: I, - pub(crate) yubikey_id: Option, + pub id: I, + pub yubikey_id: Option, pub name: Option, - pub(crate) user_id: Id, - pub(crate) key: String, + pub user_id: Id, + pub key: String, #[model(enum)] pub key_type: AuthenticationKeyType, } diff --git a/crates/defguard_core/src/db/models/biometric_auth.rs b/crates/defguard_common/src/db/models/biometric_auth.rs similarity index 95% rename from crates/defguard_core/src/db/models/biometric_auth.rs rename to crates/defguard_common/src/db/models/biometric_auth.rs index 1ac93a4e7a..8f477b5fc0 100644 --- a/crates/defguard_core/src/db/models/biometric_auth.rs +++ b/crates/defguard_common/src/db/models/biometric_auth.rs @@ -57,7 +57,7 @@ impl BiometricAuth { } impl BiometricAuth { - pub(crate) async fn find_by_device_id<'e, E>( + pub async fn find_by_device_id<'e, E>( executor: E, device_id: Id, ) -> Result, sqlx::Error> @@ -73,7 +73,7 @@ impl BiometricAuth { .await } - pub(crate) async fn verify_owner<'e, E>( + pub async fn verify_owner<'e, E>( executor: E, user_id: Id, pub_key: &str, @@ -91,10 +91,7 @@ impl BiometricAuth { Ok(q_result.is_some()) } - pub(crate) async fn find_by_user_id<'e, E>( - executor: E, - user_id: Id, - ) -> Result, sqlx::Error> + pub async fn find_by_user_id<'e, E>(executor: E, user_id: Id) -> Result, sqlx::Error> where E: PgExecutor<'e>, { @@ -124,6 +121,12 @@ fn decode_pub_key(public_key: &str) -> Result Ok(verifying_key) } +impl Default for BiometricChallenge { + fn default() -> Self { + Self::new() + } +} + impl BiometricChallenge { pub fn new_with_owner(pub_key: &str) -> Result { let _ = decode_pub_key(pub_key)?; diff --git a/crates/defguard_core/src/db/models/device_login.rs b/crates/defguard_common/src/db/models/device_login.rs similarity index 93% rename from crates/defguard_core/src/db/models/device_login.rs rename to crates/defguard_common/src/db/models/device_login.rs index e3a8e5b7d6..09b7d52eeb 100644 --- a/crates/defguard_core/src/db/models/device_login.rs +++ b/crates/defguard_common/src/db/models/device_login.rs @@ -2,6 +2,7 @@ use std::fmt; use chrono::{NaiveDateTime, Utc}; use model_derive::Model; +use serde::{Deserialize, Serialize}; use sqlx::{Error as SqlxError, PgPool, query_as}; use crate::db::{Id, NoId}; @@ -34,6 +35,7 @@ impl fmt::Display for DeviceLoginEvent { } impl DeviceLoginEvent { + #[allow(clippy::too_many_arguments)] #[must_use] pub fn new( user_id: Id, @@ -59,7 +61,7 @@ impl DeviceLoginEvent { } } - pub(crate) async fn check_if_device_already_logged_in( + pub async fn check_if_device_already_logged_in( self, pool: &PgPool, ) -> Result>, anyhow::Error> { @@ -72,7 +74,7 @@ impl DeviceLoginEvent { } } - pub(crate) async fn find_device_login_event( + pub async fn find_device_login_event( &self, pool: &PgPool, ) -> Result>, SqlxError> { diff --git a/crates/defguard_core/src/db/models/error.rs b/crates/defguard_common/src/db/models/error.rs similarity index 100% rename from crates/defguard_core/src/db/models/error.rs rename to crates/defguard_common/src/db/models/error.rs diff --git a/crates/defguard_common/src/db/models/mod.rs b/crates/defguard_common/src/db/models/mod.rs new file mode 100644 index 0000000000..0aa3a601bb --- /dev/null +++ b/crates/defguard_common/src/db/models/mod.rs @@ -0,0 +1,15 @@ +pub mod auth_code; +pub mod authentication_key; +pub mod biometric_auth; +pub mod device_login; +pub mod error; +pub mod settings; +pub mod user; + +pub use auth_code::AuthCode; +pub use authentication_key::{AuthenticationKey, AuthenticationKeyType}; +pub use biometric_auth::{BiometricAuth, BiometricChallenge}; +pub use device_login::DeviceLoginEvent; +pub use error::ModelError; +pub use settings::{Settings, SettingsEssentials}; +pub use user::MFAMethod; diff --git a/crates/defguard_core/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs similarity index 75% rename from crates/defguard_core/src/db/models/settings.rs rename to crates/defguard_common/src/db/models/settings.rs index d74ebd49ee..e2ae4fee63 100644 --- a/crates/defguard_core/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -1,12 +1,13 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; +use crate::{global_value, secret::SecretStringWrapper}; +use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, Type, query, query_as}; use struct_patch::Patch; use thiserror::Error; +use tracing::{debug, info, warn}; use uuid::Uuid; -use crate::{enterprise::ldap::sync::SyncStatus, global_value, secret::SecretStringWrapper}; - global_value!(SETTINGS, Option, None, set_settings, get_settings); /// Initializes global `SETTINGS` struct at program startup @@ -61,7 +62,22 @@ pub enum OpenidUsernameHandling { PruneEmailDomain, } -#[derive(Clone, Debug, Deserialize, PartialEq, Patch, Serialize, Default)] +#[derive(Clone, Debug, Copy, Eq, PartialEq, Deserialize, Serialize, Default, Type)] +#[sqlx(type_name = "ldap_sync_status", rename_all = "lowercase")] +pub enum LdapSyncStatus { + InSync, + #[default] + OutOfSync, +} + +impl LdapSyncStatus { + #[must_use] + pub fn is_out_of_sync(&self) -> bool { + matches!(self, LdapSyncStatus::OutOfSync) + } +} + +#[derive(Clone, Deserialize, PartialEq, Patch, Serialize, Default)] #[patch(attribute(derive(Deserialize, Serialize, Debug)))] pub struct Settings { // Modules @@ -107,7 +123,7 @@ pub struct Settings { pub ldap_member_attr: Option, pub ldap_use_starttls: bool, pub ldap_tls_verify_cert: bool, - pub ldap_sync_status: SyncStatus, + pub ldap_sync_status: LdapSyncStatus, pub ldap_enabled: bool, pub ldap_sync_enabled: bool, pub ldap_is_authoritative: bool, @@ -128,6 +144,85 @@ pub struct Settings { pub gateway_disconnect_notifications_reconnect_notification_enabled: bool, } +// Implement manually to avoid exposing the license key. +impl fmt::Debug for Settings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Settings") + .field("openid_enabled", &self.openid_enabled) + .field("wireguard_enabled", &self.wireguard_enabled) + .field("webhooks_enabled", &self.webhooks_enabled) + .field("worker_enabled", &self.worker_enabled) + .field("challenge_template", &self.challenge_template) + .field("instance_name", &self.instance_name) + .field("main_logo_url", &self.main_logo_url) + .field("nav_logo_url", &self.nav_logo_url) + .field("smtp_server", &self.smtp_server) + .field("smtp_port", &self.smtp_port) + .field("smtp_encryption", &self.smtp_encryption) + .field("smtp_user", &self.smtp_user) + .field("smtp_password", &self.smtp_password) + .field("smtp_sender", &self.smtp_sender) + .field( + "enrollment_vpn_step_optional", + &self.enrollment_vpn_step_optional, + ) + .field( + "enrollment_welcome_message", + &self.enrollment_welcome_message, + ) + .field("enrollment_welcome_email", &self.enrollment_welcome_email) + .field( + "enrollment_welcome_email_subject", + &self.enrollment_welcome_email_subject, + ) + .field( + "enrollment_use_welcome_message_as_email", + &self.enrollment_use_welcome_message_as_email, + ) + .field("uuid", &self.uuid) + .field("ldap_url", &self.ldap_url) + .field("ldap_bind_username", &self.ldap_bind_username) + .field("ldap_bind_password", &self.ldap_bind_password) + .field("ldap_group_search_base", &self.ldap_group_search_base) + .field("ldap_user_search_base", &self.ldap_user_search_base) + .field("ldap_user_obj_class", &self.ldap_user_obj_class) + .field("ldap_group_obj_class", &self.ldap_group_obj_class) + .field("ldap_username_attr", &self.ldap_username_attr) + .field("ldap_groupname_attr", &self.ldap_groupname_attr) + .field("ldap_group_member_attr", &self.ldap_group_member_attr) + .field("ldap_member_attr", &self.ldap_member_attr) + .field("ldap_use_starttls", &self.ldap_use_starttls) + .field("ldap_tls_verify_cert", &self.ldap_tls_verify_cert) + .field("ldap_sync_status", &self.ldap_sync_status) + .field("ldap_enabled", &self.ldap_enabled) + .field("ldap_sync_enabled", &self.ldap_sync_enabled) + .field("ldap_is_authoritative", &self.ldap_is_authoritative) + .field("ldap_uses_ad", &self.ldap_uses_ad) + .field("ldap_sync_interval", &self.ldap_sync_interval) + .field( + "ldap_user_auxiliary_obj_classes", + &self.ldap_user_auxiliary_obj_classes, + ) + .field("ldap_user_rdn_attr", &self.ldap_user_rdn_attr) + .field("ldap_sync_groups", &self.ldap_sync_groups) + .field("openid_create_account", &self.openid_create_account) + .field("openid_username_handling", &self.openid_username_handling) + .field( + "gateway_disconnect_notifications_enabled", + &self.gateway_disconnect_notifications_enabled, + ) + .field( + "gateway_disconnect_notifications_inactivity_threshold", + &self.gateway_disconnect_notifications_inactivity_threshold, + ) + .field( + "gateway_disconnect_notifications_reconnect_notification_enabled", + &self.gateway_disconnect_notifications_reconnect_notification_enabled, + ) + .finish_non_exhaustive() + } +} + impl Settings { pub async fn get<'e, E>(executor: E) -> Result, sqlx::Error> where @@ -149,7 +244,7 @@ impl Settings { license, gateway_disconnect_notifications_enabled, ldap_use_starttls, \ ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, \ gateway_disconnect_notifications_reconnect_notification_enabled, \ - ldap_sync_status \"ldap_sync_status: SyncStatus\", \ + ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", \ ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, \ ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, \ ldap_user_rdn_attr, ldap_sync_groups, \ @@ -270,7 +365,7 @@ impl Settings { self.gateway_disconnect_notifications_enabled, self.gateway_disconnect_notifications_inactivity_threshold, self.gateway_disconnect_notifications_reconnect_notification_enabled, - &self.ldap_sync_status as &SyncStatus, + &self.ldap_sync_status as &LdapSyncStatus, self.ldap_enabled, self.ldap_sync_enabled, self.ldap_is_authoritative, @@ -352,7 +447,7 @@ pub struct SettingsEssentials { } impl SettingsEssentials { - pub(crate) async fn get_settings_essentials<'e, E>(executor: E) -> Result + pub async fn get_settings_essentials<'e, E>(executor: E) -> Result where E: PgExecutor<'e>, { @@ -381,7 +476,7 @@ impl From for SettingsEssentials { } } -mod defaults { +pub mod defaults { pub static WELCOME_MESSAGE: &str = "Dear {{ first_name }} {{ last_name }}, By completing the enrollment process, you now have access to all company systems. @@ -449,4 +544,17 @@ mod test { settings.smtp_password = Some(SecretStringWrapper::from_str("hunter2").unwrap()); assert!(settings.smtp_configured()); } + + #[test] + fn dg25_32_test_dont_expose_license_key() { + let key = "0000000000000000"; + let settings = Settings { + license: Some(key.to_string()), + ..Default::default() + }; + + let debug = format!("{settings:?}"); + assert!(!debug.contains("license")); + assert!(!debug.contains(key)); + } } diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs new file mode 100644 index 0000000000..d632bb9483 --- /dev/null +++ b/crates/defguard_common/src/db/models/user.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use sqlx::Type; +use utoipa::ToSchema; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, ToSchema, Type)] +#[sqlx(type_name = "mfa_method", rename_all = "snake_case")] +pub enum MFAMethod { + None, + OneTimePassword, + Webauthn, + Email, +} + +// Web MFA methods +impl fmt::Display for MFAMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + MFAMethod::None => "None", + MFAMethod::OneTimePassword => "TOTP", + MFAMethod::Webauthn => "WebAuthn", + MFAMethod::Email => "Email", + } + ) + } +} diff --git a/crates/defguard_core/src/globals.rs b/crates/defguard_common/src/globals.rs similarity index 100% rename from crates/defguard_core/src/globals.rs rename to crates/defguard_common/src/globals.rs diff --git a/crates/defguard_core/src/hex.rs b/crates/defguard_common/src/hex.rs similarity index 100% rename from crates/defguard_core/src/hex.rs rename to crates/defguard_common/src/hex.rs diff --git a/crates/defguard_common/src/lib.rs b/crates/defguard_common/src/lib.rs new file mode 100644 index 0000000000..c326bfc3c3 --- /dev/null +++ b/crates/defguard_common/src/lib.rs @@ -0,0 +1,11 @@ +pub mod auth; +pub mod config; +pub mod csv; +pub mod db; +pub mod globals; +pub mod hex; +pub mod random; +pub mod secret; + +pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "+", env!("VERGEN_GIT_SHA")); +pub const CARGO_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/defguard_core/src/random.rs b/crates/defguard_common/src/random.rs similarity index 77% rename from crates/defguard_core/src/random.rs rename to crates/defguard_common/src/random.rs index 30edce4459..ce12448d41 100644 --- a/crates/defguard_core/src/random.rs +++ b/crates/defguard_common/src/random.rs @@ -2,7 +2,7 @@ use rand::{Rng, distributions::Alphanumeric, thread_rng}; /// Generate random alphanumeric string. #[must_use] -pub(crate) fn gen_alphanumeric(n: usize) -> String { +pub fn gen_alphanumeric(n: usize) -> String { thread_rng() .sample_iter(Alphanumeric) .take(n) @@ -12,6 +12,6 @@ pub(crate) fn gen_alphanumeric(n: usize) -> String { /// Generate random 20-byte secret for TOTP. #[must_use] -pub(crate) fn gen_totp_secret() -> Vec { +pub fn gen_totp_secret() -> Vec { thread_rng().r#gen::<[u8; 20]>().to_vec() } diff --git a/crates/defguard_core/src/secret.rs b/crates/defguard_common/src/secret.rs similarity index 100% rename from crates/defguard_core/src/secret.rs rename to crates/defguard_common/src/secret.rs diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index acd2a5d78c..2d57b13188 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard_core" -version = "1.5.0" +version = "0.0.0" edition.workspace = true license-file.workspace = true homepage.workspace = true @@ -9,6 +9,9 @@ rust-version.workspace = true [dependencies] # internal crates +defguard_common = { workspace = true } +defguard_mail = { workspace = true } +defguard_proto = { workspace = true } defguard_web_ui = { workspace = true } defguard_version = { workspace = true } model_derive = { workspace = true } @@ -22,7 +25,6 @@ axum-extra = { workspace = true } base32 = { workspace = true } base64 = { workspace = true } chrono = { workspace = true } -clap = { workspace = true } humantime = { workspace = true } # match version used by sqlx ipnetwork = { workspace = true } @@ -31,14 +33,11 @@ jsonwebtoken = { workspace = true } ldap3 = { workspace = true } lettre = { workspace = true } md4 = { workspace = true } -openidconnect = { version = "4.0", default-features = false, optional = true, features = [ - "reqwest", -] } +openidconnect.workspace = true parse_link_header = { workspace = true } paste = { workspace = true } pgp = { workspace = true } -prost = "0.14" -pulldown-cmark = { workspace = true } +prost.workspace = true # match version used by sqlx rand = { workspace = true } reqwest = { workspace = true } @@ -65,7 +64,7 @@ tokio-stream = { workspace = true } tokio-util = { workspace = true } tonic = { workspace = true } tonic-health = { workspace = true } -tonic-prost = "0.14" +tonic-prost.workspace = true totp-lite = { workspace = true } tower-http = { workspace = true } tracing = { workspace = true } @@ -82,14 +81,15 @@ x25519-dalek = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } bytes = { workspace = true } -ed25519-dalek = { version = "2.2", features = ["rand_core"] } tower = "0.5" +regex = "1.10" +ammonia = "4.1.1" [dev-dependencies] bytes = "1.6" -claims = "0.8" +claims.workspace = true hyper-util = "0.1" -matches = "0.1" +matches.workspace = true regex = "1.10" reqwest = { version = "0.12", features = [ "cookies", @@ -103,11 +103,4 @@ tower = "0.5" webauthn-authenticator-rs = { version = "0.5", features = ["softpasskey"] } [build-dependencies] -tonic-prost-build = "0.14" -vergen-git2 = { version = "1.0", features = ["build"] } - -[features] -default = ["openid", "wireguard", "worker"] -openid = ["dep:openidconnect"] -worker = [] -wireguard = [] +tonic-prost-build.workspace = true diff --git a/crates/defguard_core/build.rs b/crates/defguard_core/build.rs index a4c2030a96..96c212192d 100644 --- a/crates/defguard_core/build.rs +++ b/crates/defguard_core/build.rs @@ -1,35 +1,14 @@ -use vergen_git2::{Emitter, Git2Builder}; - fn main() -> Result<(), Box> { - // set VERGEN_GIT_SHA env variable based on git commit hash - let git2 = Git2Builder::default().branch(true).sha(true).build()?; - Emitter::default().add_instructions(&git2)?.emit()?; - tonic_prost_build::configure() .protoc_arg("--experimental_allow_proto3_optional") .type_attribute( - "license.LicenseLimits", + "LicenseLimits", "#[derive(serde::Serialize, serde::Deserialize)]", ) .compile_protos( - &[ - "../../proto/core/auth.proto", - "../../proto/core/proxy.proto", - "../../proto/worker/worker.proto", - "../../proto/wireguard/gateway.proto", - "../../proto/enterprise/firewall/firewall.proto", - "src/enterprise/proto/license.proto", - ], - &[ - "../../proto/core", - "../../proto/worker", - "../../proto/wireguard", - "../../proto/enterprise/firewall", - "src/enterprise/proto", - ], + &["src/enterprise/proto/license.proto"], + &["src/enterprise/proto"], )?; - println!("cargo:rerun-if-changed=../../migrations"); - println!("cargo:rerun-if-changed=../../proto"); println!("cargo:rerun-if-changed=src/enterprise"); Ok(()) } diff --git a/crates/defguard_core/src/appstate.rs b/crates/defguard_core/src/appstate.rs index 3930b72c6a..7483b0b725 100644 --- a/crates/defguard_core/src/appstate.rs +++ b/crates/defguard_core/src/appstate.rs @@ -2,6 +2,8 @@ use std::sync::{Arc, Mutex, RwLock}; use axum::extract::FromRef; use axum_extra::extract::cookie::Key; +use defguard_common::config::server_config; +use defguard_mail::Mail; use reqwest::Client; use secrecy::ExposeSecret; use serde_json::json; @@ -21,8 +23,6 @@ use crate::{ error::WebError, events::ApiEvent, grpc::gateway::{send_multiple_wireguard_events, send_wireguard_event}, - mail::Mail, - server_config, version::IncompatibleComponents, }; diff --git a/crates/defguard_core/src/auth/mod.rs b/crates/defguard_core/src/auth/mod.rs index b0fd990e20..462f904fa6 100644 --- a/crates/defguard_core/src/auth/mod.rs +++ b/crates/defguard_core/src/auth/mod.rs @@ -1,13 +1,8 @@ pub mod failed_login; -use std::{ - env, - time::{Duration, SystemTime}, -}; - use axum::{ extract::{FromRef, FromRequestParts, OptionalFromRequestParts}, - http::{header::AUTHORIZATION, request::Parts}, + http::request::Parts, }; use axum_client_ip::InsecureClientIp; use axum_extra::{ @@ -15,15 +10,12 @@ use axum_extra::{ extract::cookie::CookieJar, headers::{Authorization, authorization::Bearer}, }; -use jsonwebtoken::{ - DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::Error as JWTError, -}; -use serde::{Deserialize, Serialize}; +use defguard_common::db::Id; use crate::{ appstate::AppState, db::{ - Group, Id, OAuth2AuthorizedApp, OAuth2Token, Session, SessionState, User, + Group, OAuth2Token, Session, SessionState, User, models::{group::Permission, oauth2client::OAuth2Client}, }, enterprise::{db::models::api_tokens::ApiToken, is_enterprise_enabled}, @@ -31,98 +23,10 @@ use crate::{ handlers::SESSION_COOKIE_NAME, }; -pub static JWT_ISSUER: &str = "DefGuard"; -pub static AUTH_SECRET_ENV: &str = "DEFGUARD_AUTH_SECRET"; -pub static GATEWAY_SECRET_ENV: &str = "DEFGUARD_GATEWAY_SECRET"; -pub static YUBIBRIDGE_SECRET_ENV: &str = "DEFGUARD_YUBIBRIDGE_SECRET"; pub const TOTP_CODE_VALIDITY_PERIOD: u64 = 30; pub const EMAIL_CODE_DIGITS: u32 = 6; pub const TOTP_CODE_DIGITS: u32 = 6; -#[derive(Clone, Copy, Default)] -pub enum ClaimsType { - #[default] - Auth, - Gateway, - YubiBridge, - DesktopClient, -} - -/// Standard claims: https://www.iana.org/assignments/jwt/jwt.xhtml -#[derive(Deserialize, Serialize)] -pub struct Claims { - #[serde(skip_serializing, skip_deserializing)] - secret: String, - // issuer - pub iss: String, - // subject - pub sub: String, - // client identifier - pub client_id: String, - // expiration time - pub exp: u64, - // not before - pub nbf: u64, -} - -impl Claims { - #[must_use] - pub fn new(claims_type: ClaimsType, sub: String, client_id: String, duration: u64) -> Self { - let now = SystemTime::now(); - let exp = now - .checked_add(Duration::from_secs(duration)) - .expect("valid time") - .duration_since(SystemTime::UNIX_EPOCH) - .expect("valid timestamp") - .as_secs(); - let nbf = now - .duration_since(SystemTime::UNIX_EPOCH) - .expect("valid timestamp") - .as_secs(); - Self { - secret: Self::get_secret(claims_type), - iss: JWT_ISSUER.to_string(), - sub, - client_id, - exp, - nbf, - } - } - - fn get_secret(claims_type: ClaimsType) -> String { - let env_var = match claims_type { - ClaimsType::Auth | ClaimsType::DesktopClient => AUTH_SECRET_ENV, - ClaimsType::Gateway => GATEWAY_SECRET_ENV, - ClaimsType::YubiBridge => YUBIBRIDGE_SECRET_ENV, - }; - env::var(env_var).unwrap_or_default() - } - - /// Convert claims to JWT. - pub fn to_jwt(&self) -> Result { - encode( - &Header::default(), - self, - &EncodingKey::from_secret(self.secret.as_bytes()), - ) - } - - /// Verify JWT and, if successful, convert it to claims. - pub fn from_jwt(claims_type: ClaimsType, token: &str) -> Result { - let secret = Self::get_secret(claims_type); - let mut validation = Validation::default(); - validation.validate_nbf = true; - validation.set_issuer(&[JWT_ISSUER]); - validation.set_required_spec_claims(&["iss", "sub", "exp", "nbf"]); - decode::( - token, - &DecodingKey::from_secret(secret.as_bytes()), - &validation, - ) - .map(|data| data.claims) - } -} - impl FromRequestParts for Session where S: Send + Sync, @@ -375,82 +279,6 @@ impl UserClaims { } } -// User authenticated by a valid access token -pub struct AccessUserInfo(pub(crate) UserClaims); - -impl FromRequestParts for AccessUserInfo -where - S: Send + Sync, - AppState: FromRef, -{ - type Rejection = WebError; - - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let appstate = AppState::from_ref(state); - if let Some(token) = parts.headers.get(AUTHORIZATION).and_then(|value| { - if let Ok(value) = value.to_str() { - if value.to_lowercase().starts_with("bearer ") { - value.get(7..) - } else { - None - } - } else { - None - } - }) { - // TODO: #[cfg(feature = "openid")] - match OAuth2Token::find_access_token(&appstate.pool, token).await { - Ok(Some(oauth2token)) => { - match OAuth2AuthorizedApp::find_by_id( - &appstate.pool, - oauth2token.oauth2authorizedapp_id, - ) - .await - { - Ok(Some(authorized_app)) => { - if let Ok(Some(user)) = - User::find_by_id(&appstate.pool, authorized_app.user_id).await - { - if let Some(client) = OAuth2Client::find_by_id( - &appstate.pool, - authorized_app.oauth2client_id, - ) - .await? - { - return Ok(AccessUserInfo(UserClaims::from_user( - &user, - &client, - &oauth2token, - ))); - } else { - return Err(WebError::Authorization( - "OAuth2 client not found".into(), - )); - } - } - } - Ok(None) => { - return Err(WebError::Authorization("Authorized app not found".into())); - } - - Err(err) => { - return Err(err.into()); - } - } - } - Ok(None) => { - return Err(WebError::Authorization("Invalid token".into())); - } - Err(err) => { - return Err(err.into()); - } - } - } - - Err(WebError::Authorization("Invalid session".into())) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/defguard_core/src/db/mod.rs b/crates/defguard_core/src/db/mod.rs index 1dcc96ec1c..37bcc961f0 100644 --- a/crates/defguard_core/src/db/mod.rs +++ b/crates/defguard_core/src/db/mod.rs @@ -1,33 +1,5 @@ pub mod models; -use sqlx::postgres::{PgConnectOptions, PgPool, PgPoolOptions}; -use utoipa::ToSchema; - -use crate::MIGRATOR; - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Eq, Default, Hash)] -pub struct NoId; -pub type Id = i64; - -/// Initializes and migrates postgres database. Returns DB pool object. -pub async fn init_db(host: &str, port: u16, name: &str, user: &str, password: &str) -> PgPool { - info!("Initializing DB pool"); - let opts = PgConnectOptions::new() - .host(host) - .port(port) - .username(user) - .password(password) - .database(name); - let pool = PgPool::connect_with(opts) - .await - .expect("Database connection failed"); - MIGRATOR - .run(&pool) - .await - .expect("Cannot run database migrations."); - pool -} - pub use models::{ MFAInfo, UserDetails, UserInfo, device::{AddDevice, Device}, @@ -35,21 +7,9 @@ pub use models::{ oauth2authorizedapp::OAuth2AuthorizedApp, oauth2token::OAuth2Token, session::{Session, SessionState}, - settings::Settings, - user::{MFAMethod, User}, + user::User, webauthn::WebAuthn, webhook::{AppEvent, HWKeyUserData, WebHook}, wireguard::{GatewayEvent, WireguardNetwork}, yubikey::YubiKey, }; - -// Helper function to instantiate pool manually as a workaround for issues with `sqlx::test` macro -// reference: https://github.com/launchbadge/sqlx/issues/2567#issuecomment-2009849261 -pub async fn setup_pool(options: PgConnectOptions) -> PgPool { - let pool = PgPoolOptions::new().connect_with(options).await.unwrap(); - MIGRATOR - .run(&pool) - .await - .expect("Cannot run database migrations."); - pool -} diff --git a/crates/defguard_core/src/db/models/activity_log/metadata.rs b/crates/defguard_core/src/db/models/activity_log/metadata.rs index c1c1032cce..e358560fe2 100644 --- a/crates/defguard_core/src/db/models/activity_log/metadata.rs +++ b/crates/defguard_core/src/db/models/activity_log/metadata.rs @@ -1,22 +1,22 @@ use chrono::NaiveDateTime; +use defguard_common::db::{ + Id, + models::{ + AuthenticationKey, AuthenticationKeyType, MFAMethod, Settings, + settings::{LdapSyncStatus, OpenidUsernameHandling, SmtpEncryption}, + }, +}; use crate::{ db::{ - Device, Group, Id, MFAMethod, Settings, User, WebAuthn, WebHook, WireguardNetwork, - models::{ - authentication_key::{AuthenticationKey, AuthenticationKeyType}, - oauth2client::OAuth2Client, - settings::{OpenidUsernameHandling, SmtpEncryption}, - }, + Device, Group, User, WebAuthn, WebHook, WireguardNetwork, + models::oauth2client::OAuth2Client, }, - enterprise::{ - db::models::{ - activity_log_stream::{ActivityLogStream, ActivityLogStreamType}, - api_tokens::ApiToken, - openid_provider::{DirectorySyncTarget, DirectorySyncUserBehavior, OpenIdProvider}, - snat::UserSnatBinding, - }, - ldap::sync::SyncStatus, + enterprise::db::models::{ + activity_log_stream::{ActivityLogStream, ActivityLogStreamType}, + api_tokens::ApiToken, + openid_provider::{DirectorySyncTarget, DirectorySyncUserBehavior, OpenIdProvider}, + snat::UserSnatBinding, }, events::ClientMFAMethod, }; @@ -377,7 +377,7 @@ pub struct SettingsNoSecrets { pub ldap_member_attr: Option, pub ldap_use_starttls: bool, pub ldap_tls_verify_cert: bool, - pub ldap_sync_status: SyncStatus, + pub ldap_sync_status: LdapSyncStatus, pub ldap_enabled: bool, pub ldap_sync_enabled: bool, pub ldap_is_authoritative: bool, diff --git a/crates/defguard_core/src/db/models/activity_log/mod.rs b/crates/defguard_core/src/db/models/activity_log/mod.rs index a175e50c2e..62cf59154c 100644 --- a/crates/defguard_core/src/db/models/activity_log/mod.rs +++ b/crates/defguard_core/src/db/models/activity_log/mod.rs @@ -3,7 +3,7 @@ use ipnetwork::IpNetwork; use model_derive::Model; use sqlx::{FromRow, Type}; -use crate::db::{Id, NoId}; +use defguard_common::db::{Id, NoId}; pub mod metadata; diff --git a/crates/defguard_core/src/db/models/device.rs b/crates/defguard_core/src/db/models/device.rs index 8977f70288..00c895ff15 100644 --- a/crates/defguard_core/src/db/models/device.rs +++ b/crates/defguard_core/src/db/models/device.rs @@ -4,6 +4,10 @@ use base64::{Engine, prelude::BASE64_STANDARD}; #[cfg(test)] use chrono::NaiveDate; use chrono::{NaiveDateTime, Utc}; +use defguard_common::{ + csv::AsCsv, + db::{Id, NoId, models::ModelError}, +}; use ipnetwork::IpNetwork; use model_derive::Model; #[cfg(test)] @@ -19,14 +23,10 @@ use sqlx::{ use thiserror::Error; use utoipa::ToSchema; -use super::{ - error::ModelError, - wireguard::{LocationMfaMode, NetworkAddressError, WIREGUARD_MAX_HANDSHAKE, WireguardNetwork}, -}; -use crate::{ - AsCsv, KEY_LENGTH, - db::{Id, NoId, User}, +use super::wireguard::{ + LocationMfaMode, NetworkAddressError, WIREGUARD_MAX_HANDSHAKE, WireguardNetwork, }; +use crate::{KEY_LENGTH, db::User}; #[derive(Serialize, ToSchema)] pub struct DeviceConfig { @@ -1010,10 +1010,11 @@ mod test { use std::str::FromStr; use claims::{assert_err, assert_ok}; + use defguard_common::db::setup_pool; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use crate::db::{User, setup_pool}; + use crate::db::User; impl Device { /// Create new device and assign IP in a given network diff --git a/crates/defguard_core/src/db/models/enrollment.rs b/crates/defguard_core/src/db/models/enrollment.rs index 8204991123..7c9237c3ca 100644 --- a/crates/defguard_core/src/db/models/enrollment.rs +++ b/crates/defguard_core/src/db/models/enrollment.rs @@ -1,4 +1,14 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; +use defguard_common::{ + VERSION, + config::server_config, + db::{Id, models::Settings}, + random::gen_alphanumeric, +}; +use defguard_mail::{ + Mail, + templates::{self, TemplateError, safe_tera}, +}; use reqwest::Url; use sqlx::{Error as SqlxError, PgConnection, PgExecutor, PgPool, query, query_as}; use tera::Context; @@ -6,15 +16,7 @@ use thiserror::Error; use tokio::sync::mpsc::UnboundedSender; use tonic::{Code, Status}; -use super::{User, settings::Settings}; -use crate::{ - VERSION, - db::Id, - mail::Mail, - random::gen_alphanumeric, - server_config, - templates::{self, TemplateError, safe_tera}, -}; +use super::User; pub static ENROLLMENT_TOKEN_TYPE: &str = "ENROLLMENT"; pub static PASSWORD_RESET_TOKEN_TYPE: &str = "PASSWORD_RESET"; @@ -356,7 +358,7 @@ impl Token { // load configured content as template let mut tera = safe_tera(); - tera.add_raw_template("welcome_page", &settings.enrollment_welcome_message()?)?; + tera.add_raw_template("welcome_page", &enrollment_welcome_message(&settings)?)?; let context = self.get_welcome_message_context(&mut *transaction).await?; @@ -374,7 +376,7 @@ impl Token { // load configured content as template let mut tera = safe_tera(); - tera.add_raw_template("welcome_email", &settings.enrollment_welcome_email()?)?; + tera.add_raw_template("welcome_email", &enrollment_welcome_email(&settings)?)?; let context = self.get_welcome_message_context(&mut *transaction).await?; let content = tera.render("welcome_email", &context)?; @@ -616,21 +618,19 @@ impl User { } } -impl Settings { - pub fn enrollment_welcome_message(&self) -> Result { - self.enrollment_welcome_message.clone().ok_or_else(|| { - error!("Enrollment welcome message not configured"); - TokenError::WelcomeMsgNotConfigured - }) - } +pub fn enrollment_welcome_message(settings: &Settings) -> Result { + settings.enrollment_welcome_message.clone().ok_or_else(|| { + error!("Enrollment welcome message not configured"); + TokenError::WelcomeMsgNotConfigured + }) +} - pub fn enrollment_welcome_email(&self) -> Result { - if self.enrollment_use_welcome_message_as_email { - return self.enrollment_welcome_message(); - } - self.enrollment_welcome_email.clone().ok_or_else(|| { - error!("Enrollment welcome email not configured"); - TokenError::WelcomeEmailNotConfigured - }) +pub fn enrollment_welcome_email(settings: &Settings) -> Result { + if settings.enrollment_use_welcome_message_as_email { + return enrollment_welcome_message(settings); } + settings.enrollment_welcome_email.clone().ok_or_else(|| { + error!("Enrollment welcome email not configured"); + TokenError::WelcomeEmailNotConfigured + }) } diff --git a/crates/defguard_core/src/db/models/group.rs b/crates/defguard_core/src/db/models/group.rs index 6dadfbf904..b9734fd4f6 100644 --- a/crates/defguard_core/src/db/models/group.rs +++ b/crates/defguard_core/src/db/models/group.rs @@ -1,10 +1,11 @@ use std::fmt; +use defguard_common::db::{Id, NoId, models::ModelError}; use model_derive::Model; use sqlx::{Error as SqlxError, FromRow, PgConnection, PgExecutor, query, query_as, query_scalar}; use utoipa::ToSchema; -use crate::db::{Id, NoId, User, WireguardNetwork, models::error::ModelError}; +use crate::db::{User, WireguardNetwork}; #[derive(Debug)] pub enum Permission { @@ -296,10 +297,11 @@ impl WireguardNetwork { #[cfg(test)] mod test { + use defguard_common::db::setup_pool; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use crate::db::{User, setup_pool}; + use crate::db::User; #[sqlx::test] async fn test_group(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/db/models/mod.rs b/crates/defguard_core/src/db/models/mod.rs index 265c027f6b..df2faac41b 100644 --- a/crates/defguard_core/src/db/models/mod.rs +++ b/crates/defguard_core/src/db/models/mod.rs @@ -1,22 +1,12 @@ pub mod activity_log; -#[cfg(feature = "openid")] -pub mod auth_code; -pub mod authentication_key; -pub mod biometric_auth; pub mod device; -pub mod device_login; pub mod enrollment; -pub mod error; pub mod group; -#[cfg(feature = "openid")] pub mod oauth2authorizedapp; -#[cfg(feature = "openid")] pub mod oauth2client; -#[cfg(feature = "openid")] pub mod oauth2token; pub mod polling_token; pub mod session; -pub mod settings; pub mod user; pub mod webauthn; pub mod webhook; @@ -26,17 +16,16 @@ pub mod yubikey; use std::collections::HashSet; +use defguard_common::db::{ + Id, + models::{BiometricAuth, MFAMethod}, +}; use sqlx::{Error as SqlxError, PgConnection, PgPool, query_as}; use utoipa::ToSchema; -use self::{ - device::UserDevice, - user::{MFAMethod, User}, -}; -use super::{Group, Id}; -use crate::db::models::biometric_auth::BiometricAuth; +use self::{device::UserDevice, user::User}; +use super::Group; -#[cfg(feature = "openid")] #[derive(Deserialize, Serialize)] pub struct NewOpenIDClient { pub name: String, @@ -143,7 +132,7 @@ impl UserInfo { /// /// Return `true` if groups were changed, `false` otherwise. pub(crate) async fn handle_user_groups( - &mut self, + &self, transaction: &mut PgConnection, user: &mut User, ) -> Result { @@ -283,10 +272,10 @@ impl MFAInfo { #[cfg(test)] mod test { + use defguard_common::db::setup_pool; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use crate::db::setup_pool; #[sqlx::test] async fn test_user_info(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/db/models/oauth2authorizedapp.rs b/crates/defguard_core/src/db/models/oauth2authorizedapp.rs index 421a93437b..0039265a65 100644 --- a/crates/defguard_core/src/db/models/oauth2authorizedapp.rs +++ b/crates/defguard_core/src/db/models/oauth2authorizedapp.rs @@ -1,7 +1,7 @@ use model_derive::Model; use sqlx::{Error as SqlxError, PgPool, query_as}; -use crate::db::{Id, NoId}; +use defguard_common::db::{Id, NoId}; #[derive(Model)] pub struct OAuth2AuthorizedApp { diff --git a/crates/defguard_core/src/db/models/oauth2client.rs b/crates/defguard_core/src/db/models/oauth2client.rs index ce3561e601..5f695618b5 100644 --- a/crates/defguard_core/src/db/models/oauth2client.rs +++ b/crates/defguard_core/src/db/models/oauth2client.rs @@ -1,11 +1,11 @@ use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query_as}; +use crate::db::OAuth2Token; + use super::NewOpenIDClient; -use crate::{ - db::{Id, NoId}, - random::gen_alphanumeric, -}; +use defguard_common::db::{Id, NoId}; +use defguard_common::random::gen_alphanumeric; #[derive(Clone, Debug, Deserialize, Model, Serialize)] pub struct OAuth2Client { @@ -101,6 +101,37 @@ impl OAuth2Client { .fetch_optional(pool) .await } + + pub(crate) async fn find_by_token( + pool: &PgPool, + token: &OAuth2Token, + ) -> Result, SqlxError> { + query_as!( + Self, + "SELECT c.id, c.client_id, c.client_secret, c.redirect_uri, c.scope, c.name, c.enabled \ + FROM oauth2client c \ + JOIN oauth2authorizedapp a ON a.oauth2client_id = c.id \ + JOIN oauth2token t ON t.oauth2authorizedapp_id = a.id \ + WHERE t.access_token = $1 OR t.refresh_token = $2", + token.access_token, + token.refresh_token + ) + .fetch_optional(pool) + .await + } + + /// Checks if `url` matches client config (ignoring trailing slashes). + pub(crate) fn contains_redirect_url(&self, url: &str) -> bool { + let url_trimmed = url.trim_end_matches('/'); + + for redirect in &self.redirect_uri { + if url_trimmed == redirect.trim_end_matches('/') { + return true; + } + } + + false + } } // Safe to show for not privileged users @@ -120,3 +151,28 @@ impl From> for OAuth2ClientSafe { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_contains_redirect_url() { + let oauth2client = OAuth2Client { + id: 1, + client_id: String::new(), + client_secret: String::new(), + redirect_uri: vec![ + String::from("http://localhost/"), + String::from("http://safe.net/"), + ], + scope: Vec::new(), + name: String::new(), + enabled: true, + }; + assert!(oauth2client.contains_redirect_url("http://safe.net")); + assert!(oauth2client.contains_redirect_url("http://localhost")); + assert!(!oauth2client.contains_redirect_url("http://safe.net/api")); + assert!(!oauth2client.contains_redirect_url("http://nonexistent:8000")); + } +} diff --git a/crates/defguard_core/src/db/models/oauth2token.rs b/crates/defguard_core/src/db/models/oauth2token.rs index 3e183ffb06..abc400d711 100644 --- a/crates/defguard_core/src/db/models/oauth2token.rs +++ b/crates/defguard_core/src/db/models/oauth2token.rs @@ -1,8 +1,7 @@ use chrono::{TimeDelta, Utc}; +use defguard_common::{config::server_config, db::Id, random::gen_alphanumeric}; use sqlx::{Error as SqlxError, PgPool, query, query_as}; -use crate::{db::Id, random::gen_alphanumeric, server_config}; - pub struct OAuth2Token { pub oauth2authorizedapp_id: Id, pub access_token: String, diff --git a/crates/defguard_core/src/db/models/polling_token.rs b/crates/defguard_core/src/db/models/polling_token.rs index 59fcaba079..8b19e98dda 100644 --- a/crates/defguard_core/src/db/models/polling_token.rs +++ b/crates/defguard_core/src/db/models/polling_token.rs @@ -2,10 +2,8 @@ use chrono::{NaiveDateTime, Utc}; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query_as}; -use crate::{ - db::{Id, NoId}, - random::gen_alphanumeric, -}; +use defguard_common::db::{Id, NoId}; +use defguard_common::random::gen_alphanumeric; // Token used for polling requests. #[derive(Clone, Debug, Model)] diff --git a/crates/defguard_core/src/db/models/session.rs b/crates/defguard_core/src/db/models/session.rs index 8fe3945aa0..52d7a0fd9b 100644 --- a/crates/defguard_core/src/db/models/session.rs +++ b/crates/defguard_core/src/db/models/session.rs @@ -1,8 +1,9 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; +use defguard_common::{config::server_config, db::Id, random::gen_alphanumeric}; use sqlx::{Error as SqlxError, PgExecutor, PgPool, Type, query, query_as}; use webauthn_rs::prelude::{PasskeyAuthentication, PasskeyRegistration}; -use crate::{db::Id, random::gen_alphanumeric, server_config}; +use defguard_mail::templates::SessionContext; #[derive(Clone, PartialEq, Type)] #[repr(i16)] @@ -27,6 +28,15 @@ pub struct Session { pub device_info: Option, } +impl From for SessionContext { + fn from(value: Session) -> Self { + Self { + ip_address: value.ip_address, + device_info: value.device_info, + } + } +} + impl Session { #[must_use] pub fn new( diff --git a/crates/defguard_core/src/db/models/user.rs b/crates/defguard_core/src/db/models/user.rs index d2e02ee1d5..6ba075b646 100644 --- a/crates/defguard_core/src/db/models/user.rs +++ b/crates/defguard_core/src/db/models/user.rs @@ -8,6 +8,7 @@ use argon2::{ }, }; use axum::http::StatusCode; +use defguard_mail::templates::UserContext; use model_derive::Model; #[cfg(test)] use rand::{ @@ -17,12 +18,10 @@ use rand::{ }; use serde::Serialize; use sqlx::{ - Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, Type, query, query_as, - query_scalar, + Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, query, query_as, query_scalar, }; use tokio::sync::broadcast::Sender; use totp_lite::{Sha1, totp_custom}; -use utoipa::ToSchema; use super::{ MFAInfo, OAuth2AuthorizedAppInfo, SecurityKey, @@ -32,61 +31,19 @@ use super::{ }; use crate::{ auth::{EMAIL_CODE_DIGITS, TOTP_CODE_DIGITS, TOTP_CODE_VALIDITY_PERIOD}, - db::{GatewayEvent, Id, NoId, Session, WireguardNetwork, models::group::Permission}, + db::{GatewayEvent, Session, WireguardNetwork, models::group::Permission}, enterprise::limits::update_counts, error::WebError, - grpc::{ - gateway::{send_multiple_wireguard_events, send_wireguard_event}, - proto::proxy::MfaMethod, - }, + grpc::gateway::{send_multiple_wireguard_events, send_wireguard_event}, +}; +use defguard_common::{ + config::server_config, + db::{Id, NoId, models::MFAMethod}, random::{gen_alphanumeric, gen_totp_secret}, - server_config, }; const RECOVERY_CODES_COUNT: usize = 8; -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, ToSchema, Type)] -#[sqlx(type_name = "mfa_method", rename_all = "snake_case")] -pub enum MFAMethod { - None, - OneTimePassword, - Webauthn, - Email, -} - -// Web MFA methods -impl fmt::Display for MFAMethod { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - MFAMethod::None => "None", - MFAMethod::OneTimePassword => "TOTP", - MFAMethod::Webauthn => "WebAuthn", - MFAMethod::Email => "Email", - } - ) - } -} - -// Client MFA methods -impl fmt::Display for MfaMethod { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - MfaMethod::Totp => "TOTP", - MfaMethod::Email => "Email", - MfaMethod::Oidc => "OIDC", - MfaMethod::Biometric => "Biometric", - MfaMethod::MobileApprove => "MobileApprove", - } - ) - } -} - // User information ready to be sent as part of diagnostic data. #[derive(Serialize)] pub struct UserDiagnostic { @@ -202,6 +159,15 @@ fn hash_password(password: &str) -> Result { .to_string()) } +impl From> for UserContext { + fn from(value: User) -> Self { + Self { + last_name: value.last_name, + first_name: value.first_name, + } + } +} + impl User { #[must_use] pub fn new>( @@ -857,9 +823,9 @@ impl User { { query_as!( Self, - "SELECT 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, openid_sub, \ + "SELECT 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, openid_sub, \ from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ FROM \"user\" WHERE username = $1", username @@ -877,9 +843,10 @@ impl User { { query_as!( Self, - "SELECT 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, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + "SELECT 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, openid_sub, from_ldap, \ + ldap_pass_randomized, ldap_rdn, ldap_user_path \ FROM \"user\" WHERE email ILIKE $1", email ) @@ -887,8 +854,7 @@ impl User { .await } - /// Attempts to find user by username and then by email - /// of none is initially found + /// Attempts to find user by username and then by email, if none is initially found. pub async fn find_by_username_or_email( conn: &mut PgConnection, username_or_email: &str, @@ -922,7 +888,6 @@ impl User { .await } - // FIXME: Remove `LIMIT 1` when `openid_sub` is unique. pub(crate) async fn find_by_sub<'e, E>( executor: E, sub: &str, @@ -936,7 +901,7 @@ impl User { mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ - FROM \"user\" WHERE openid_sub = $1 LIMIT 1", + FROM \"user\" WHERE openid_sub = $1", sub ) .fetch_optional(executor) @@ -1308,14 +1273,14 @@ impl Distribution> for Standard { #[cfg(test)] mod test { + use defguard_common::{ + config::{DefGuardConfig, SERVER_CONFIG}, + db::setup_pool, + }; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use crate::{ - SERVER_CONFIG, - config::DefGuardConfig, - db::{models::settings::initialize_current_settings, setup_pool}, - }; + use defguard_common::db::models::settings::initialize_current_settings; #[sqlx::test] async fn test_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/db/models/webauthn.rs b/crates/defguard_core/src/db/models/webauthn.rs index 64a2c794c9..9b4407d67f 100644 --- a/crates/defguard_core/src/db/models/webauthn.rs +++ b/crates/defguard_core/src/db/models/webauthn.rs @@ -2,8 +2,7 @@ use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query, query_as, query_scalar}; use webauthn_rs::prelude::Passkey; -use super::error::ModelError; -use crate::db::{Id, NoId}; +use defguard_common::db::{Id, NoId, models::ModelError}; #[derive(Model, Clone, Debug)] pub struct WebAuthn { diff --git a/crates/defguard_core/src/db/models/webhook.rs b/crates/defguard_core/src/db/models/webhook.rs index fb7a83dd5b..f45d9831d7 100644 --- a/crates/defguard_core/src/db/models/webhook.rs +++ b/crates/defguard_core/src/db/models/webhook.rs @@ -2,7 +2,7 @@ use model_derive::Model; use sqlx::{Error as SqlxError, FromRow, PgPool, query_as}; use super::UserInfo; -use crate::db::{Id, NoId}; +use defguard_common::db::{Id, NoId}; /// App events which triggers webhook action #[derive(Debug)] diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs index 4c486ffa30..fef77250f3 100644 --- a/crates/defguard_core/src/db/models/wireguard.rs +++ b/crates/defguard_core/src/db/models/wireguard.rs @@ -7,6 +7,11 @@ use std::{ use base64::prelude::{BASE64_STANDARD, Engine}; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use defguard_common::{ + auth::claims::{Claims, ClaimsType}, + csv::AsCsv, + db::{Id, NoId, models::ModelError}, +}; use ipnetwork::{IpNetwork, IpNetworkError, NetworkSize}; use model_derive::Model; use rand::rngs::OsRng; @@ -24,23 +29,18 @@ use super::{ device::{ Device, DeviceError, DeviceInfo, DeviceNetworkInfo, DeviceType, WireguardNetworkDevice, }, - error::ModelError, user::User, wireguard_peer_stats::WireguardPeerStats, }; use crate::{ - AsCsv, - auth::{Claims, ClaimsType}, - db::{Id, NoId}, enterprise::firewall::FirewallError, - grpc::{ - gateway::{Peer, send_multiple_wireguard_events, state::GatewayState}, - proto::{ - enterprise::firewall::FirewallConfig, proxy::LocationMfaMode as ProtoLocationMfaMode, - }, - }, + grpc::gateway::{send_multiple_wireguard_events, state::GatewayState}, wg_config::ImportedDevice, }; +use defguard_proto::{ + enterprise::firewall::FirewallConfig, gateway::Peer, + proxy::LocationMfaMode as ProtoLocationMfaMode, +}; pub const DEFAULT_KEEPALIVE_INTERVAL: i32 = 25; pub const DEFAULT_DISCONNECT_THRESHOLD: i32 = 300; @@ -1419,11 +1419,12 @@ mod test { use std::str::FromStr; use chrono::{SubsecRound, TimeDelta}; + use defguard_common::db::setup_pool; use matches::assert_matches; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use crate::db::{Group, setup_pool}; + use crate::db::Group; #[sqlx::test] async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/db/models/wireguard_peer_stats.rs b/crates/defguard_core/src/db/models/wireguard_peer_stats.rs index 7a2f7322e8..2323584c7d 100644 --- a/crates/defguard_core/src/db/models/wireguard_peer_stats.rs +++ b/crates/defguard_core/src/db/models/wireguard_peer_stats.rs @@ -6,7 +6,7 @@ use ipnetwork::IpNetwork; use model_derive::Model; use sqlx::{PgExecutor, PgPool, query, query_as, query_scalar}; -use crate::db::{Id, NoId}; +use defguard_common::db::{Id, NoId}; #[derive(Debug, Deserialize, Model, Serialize)] #[table(wireguard_peer_stats)] diff --git a/crates/defguard_core/src/db/models/yubikey.rs b/crates/defguard_core/src/db/models/yubikey.rs index 1535297078..0813319334 100644 --- a/crates/defguard_core/src/db/models/yubikey.rs +++ b/crates/defguard_core/src/db/models/yubikey.rs @@ -1,7 +1,7 @@ use model_derive::Model; use sqlx::{PgExecutor, query, query_as}; -use crate::db::{Id, NoId}; +use defguard_common::db::{Id, NoId}; #[derive(Deserialize, Model, Serialize)] pub struct YubiKey { diff --git a/crates/defguard_core/src/enterprise/activity_log_stream/http_stream.rs b/crates/defguard_core/src/enterprise/activity_log_stream/http_stream.rs index d835607f83..64593c021a 100644 --- a/crates/defguard_core/src/enterprise/activity_log_stream/http_stream.rs +++ b/crates/defguard_core/src/enterprise/activity_log_stream/http_stream.rs @@ -2,16 +2,14 @@ use std::sync::Arc; use base64::prelude::{BASE64_STANDARD, Engine}; use bytes::Bytes; +use defguard_common::secret::SecretStringWrapper; use reqwest::tls; use tokio::sync::broadcast::Receiver; use tokio_util::sync::CancellationToken; use tracing::{debug, error}; -use crate::{ - enterprise::db::models::activity_log_stream::{ - LogstashHttpActivityLogStream, VectorHttpActivityLogStream, - }, - secret::SecretStringWrapper, +use crate::enterprise::db::models::activity_log_stream::{ + LogstashHttpActivityLogStream, VectorHttpActivityLogStream, }; /// Spawns an asynchronous task that reads activity log events from the channel and sends them as NDJSON via HTTP. diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index 60e781b47c..faddd5c08f 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -6,6 +6,7 @@ use std::{ }; use chrono::NaiveDateTime; +use defguard_common::db::{Id, NoId}; use ipnetwork::{IpNetwork, IpNetworkError}; use model_derive::Model; use sqlx::{ @@ -17,10 +18,7 @@ use thiserror::Error; use crate::{ DeviceType, appstate::AppState, - db::{ - Device, GatewayEvent, Group, Id, NoId, User, WireguardNetwork, - models::wireguard::LocationMfaMode, - }, + db::{Device, GatewayEvent, Group, User, WireguardNetwork, models::wireguard::LocationMfaMode}, enterprise::{ firewall::FirewallError, handlers::acl::{ApiAclAlias, ApiAclRule, EditAclAlias, EditAclRule}, diff --git a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs index ca828a4238..becdc26481 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs @@ -1,10 +1,11 @@ use std::ops::Bound; +use defguard_common::db::setup_pool; use rand::{Rng, thread_rng}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; -use crate::{db::setup_pool, handlers::wireguard::parse_address_list}; +use crate::handlers::wireguard::parse_address_list; #[sqlx::test] async fn test_alias(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/enterprise/db/models/activity_log_stream.rs b/crates/defguard_core/src/enterprise/db/models/activity_log_stream.rs index 9bfb86676a..2d472bec2c 100644 --- a/crates/defguard_core/src/enterprise/db/models/activity_log_stream.rs +++ b/crates/defguard_core/src/enterprise/db/models/activity_log_stream.rs @@ -3,9 +3,9 @@ use serde::Serialize; use sqlx::{Error as SqlxError, FromRow, PgExecutor, Type, query_as}; use strum_macros::{Display, EnumString}; -use crate::{ +use crate::enterprise::activity_log_stream::error::ActivityLogStreamError; +use defguard_common::{ db::{Id, NoId}, - enterprise::activity_log_stream::error::ActivityLogStreamError, secret::SecretStringWrapper, }; diff --git a/crates/defguard_core/src/enterprise/db/models/api_tokens.rs b/crates/defguard_core/src/enterprise/db/models/api_tokens.rs index ef1ffebfd4..9e01c7248c 100644 --- a/crates/defguard_core/src/enterprise/db/models/api_tokens.rs +++ b/crates/defguard_core/src/enterprise/db/models/api_tokens.rs @@ -2,7 +2,7 @@ use chrono::NaiveDateTime; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, query_as}; -use crate::db::{Id, NoId}; +use defguard_common::db::{Id, NoId}; #[derive(Clone, Debug, Deserialize, Model, Serialize)] #[table(api_token)] diff --git a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs index 70fa182087..1d6a563268 100644 --- a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs +++ b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs @@ -3,7 +3,7 @@ use std::fmt; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, Type, query, query_as}; -use crate::db::{Id, NoId}; +use defguard_common::db::{Id, NoId}; // The behavior when a user is deleted from the directory // Keep: Keep the user, despite being deleted from the external provider's directory diff --git a/crates/defguard_core/src/enterprise/db/models/snat.rs b/crates/defguard_core/src/enterprise/db/models/snat.rs index 127da55bf9..68b7b71c56 100644 --- a/crates/defguard_core/src/enterprise/db/models/snat.rs +++ b/crates/defguard_core/src/enterprise/db/models/snat.rs @@ -5,10 +5,8 @@ use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, query_as}; use utoipa::ToSchema; -use crate::{ - db::{Id, NoId}, - enterprise::snat::error::UserSnatBindingError, -}; +use crate::enterprise::snat::error::UserSnatBindingError; +use defguard_common::db::{Id, NoId}; #[derive(Clone, Debug, Deserialize, Model, Serialize, ToSchema)] #[table(user_snat_binding)] diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index 78e25f8a96..70ddd638ce 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -3,6 +3,7 @@ use std::{ time::Duration, }; +use defguard_common::db::Id; use paste::paste; use reqwest::header::AUTHORIZATION; use sqlx::{PgPool, error::Error as SqlxError}; @@ -16,7 +17,7 @@ use super::{ ldap::utils::ldap_update_users_state, }; use crate::{ - db::{GatewayEvent, Group, Id, User}, + db::{GatewayEvent, Group, User}, enterprise::{ db::models::openid_provider::DirectorySyncUserBehavior, ldap::utils::{ldap_add_users_to_groups, ldap_delete_users, ldap_remove_users_from_groups}, diff --git a/crates/defguard_core/src/enterprise/directory_sync/tests.rs b/crates/defguard_core/src/enterprise/directory_sync/tests.rs index 54df5c6a2c..d6ae4f4beb 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/tests.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/tests.rs @@ -2,6 +2,13 @@ mod test { use std::str::FromStr; + use defguard_common::{ + config::{DefGuardConfig, SERVER_CONFIG}, + db::{ + models::{Settings, settings::initialize_current_settings}, + setup_pool, + }, + }; use ipnetwork::IpNetwork; use secrecy::ExposeSecret; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; @@ -9,15 +16,9 @@ mod test { use super::super::*; use crate::{ - SERVER_CONFIG, - config::DefGuardConfig, db::{ - Device, Session, SessionState, Settings, WireguardNetwork, - models::{ - device::DeviceType, settings::initialize_current_settings, - wireguard::LocationMfaMode, - }, - setup_pool, + Device, Session, SessionState, WireguardNetwork, + models::{device::DeviceType, wireguard::LocationMfaMode}, }, enterprise::db::models::openid_provider::DirectorySyncTarget, }; diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index 4611be8acf..a87f9110ac 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -3,6 +3,7 @@ use std::{ ops::RangeInclusive, }; +use defguard_common::db::{Id, models::ModelError}; use ipnetwork::IpNetwork; use sqlx::{Error as SqlxError, PgConnection, query_as, query_scalar}; @@ -14,16 +15,16 @@ use super::{ utils::merge_ranges, }; use crate::{ - db::{Device, Id, User, WireguardNetwork, models::error::ModelError}, + db::{Device, User, WireguardNetwork}, enterprise::{ db::models::{acl::AliasKind, snat::UserSnatBinding}, is_enterprise_enabled, }, - grpc::proto::enterprise::firewall::{ - FirewallConfig, FirewallPolicy, FirewallRule, IpAddress, IpRange, IpVersion, Port, - PortRange as PortRangeProto, SnatBinding as SnatBindingProto, ip_address::Address, - port::Port as PortInner, - }, +}; +use defguard_proto::enterprise::firewall::{ + FirewallConfig, FirewallPolicy, FirewallRule, IpAddress, IpRange, IpVersion, Port, + PortRange as PortRangeProto, SnatBinding as SnatBindingProto, ip_address::Address, + port::Port as PortInner, }; #[derive(Debug, thiserror::Error)] diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index d286e4a1c2..0785fb85fc 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -1,6 +1,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use chrono::{DateTime, NaiveDateTime}; +use defguard_common::db::{Id, NoId, setup_pool}; use ipnetwork::{IpNetwork, Ipv6Network}; use rand::{Rng, thread_rng}; use sqlx::{ @@ -15,9 +16,8 @@ use super::{ }; use crate::{ db::{ - Device, Group, Id, NoId, User, WireguardNetwork, + Device, Group, User, WireguardNetwork, models::device::{DeviceType, WireguardNetworkDevice}, - setup_pool, }, enterprise::{ db::models::acl::{ @@ -26,10 +26,10 @@ use crate::{ }, firewall::{get_source_addrs, get_source_network_devices}, }, - grpc::proto::enterprise::firewall::{ - FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, - ip_address::Address, port::Port as PortInner, - }, +}; +use defguard_proto::enterprise::firewall::{ + FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, + ip_address::Address, port::Port as PortInner, }; impl Default for AclRuleDestinationRange { diff --git a/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs b/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs index 069d1fd86d..eeb022224f 100644 --- a/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs +++ b/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs @@ -10,10 +10,10 @@ use crate::{ events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent}, grpc::{ client_mfa::{ClientLoginSession, ClientMfaServer}, - proto::proxy::{ClientMfaOidcAuthenticateRequest, DeviceInfo, MfaMethod}, utils::parse_client_info, }, }; +use defguard_proto::proxy::{ClientMfaOidcAuthenticateRequest, DeviceInfo, MfaMethod}; impl ClientMfaServer { #[instrument(skip_all)] @@ -76,7 +76,7 @@ impl ClientMfaServer { let code = AuthorizationCode::new(request.code.clone()); let url = match Url::parse(&request.callback_url).map_err(|err| { - error!("Invalid redirect URL provided: {err:?}"); + error!("Invalid redirect URL provided: {err}"); Status::invalid_argument("invalid redirect URL") }) { Ok(url) => url, @@ -122,7 +122,7 @@ impl ClientMfaServer { ); } Err(err) => { - info!("Failed to verify OIDC code: {err:?}"); + info!("Failed to verify OIDC code: {err}"); self.sessions.remove(&pubkey); self.emit_event(BidiStreamEvent { context, @@ -131,7 +131,7 @@ impl ClientMfaServer { location: location.clone(), device: device.clone(), method, - message: format!("failed to verify OIDC code: {err:?}"), + message: format!("failed to verify OIDC code: {err}"), }, )), })?; diff --git a/crates/defguard_core/src/enterprise/grpc/polling.rs b/crates/defguard_core/src/enterprise/grpc/polling.rs index b7c7eacef7..782ca70fab 100644 --- a/crates/defguard_core/src/enterprise/grpc/polling.rs +++ b/crates/defguard_core/src/enterprise/grpc/polling.rs @@ -1,14 +1,13 @@ +use defguard_common::db::Id; use sqlx::PgPool; use tonic::Status; use crate::{ - db::{Device, Id, User, models::polling_token::PollingToken}, + db::{Device, User, models::polling_token::PollingToken}, enterprise::is_enterprise_enabled, - grpc::{ - proto::proxy::{InstanceInfoRequest, InstanceInfoResponse}, - utils::build_device_config_response, - }, + grpc::utils::build_device_config_response, }; +use defguard_proto::proxy::{InstanceInfoRequest, InstanceInfoResponse}; pub struct PollingServer { pool: PgPool, diff --git a/crates/defguard_core/src/enterprise/handlers/acl.rs b/crates/defguard_core/src/enterprise/handlers/acl.rs index d2ce367206..953550e1cb 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl.rs @@ -4,13 +4,13 @@ use axum::{ http::StatusCode, }; use chrono::NaiveDateTime; +use defguard_common::db::Id; use serde_json::{Value, json}; use super::LicenseInfo; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, - db::Id, enterprise::db::models::acl::{ AclAlias, AclAliasInfo, AclRule, AclRuleInfo, AliasKind, AliasState, Protocol, RuleState, }, diff --git a/crates/defguard_core/src/enterprise/handlers/activity_log_stream.rs b/crates/defguard_core/src/enterprise/handlers/activity_log_stream.rs index feca06d5b1..f125fddf5e 100644 --- a/crates/defguard_core/src/enterprise/handlers/activity_log_stream.rs +++ b/crates/defguard_core/src/enterprise/handlers/activity_log_stream.rs @@ -9,13 +9,13 @@ use super::LicenseInfo; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, - db::{Id, NoId}, enterprise::db::models::activity_log_stream::{ ActivityLogStream, ActivityLogStreamConfig, ActivityLogStreamType, }, events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{ApiResponse, ApiResult}, }; +use defguard_common::db::{Id, NoId}; pub async fn get_activity_log_stream( _admin: AdminRole, diff --git a/crates/defguard_core/src/enterprise/handlers/api_tokens.rs b/crates/defguard_core/src/enterprise/handlers/api_tokens.rs index d76a3488ea..12132fbc7e 100644 --- a/crates/defguard_core/src/enterprise/handlers/api_tokens.rs +++ b/crates/defguard_core/src/enterprise/handlers/api_tokens.rs @@ -15,8 +15,8 @@ use crate::{ error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{ApiResponse, ApiResult, user_for_admin_or_self}, - random::gen_alphanumeric, }; +use defguard_common::random::gen_alphanumeric; const API_TOKEN_LENGTH: usize = 32; diff --git a/crates/defguard_core/src/enterprise/handlers/openid_login.rs b/crates/defguard_core/src/enterprise/handlers/openid_login.rs index 0eadebbe0c..6b506f5223 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_login.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_login.rs @@ -9,6 +9,13 @@ use axum_extra::{ headers::UserAgent, }; use base64::{Engine, prelude::BASE64_STANDARD}; +use defguard_common::{ + config::server_config, + db::{ + Id, + models::{Settings, settings::OpenidUsernameHandling}, + }, +}; use openidconnect::{ AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl, Nonce, OAuth2TokenResponse, RedirectUrl, Scope, @@ -29,7 +36,7 @@ pub(crate) const SELECT_ACCOUNT_SUPPORTED_PROVIDERS: &[&str] = &["Google"]; use super::LicenseInfo; use crate::{ appstate::AppState, - db::{Id, Settings, User, models::settings::OpenidUsernameHandling}, + db::User, enterprise::{ db::models::openid_provider::OpenIdProvider, directory_sync::sync_user_groups_if_configured, ldap::utils::ldap_update_user_state, @@ -37,10 +44,10 @@ use crate::{ }, error::WebError, handlers::{ - ApiResponse, AuthResponse, SESSION_COOKIE_NAME, SIGN_IN_COOKIE_NAME, auth::create_session, - user::check_username, + ApiResponse, AuthResponse, SESSION_COOKIE_NAME, SIGN_IN_COOKIE_NAME, + auth::create_session, + user::{MAX_USERNAME_CHARS, check_username}, }, - server_config, }; /// Prune the given username from illegal characters in accordance with the following rules: @@ -84,8 +91,7 @@ pub fn prune_username(username: &str, handling: OpenidUsernameHandling) -> Strin } } - result.truncate(64); - + result.truncate(MAX_USERNAME_CHARS); result } @@ -95,7 +101,7 @@ fn get_async_http_client() -> Result { .redirect(reqwest::redirect::Policy::none()) .build() .map_err(|err| { - error!("Failed to create HTTP client: {err:?}"); + error!("Failed to create HTTP client: {err}"); WebError::Http(StatusCode::INTERNAL_SERVER_ERROR) }) } @@ -103,22 +109,24 @@ fn get_async_http_client() -> Result { async fn get_provider_metadata(url: &str) -> Result { let issuer_url = IssuerUrl::new(url.to_string()).map_err(|err| { WebError::BadRequest(format!( - "Failed to create issuer URL from the provided URL: {url}. Error details: {err:?}", + "Failed to create issuer URL from the provided URL: {url}. Error details: {err}", )) })?; let async_http_client = get_async_http_client()?; - // Discover the provider metadata based on a known base issuer URL - // The url should be in the form of e.g. https://accounts.google.com - // The url shouldn't contain a .well-known part, it will be added automatically + // Discover the provider metadata based on a known base issuer URL. + // The URL should be in the form of e.g. https://accounts.google.com. + // The URL shouldn't contain the ".well-known" part – it will be added automatically. match CoreProviderMetadata::discover_async(issuer_url, &async_http_client).await { Ok(provider_metadata) => Ok(provider_metadata), Err(err) => Err(WebError::Authorization(format!( - "Failed to discover provider metadata, make sure the provider's URL is correct: {url}. Error details: {err:?}", + "Failed to discover provider metadata, make sure the provider's URL is correct: {url}. \ + Error details: {err}", ))), } } -/// Build a state with optional embedded data. Useful for passing additional information around the authentication flow. +/// Build a state with optional embedded data. Useful for passing additional information around the +/// authentication flow. pub(crate) fn build_state(state_data: Option) -> CsrfToken { let csrf_token = CsrfToken::new_random(); if let Some(data) = state_data { @@ -196,7 +204,7 @@ pub(crate) async fn user_from_claims( let token_response = match core_client .exchange_code(code) .map_err(|err| { - error!("Failed to exchange code for ID token: {err:?}"); + error!("Failed to exchange code for ID token: {err}"); WebError::Http(StatusCode::INTERNAL_SERVER_ERROR) })? .request_async(&async_http_client) @@ -205,7 +213,7 @@ pub(crate) async fn user_from_claims( Ok(token) => token, Err(err) => { return Err(WebError::Authorization(format!( - "Failed to exchange code for ID token; OpenID provider error: {err:?}" + "Failed to exchange code for ID token; OpenID provider error: {err}" ))); } }; @@ -243,7 +251,7 @@ pub(crate) async fn user_from_claims( client_id.as_str(), audiences .iter() - .map(|aud| aud.to_string()) + .map(|aud| aud.as_str()) .collect::>() .join(", ") ))); @@ -254,7 +262,7 @@ pub(crate) async fn user_from_claims( audiences .iter() .filter(|&aud| **aud != *client_id) - .map(|aud| aud.to_string()) + .map(|aud| aud.as_str()) .collect::>() .join(", ") ); @@ -268,26 +276,6 @@ pub(crate) async fn user_from_claims( .to_string(), ))?; - // Try to get the username from the preferred_username claim. - // If it's not there, extract it from email. - let username = if let Some(username) = token_claims.preferred_username() { - debug!("Preferred username {username:?} found in the claims, extracting username from it."); - username - } else { - debug!("Preferred username not found in the claims, extracting from email address."); - // Extract the username from the email address - let username = email.split('@').next().ok_or(WebError::BadRequest( - "Failed to extract username from email address".to_string(), - ))?; - debug!("Username extracted from email ({email:?}): {username})"); - username - }; - let settings = Settings::get_current_settings(); - - let username = prune_username(username, settings.openid_username_handling); - // Check if the username is valid just in case, not everything can be handled by the pruning. - check_username(&username)?; - // Get the *sub* claim from the token. let sub = token_claims.subject().to_string(); @@ -314,7 +302,7 @@ pub(crate) async fn user_from_claims( debug!("User {} tried to log in, but is disabled", user.username); return Err(WebError::Authorization("User is disabled".into())); } - // User with the same email already exists, merge the accounts + // User with the same email already exists, merge the accounts. info!( "User with email address {} is logging in through OpenID Connect for the \ first time and we've found an existing account with the same email \ @@ -325,7 +313,8 @@ pub(crate) async fn user_from_claims( user.save(pool).await?; user } else { - // Check if the user should be created if they don't exist (default: true) + let settings = Settings::get_current_settings(); + // Check if the user should be created, if doesn't exist (default: true). if !settings.openid_create_account { warn!( "User with email address {} is trying to log in through OpenID Connect \ @@ -340,6 +329,31 @@ pub(crate) async fn user_from_claims( )); } + // Try to get the username from `preferred_username` claim. + // If it's not there, extract it from email. + let username = if let Some(username) = token_claims.preferred_username() { + let username = username.as_str(); + debug!( + "Preferred username {username} found in the claims. Using the username." + ); + username + } else { + debug!( + "Preferred username not found in the claims, extracting from email address." + ); + // Extract the username from the email address + let username = email.split('@').next().ok_or(WebError::BadRequest( + "Failed to extract username from email address".to_string(), + ))?; + debug!("Username extracted from email ({email:?}): {username})"); + username + }; + + let username = prune_username(username, settings.openid_username_handling); + // Check if the username is valid just in case, not everything can be handled by the + // pruning. + check_username(&username)?; + info!( "User {username} is logging in through OpenID Connect for the first time and \ there is no account with the same email address ({}). Creating a new account.", @@ -352,11 +366,11 @@ pub(crate) async fn user_from_claims( ))); } - // Extract all necessary information from the token or call the userinfo endpoint + // Extract all necessary information from the token or call the userinfo endpoint. let given_name = token_claims .given_name() - // 'None' gets you the default value from a localized claim. - // Otherwise you would need to pass a locale. + // `None` gets the default value from a localized claim. + // Otherwise, it is required to pass a locale. .and_then(|claim| claim.get(None)); let family_name = token_claims.family_name().and_then(|claim| claim.get(None)); let phone = token_claims.phone_number(); @@ -372,47 +386,42 @@ pub(crate) async fn user_from_claims( (given_name, family_name, phone) } else { debug!( - "Given name or family name not found in the claims for user {username}, trying to get them \ - from the user info endpoint. Current values: given_name: {given_name:?}, family_name: {family_name:?}, phone: {phone:?}" + "Given name or family name not found in the claims for user {username}, \ + trying to get them from the user info endpoint. Current values: \ + given_name: {given_name:?}, family_name: {family_name:?}, phone: {phone:?}" ); let async_http_client = get_async_http_client()?; - let retrieval_error = "Failed to retrieve given name and family name from provider's userinfo endpoint. \ - Make sure you have configured your provider correctly and that you have granted the \ - necessary permissions to retrieve such information from the token or the userinfo endpoint."; + let retrieval_error = "Failed to retrieve given name and family name from \ + provider's userinfo endpoint. Make sure you have configured your provider \ + correctly and that you have granted the necessary permissions to retrieve \ + such information from the token or the userinfo endpoint."; userinfo_response = core_client .user_info(access_token.clone(), Some(token_claims.subject().clone())) - .map_err( - |err| { - error!( - "Failed to get family name and given name from provider's userinfo endpoint, they may not support this. Error details: {err:?}", - ); - - WebError::BadRequest( - retrieval_error.into(), - ) - } - )? + .map_err(|err| { + error!( + "Failed to get family name and given name from provider's \ + userinfo endpoint, they may not support this. Error details: {err}" + ); + WebError::BadRequest(retrieval_error.into()) + })? .request_async(&async_http_client) .await - .map_err( - |err| { - error!( - "Failed to get family name and given name from provider's userinfo endpoint. Error details: {err:?}", - ); - - WebError::BadRequest( - retrieval_error.into(), - ) - } - )?; + .map_err(|err| { + error!( + "Failed to get family name and given name from provider's userinfo \ + endpoint. Error details: {err}", + ); + WebError::BadRequest(retrieval_error.into()) + })?; let claim_error = |claim_name: &str| { format!( - "Failed to retrieve {claim_name} from provider's userinfo endpoint and the ID token. \ - Make sure you have configured your provider correctly and that you have \ - granted the necessary permissions to retrieve such information from the token or the userinfo endpoint.", + "Failed to retrieve {claim_name} from provider's userinfo endpoint and \ + the ID token. Make sure you have configured your provider correctly \ + and that you have granted the necessary permissions to retrieve such \ + information from the token or the userinfo endpoint.", ) }; let given_name = userinfo_response @@ -426,7 +435,8 @@ pub(crate) async fn user_from_claims( let phone = userinfo_response.phone_number(); debug!( - "Given name and family name successfully retrieved from the user info endpoint for user {username}." + "Given name and family name successfully retrieved from the user info \ + endpoint for user {username}." ); (given_name, family_name, phone) @@ -511,10 +521,7 @@ pub(crate) async fn get_auth_info( private_cookies, ApiResponse { json: json!( - { - "url": authorize_url, - "button_display_name": provider.display_name - } + {"url": authorize_url, "button_display_name": provider.display_name} ), status: StatusCode::OK, }, @@ -591,14 +598,16 @@ pub(crate) async fn auth_callback( .max_age(max_age); let cookies = cookies.add(auth_cookie); - // The user may not be yet authorized (pre-MFA) but syncing their groups should be fine here, since he already managed to login through the provider. - // There is currently no other way to sync the groups for the MFA enabled user logging in through the provider without firing it - // on every login attempt, even for standard, non-provider users. + // The user may not yet be authorized (pre-MFA), but syncing their groups should be fine here, + // since he already managed to login through the provider. Currently, there is no other way to + // sync the groups for the MFA enabled user logging in through the provider without firing it on + // every login attempt, even for standard, non-provider users. if let Err(err) = sync_user_groups_if_configured(&user, &appstate.pool, &appstate.wireguard_tx).await { error!( - "Failed to sync user groups for user {} with the directory while the user was trying to login in through an external provider: {err:?}", + "Failed to sync user groups for user {} with the directory while the user was trying \ + to login in through an external provider: {err}", user.username ); } else { diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index 51d300116f..565e573913 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -3,6 +3,10 @@ use axum::{ extract::{Path, State}, http::StatusCode, }; +use defguard_common::db::models::{ + Settings, + settings::{OpenidUsernameHandling, update_current_settings}, +}; use rsa::{RsaPrivateKey, pkcs8::DecodePrivateKey}; use serde_json::json; @@ -10,13 +14,7 @@ use super::LicenseInfo; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, - db::{ - Settings, WireguardNetwork, - models::{ - settings::{OpenidUsernameHandling, update_current_settings}, - wireguard::LocationMfaMode, - }, - }, + db::{WireguardNetwork, models::wireguard::LocationMfaMode}, enterprise::{ db::models::openid_provider::OpenIdProvider, directory_sync::test_directory_sync_connection, }, @@ -24,7 +22,7 @@ use crate::{ handlers::{ApiResponse, ApiResult}, }; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Deserialize, Serialize)] pub struct AddProviderData { pub name: String, pub base_url: String, @@ -47,7 +45,7 @@ pub struct AddProviderData { pub jumpcloud_api_key: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Deserialize, Serialize)] pub struct DeleteProviderData { name: String, } @@ -66,26 +64,29 @@ pub async fn add_openid_provider( ); let current_provider = OpenIdProvider::get_current(&appstate.pool).await?; - // The key is sent from the frontend only when user explicitly changes it, as we never send it back. - // Check if the thing received from the frontend is a valid RSA private key (signaling user intent to change key) - // or is it just some empty string or other junk. + // The key is sent from the frontend only when user explicitly changes it, as we never send it + // back. Check if the thing received from the frontend is a valid RSA private key (signaling + // user intent to change key) or is it just some empty string or other junk. let private_key = match &provider_data.google_service_account_key { Some(key) => { if RsaPrivateKey::from_pkcs8_pem(key).is_ok() { debug!( - "User {} provided a valid RSA private key for provider's directory sync, using it", + "User {} provided a valid RSA private key for provider's directory sync. Using \ + it.", session.user.username ); provider_data.google_service_account_key.clone() } else if let Some(provider) = ¤t_provider { debug!( - "User {} did not provide a valid RSA private key for provider's directory sync or the key did not change, using the existing key", + "User {} did not provide a valid RSA private key for provider's directory sync \ + or the key did not change. Using the existing key", session.user.username ); provider.google_service_account_key.clone() } else { warn!( - "User {} did not provide a valid RSA private key for provider's directory sync", + "User {} did not provide a valid RSA private key for provider's directory \ + sync.", session.user.username ); None @@ -98,19 +99,22 @@ pub async fn add_openid_provider( Some(key) => { if serde_json::from_str::(key).is_ok() { debug!( - "User {} provided a valid JWK private key for provider's Okta directory sync, using it", + "User {} provided a valid JWK private key for provider's Okta directory sync. \ + Using it.", session.user.username ); provider_data.okta_private_jwk.clone() } else if let Some(provider) = ¤t_provider { debug!( - "User {} did not provide a valid JWK private key for provider's Okta directory sync or the key did not change, using the existing key", + "User {} did not provide a valid JWK private key for provider's Okta directory \ + sync or the key did not change. Using the existing key.", session.user.username ); provider.okta_private_jwk.clone() } else { warn!( - "User {} did not provide a valid JWK private key for provider's Okta directory sync", + "User {} did not provide a valid JWK private key for provider's Okta directory \ + sync.", session.user.username ); None @@ -126,7 +130,7 @@ pub async fn add_openid_provider( let group_match = if let Some(group_match) = provider_data.directory_sync_group_match { if group_match.is_empty() { - vec![] + Vec::new() } else { group_match .split(',') @@ -134,7 +138,7 @@ pub async fn add_openid_provider( .collect() } } else { - vec![] + Vec::new() }; // Currently, we only support one OpenID provider at a time @@ -190,7 +194,8 @@ pub async fn get_current_openid_provider( Ok(ApiResponse { json: json!({ "provider": json!(provider), - "settings": json!({ "create_account": settings.openid_create_account, "username_handling": settings.openid_username_handling }), + "settings": json!({"create_account": settings.openid_create_account, + "username_handling": settings.openid_username_handling}), }), status: StatusCode::OK, }) @@ -198,7 +203,8 @@ pub async fn get_current_openid_provider( None => Ok(ApiResponse { json: json!({ "provider": null, - "settings": json!({ "create_account": settings.openid_create_account, "username_handling": settings.openid_username_handling }), + "settings": json!({"create_account": settings.openid_create_account, + "username_handling": settings.openid_username_handling}), }), status: StatusCode::NO_CONTENT, }), @@ -229,7 +235,8 @@ pub async fn delete_openid_provider( // fall back to internal MFA in all relevant locations for mut location in locations { debug!( - "Falling back to internal MFA for {location} because exteral OIDC provider has been removed" + "Falling back to internal MFA for {location} because exteral OIDC provider has \ + been removed" ); location.location_mfa_mode = LocationMfaMode::Internal; location.save(&mut *transaction).await?; @@ -326,7 +333,7 @@ pub async fn test_dirsync_connection( session.user.username, err ); return Ok(ApiResponse { - json: json!({ "message": err.to_string(), "success": false }), + json: json!({"message": err.to_string(), "success": false}), status: StatusCode::OK, }); } @@ -335,7 +342,7 @@ pub async fn test_dirsync_connection( session.user.username ); Ok(ApiResponse { - json: json!({ "message": "Connection successful", "success": true }), + json: json!({"message": "Connection successful", "success": true}), status: StatusCode::OK, }) } diff --git a/crates/defguard_core/src/enterprise/ldap/client.rs b/crates/defguard_core/src/enterprise/ldap/client.rs index 6e029ac291..36a5f1b0d9 100644 --- a/crates/defguard_core/src/enterprise/ldap/client.rs +++ b/crates/defguard_core/src/enterprise/ldap/client.rs @@ -10,10 +10,8 @@ use ldap3::{ }; use super::error::LdapError; -use crate::{ - db::{Settings, User}, - enterprise::ldap::model::extract_rdn_value, -}; +use crate::{db::User, enterprise::ldap::model::extract_rdn_value}; +use defguard_common::db::models::Settings; impl super::LDAPConnection { pub(crate) async fn create() -> Result { diff --git a/crates/defguard_core/src/enterprise/ldap/hash.rs b/crates/defguard_core/src/enterprise/ldap/hash.rs index a5b47dbb8e..ddb6d4c8d6 100644 --- a/crates/defguard_core/src/enterprise/ldap/hash.rs +++ b/crates/defguard_core/src/enterprise/ldap/hash.rs @@ -1,4 +1,5 @@ use base64::Engine; +use defguard_common::hex::to_lower_hex; use md4::Md4; use rand::{RngCore, rngs::OsRng}; use sha1::{ @@ -6,8 +7,6 @@ use sha1::{ digest::generic_array::{GenericArray, sequence::Concat}, }; -use crate::hex::to_lower_hex; - /// Calculate salted SHA1 hash from given password in SSHA password storage scheme. #[must_use] pub fn salted_sha1_hash(password: &str) -> String { diff --git a/crates/defguard_core/src/enterprise/ldap/mod.rs b/crates/defguard_core/src/enterprise/ldap/mod.rs index c4ab28da63..17152ee73d 100644 --- a/crates/defguard_core/src/enterprise/ldap/mod.rs +++ b/crates/defguard_core/src/enterprise/ldap/mod.rs @@ -1,16 +1,23 @@ use std::{collections::HashSet, future::Future}; +use defguard_common::db::{ + Id, + models::{ + Settings, + settings::{LdapSyncStatus, update_current_settings}, + }, +}; #[cfg(not(test))] use ldap3::Ldap; use ldap3::{Mod, SearchEntry, ldap_escape}; use model::UserObjectClass; use rand::Rng; use sqlx::PgPool; -use sync::{SyncStatus, get_ldap_sync_status, is_ldap_desynced, set_ldap_sync_status}; +use sync::{get_ldap_sync_status, is_ldap_desynced, set_ldap_sync_status}; use self::error::LdapError; use crate::{ - db::{self, Id, Settings, User, models::settings::update_current_settings}, + db::{self, User}, enterprise::{is_enterprise_enabled, ldap::model::extract_dn_path, limits::update_counts}, }; @@ -36,8 +43,8 @@ pub(crate) async fn do_ldap_sync(pool: &PgPool) -> Result<(), LdapError> { // doesn't matter for the sync status if we can't pull changes. if !settings.ldap_enabled { debug!("LDAP is disabled, not performing LDAP sync"); - if get_ldap_sync_status() == SyncStatus::InSync { - set_ldap_sync_status(SyncStatus::OutOfSync, pool).await?; + if get_ldap_sync_status() == LdapSyncStatus::InSync { + set_ldap_sync_status(LdapSyncStatus::OutOfSync, pool).await?; } return Ok(()); } @@ -68,16 +75,16 @@ pub(crate) async fn do_ldap_sync(pool: &PgPool) -> Result<(), LdapError> { let mut ldap_connection = match LDAPConnection::create().await { Ok(connection) => connection, Err(err) => { - set_ldap_sync_status(SyncStatus::OutOfSync, pool).await?; + set_ldap_sync_status(LdapSyncStatus::OutOfSync, pool).await?; return Err(err); } }; if let Err(err) = ldap_connection.sync(pool, is_ldap_desynced()).await { - set_ldap_sync_status(SyncStatus::OutOfSync, pool).await?; + set_ldap_sync_status(LdapSyncStatus::OutOfSync, pool).await?; return Err(err); } - set_ldap_sync_status(SyncStatus::InSync, pool).await?; + set_ldap_sync_status(LdapSyncStatus::InSync, pool).await?; let _ = update_counts(pool).await; @@ -95,17 +102,17 @@ where let settings = Settings::get_current_settings(); if !is_enterprise_enabled() { info!("Enterprise features are disabled, not performing LDAP operation"); - set_ldap_sync_status(SyncStatus::OutOfSync, pool).await?; + set_ldap_sync_status(LdapSyncStatus::OutOfSync, pool).await?; return Err(LdapError::EnterpriseDisabled("LDAP".to_string())); } if !settings.ldap_enabled { debug!("LDAP is disabled, not performing LDAP operation"); - set_ldap_sync_status(SyncStatus::OutOfSync, pool).await?; + set_ldap_sync_status(LdapSyncStatus::OutOfSync, pool).await?; return Err(LdapError::MissingSettings("LDAP is disabled".into())); } - if settings.ldap_sync_enabled && get_ldap_sync_status() == SyncStatus::OutOfSync { + if settings.ldap_sync_enabled && get_ldap_sync_status() == LdapSyncStatus::OutOfSync { warn!("LDAP is considered to be desynced, not performing LDAP operation"); return Err(LdapError::Desynced); } @@ -114,7 +121,7 @@ where Ok(result) => Ok(result), Err(e) => { warn!("Encountered an error while performing LDAP operation: {e:?}"); - if let Err(status_err) = set_ldap_sync_status(SyncStatus::OutOfSync, pool).await { + if let Err(status_err) = set_ldap_sync_status(LdapSyncStatus::OutOfSync, pool).await { warn!("Failed to update LDAP sync status: {status_err:?}"); } diff --git a/crates/defguard_core/src/enterprise/ldap/model.rs b/crates/defguard_core/src/enterprise/ldap/model.rs index bfb3a20561..79a295732d 100644 --- a/crates/defguard_core/src/enterprise/ldap/model.rs +++ b/crates/defguard_core/src/enterprise/ldap/model.rs @@ -1,14 +1,11 @@ use std::collections::HashSet; +use defguard_common::db::{Id, models::Settings}; use ldap3::{Mod, SearchEntry}; use sqlx::{Error as SqlxError, PgExecutor}; use super::{LDAPConfig, error::LdapError}; -use crate::{ - db::{Id, Settings, User}, - handlers::user::check_username, - hashset, -}; +use crate::{db::User, handlers::user::check_username, hashset}; pub(crate) enum UserObjectClass { SambaSamAccount, diff --git a/crates/defguard_core/src/enterprise/ldap/sync.rs b/crates/defguard_core/src/enterprise/ldap/sync.rs index d7a795b6f5..f1dbeb27e1 100644 --- a/crates/defguard_core/src/enterprise/ldap/sync.rs +++ b/crates/defguard_core/src/enterprise/ldap/sync.rs @@ -54,11 +54,18 @@ //! use std::collections::{HashMap, HashSet}; -use sqlx::{PgConnection, PgPool, Type}; +use defguard_common::db::{ + Id, + models::{ + Settings, + settings::{LdapSyncStatus, update_current_settings}, + }, +}; +use sqlx::{PgConnection, PgPool}; use super::{LDAPConfig, error::LdapError}; use crate::{ - db::{Group, Id, Settings, User, models::settings::update_current_settings}, + db::{Group, User}, hashset, }; @@ -85,28 +92,13 @@ pub enum Authority { Defguard, } -#[derive(Clone, Debug, Copy, Eq, PartialEq, Deserialize, Serialize, Default, Type)] -#[sqlx(type_name = "ldap_sync_status", rename_all = "lowercase")] -pub enum SyncStatus { - InSync, - #[default] - OutOfSync, -} - -impl SyncStatus { - #[must_use] - pub fn is_out_of_sync(&self) -> bool { - matches!(self, SyncStatus::OutOfSync) - } -} - #[must_use] -pub fn get_ldap_sync_status() -> SyncStatus { +pub fn get_ldap_sync_status() -> LdapSyncStatus { let settings = Settings::get_current_settings(); settings.ldap_sync_status } -pub async fn set_ldap_sync_status(status: SyncStatus, pool: &PgPool) -> Result<(), LdapError> { +pub async fn set_ldap_sync_status(status: LdapSyncStatus, pool: &PgPool) -> Result<(), LdapError> { debug!("Setting LDAP sync status to {status:?}"); let mut settings = Settings::get_current_settings(); settings.ldap_sync_status = status; diff --git a/crates/defguard_core/src/enterprise/ldap/tests.rs b/crates/defguard_core/src/enterprise/ldap/tests.rs index 8d9d47fe4b..580573c416 100644 --- a/crates/defguard_core/src/enterprise/ldap/tests.rs +++ b/crates/defguard_core/src/enterprise/ldap/tests.rs @@ -1,15 +1,12 @@ use std::collections::HashMap; +use defguard_common::db::{models::settings::initialize_current_settings, setup_pool}; use ldap3::SearchEntry; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; use crate::{ - db::{ - Group, User, - models::settings::{Settings, initialize_current_settings, update_current_settings}, - setup_pool, - }, + db::{Group, User}, enterprise::ldap::{ model::extract_rdn_value, sync::{ diff --git a/crates/defguard_core/src/enterprise/ldap/utils.rs b/crates/defguard_core/src/enterprise/ldap/utils.rs index d952e8e22f..4ddc7bc8e6 100644 --- a/crates/defguard_core/src/enterprise/ldap/utils.rs +++ b/crates/defguard_core/src/enterprise/ldap/utils.rs @@ -3,11 +3,12 @@ use std::collections::{HashMap, HashSet}; +use defguard_common::db::Id; use sqlx::PgPool; use super::{LDAPConnection, error::LdapError}; use crate::{ - db::{Group, Id, User}, + db::{Group, User}, enterprise::ldap::with_ldap_status, }; diff --git a/crates/defguard_core/src/enterprise/license.rs b/crates/defguard_core/src/enterprise/license.rs index 420bdac777..499839d8cd 100644 --- a/crates/defguard_core/src/enterprise/license.rs +++ b/crates/defguard_core/src/enterprise/license.rs @@ -14,12 +14,12 @@ use thiserror::Error; use tokio::time::sleep; use super::limits::Counts; -use crate::{ +use crate::grpc::proto::enterprise::license::{LicenseKey, LicenseLimits, LicenseMetadata}; +use defguard_common::{ VERSION, - db::{Settings, models::settings::update_current_settings}, + config::server_config, + db::models::{Settings, settings::update_current_settings}, global_value, - grpc::proto::enterprise::license::{LicenseKey, LicenseLimits, LicenseMetadata}, - server_config, }; const LICENSE_SERVER_URL: &str = "https://pkgs.defguard.net/api/license/renew"; @@ -478,7 +478,7 @@ async fn renew_license() -> Result { reqwest::StatusCode::OK => { let response: RefreshRequestResponse = response.json().await.map_err(|err| { error!("Failed to parse the response from the license server while trying to \ - renew the license: {err:?}"); + renew the license: {err}"); LicenseError::LicenseServerError(err.to_string()) })?; response.key diff --git a/crates/defguard_core/src/enterprise/limits.rs b/crates/defguard_core/src/enterprise/limits.rs index 51a006a682..6e7d0f66b7 100644 --- a/crates/defguard_core/src/enterprise/limits.rs +++ b/crates/defguard_core/src/enterprise/limits.rs @@ -1,9 +1,10 @@ +use defguard_common::global_value; use sqlx::{PgPool, error::Error as SqlxError, query}; use super::license::License; #[cfg(test)] use super::license::get_cached_license; -use crate::{global_value, grpc::proto::enterprise::license::LicenseLimits}; +use crate::grpc::proto::enterprise::license::LicenseLimits; // Limits for free users pub const DEFAULT_USERS_LIMIT: u32 = 5; diff --git a/crates/defguard_core/src/enterprise/snat/handlers.rs b/crates/defguard_core/src/enterprise/snat/handlers.rs index 3b9e71adbf..3db0d81760 100644 --- a/crates/defguard_core/src/enterprise/snat/handlers.rs +++ b/crates/defguard_core/src/enterprise/snat/handlers.rs @@ -4,6 +4,7 @@ use axum::{ Json, extract::{Path, State}, }; +use defguard_common::db::Id; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -12,7 +13,7 @@ use utoipa::ToSchema; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, - db::{GatewayEvent, Id, User, WireguardNetwork}, + db::{GatewayEvent, User, WireguardNetwork}, enterprise::{ db::models::snat::UserSnatBinding, handlers::LicenseInfo, snat::error::UserSnatBindingError, }, diff --git a/crates/defguard_core/src/error.rs b/crates/defguard_core/src/error.rs index cea003dcc2..e58a2f8876 100644 --- a/crates/defguard_core/src/error.rs +++ b/crates/defguard_core/src/error.rs @@ -1,4 +1,6 @@ use axum::http::StatusCode; +use defguard_common::db::models::{ModelError, settings::SettingsValidationError}; +use defguard_mail::templates::TemplateError; use sqlx::error::Error as SqlxError; use thiserror::Error; use tokio::sync::mpsc::error::SendError; @@ -6,17 +8,13 @@ use utoipa::ToSchema; use crate::{ auth::failed_login::FailedLoginError, - db::models::{ - device::DeviceError, enrollment::TokenError, error::ModelError, - settings::SettingsValidationError, wireguard::WireguardNetworkError, - }, + db::models::{device::DeviceError, enrollment::TokenError, wireguard::WireguardNetworkError}, enterprise::{ activity_log_stream::error::ActivityLogStreamError, db::models::acl::AclError, firewall::FirewallError, ldap::error::LdapError, license::LicenseError, }, events::ApiEvent, grpc::gateway::map::GatewayMapError, - templates::TemplateError, }; /// Represents kinds of error that occurred @@ -42,6 +40,8 @@ pub enum WebError { Deserialization(String), #[error("Authorization error: {0}")] Authorization(String), + #[error("Authentication error")] + Authentication, #[error("Forbidden error: {0}")] Forbidden(String), #[error("Database error: {0}")] diff --git a/crates/defguard_core/src/events.rs b/crates/defguard_core/src/events.rs index 59c3d9ffcf..68bc260a72 100644 --- a/crates/defguard_core/src/events.rs +++ b/crates/defguard_core/src/events.rs @@ -1,19 +1,22 @@ use std::net::IpAddr; use chrono::{NaiveDateTime, Utc}; -use serde::Serialize; +use defguard_common::db::{ + Id, + models::{AuthenticationKey, MFAMethod, Settings}, +}; use crate::{ db::{ - Device, Group, Id, MFAMethod, Settings, User, WebAuthn, WebHook, WireguardNetwork, - models::{authentication_key::AuthenticationKey, oauth2client::OAuth2Client}, + Device, Group, User, WebAuthn, WebHook, WireguardNetwork, + models::oauth2client::OAuth2Client, }, enterprise::db::models::{ activity_log_stream::ActivityLogStream, api_tokens::ApiToken, openid_provider::OpenIdProvider, snat::UserSnatBinding, }, - grpc::proto::proxy::MfaMethod, }; +use defguard_proto::proxy::MfaMethod; /// Shared context that needs to be added to every API event /// @@ -383,23 +386,6 @@ pub enum PasswordResetEvent { pub type ClientMFAMethod = MfaMethod; -impl Serialize for ClientMFAMethod { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match *self { - MfaMethod::Totp => serializer.serialize_unit_variant("MfaMethod", 0, "Totp"), - MfaMethod::Email => serializer.serialize_unit_variant("MfaMethod", 1, "Email"), - MfaMethod::Oidc => serializer.serialize_unit_variant("MfaMethod", 2, "Oidc"), - MfaMethod::Biometric => serializer.serialize_unit_variant("MfaMethod", 3, "Biometric"), - MfaMethod::MobileApprove => { - serializer.serialize_unit_variant("MfaMethod", 4, "MobileApprove") - } - } - } -} - #[derive(Debug)] pub enum DesktopClientMfaEvent { Connected { diff --git a/crates/defguard_core/src/grpc/auth.rs b/crates/defguard_core/src/grpc/auth.rs index 99ebd75608..c8099389f6 100644 --- a/crates/defguard_core/src/grpc/auth.rs +++ b/crates/defguard_core/src/grpc/auth.rs @@ -1,20 +1,17 @@ use std::sync::{Arc, Mutex}; +use defguard_common::auth::claims::{Claims, ClaimsType}; +use defguard_proto::auth::{AuthenticateRequest, AuthenticateResponse, auth_service_server}; use jsonwebtoken::errors::Error as JWTError; use sqlx::PgPool; use tonic::{Request, Response, Status}; use crate::{ - auth::{ - Claims, ClaimsType, - failed_login::{FailedLoginMap, check_failed_logins, log_failed_login_attempt}, - }, + auth::failed_login::{FailedLoginMap, check_failed_logins, log_failed_login_attempt}, db::User, server_config, }; -tonic::include_proto!("auth"); - pub struct AuthServer { pool: PgPool, failed_logins: Arc>, diff --git a/crates/defguard_core/src/grpc/client_mfa.rs b/crates/defguard_core/src/grpc/client_mfa.rs index f7ac11e548..e2b86685b0 100644 --- a/crates/defguard_core/src/grpc/client_mfa.rs +++ b/crates/defguard_core/src/grpc/client_mfa.rs @@ -1,6 +1,14 @@ use std::collections::HashMap; use chrono::Utc; +use defguard_common::{ + auth::claims::{Claims, ClaimsType}, + db::{ + Id, + models::{BiometricAuth, BiometricChallenge}, + }, +}; +use defguard_mail::Mail; use sqlx::PgPool; use thiserror::Error; use tokio::sync::{ @@ -9,28 +17,23 @@ use tokio::sync::{ }; use tonic::{Code, Status}; -use super::proto::proxy::{ - self, ClientMfaFinishRequest, ClientMfaFinishResponse, ClientMfaStartRequest, - ClientMfaStartResponse, MfaMethod, -}; use crate::{ - auth::{Claims, ClaimsType}, db::{ - Device, GatewayEvent, Id, User, UserInfo, WireguardNetwork, + Device, GatewayEvent, User, UserInfo, WireguardNetwork, models::{ - biometric_auth::{BiometricAuth, BiometricChallenge}, device::{DeviceInfo, DeviceNetworkInfo, WireguardNetworkDevice}, wireguard::LocationMfaMode, }, }, enterprise::{db::models::openid_provider::OpenIdProvider, is_enterprise_enabled}, events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent}, - grpc::{ - proto::proxy::{ClientMfaTokenValidationRequest, ClientMfaTokenValidationResponse}, - utils::parse_client_info, - }, + grpc::utils::parse_client_info, handlers::mail::send_email_mfa_code_email, - mail::Mail, +}; +use defguard_proto::proxy::{ + self, ClientMfaFinishRequest, ClientMfaFinishResponse, ClientMfaStartRequest, + ClientMfaStartResponse, ClientMfaTokenValidationRequest, ClientMfaTokenValidationResponse, + MfaMethod, }; const CLIENT_SESSION_TIMEOUT: u64 = 60 * 5; // 10 minutes @@ -91,7 +94,7 @@ impl ClientMfaServer { ) .to_jwt() .map_err(|err| { - error!("Failed to generate JWT token: {err:?}"); + error!("Failed to generate JWT token: {err}"); Status::internal("unexpected error") }) } @@ -99,7 +102,7 @@ impl ClientMfaServer { /// Validate JWT and extract client pubkey pub(crate) fn parse_token(token: &str) -> Result { let claims = Claims::from_jwt(ClaimsType::DesktopClient, token).map_err(|err| { - error!("Failed to parse JWT token: {err:?}"); + error!("Failed to parse JWT token: {err}"); Status::invalid_argument("invalid token") })?; Ok(claims.client_id) @@ -163,7 +166,7 @@ impl ClientMfaServer { user.verify_mfa_state(&self.pool).await.map_err(|err| { error!( - "Failed to verify MFA state for user {}: {err:?}", + "Failed to verify MFA state for user {}: {err}", user.username ); Status::internal("unexpected error") @@ -249,7 +252,7 @@ impl ClientMfaServer { // send email code send_email_mfa_code_email(&user, &self.mail_tx, None).map_err(|err| { error!( - "Failed to send email MFA code for user {}: {err:?}", + "Failed to send email MFA code for user {}: {err}", user.username ); Status::internal("unexpected error") @@ -266,7 +269,7 @@ impl ClientMfaServer { if OpenIdProvider::get_current(&self.pool) .await .map_err(|err| { - error!("Failed to get current OpenID provider: {err:?}",); + error!("Failed to get current OpenID provider: {err}",); Status::internal("unexpected error") })? .is_none() @@ -346,7 +349,7 @@ impl ClientMfaServer { .get_allowed_groups(&mut conn) .await .map_err(|err| { - error!("Failed to fetch allowed groups for location {location}: {err:?}"); + error!("Failed to fetch allowed groups for location {location}: {err}"); Status::internal("unexpected error") })?; // if no groups are specified all users are allowed @@ -604,7 +607,7 @@ impl ClientMfaServer { .update(&mut *transaction) .await .map_err(|err| { - error!("Failed to update device network config {network_device:?}: {err:?}"); + error!("Failed to update device network config {network_device:?}: {err}"); Status::internal("unexpected error") })?; diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs index 4490c08582..2a744913e8 100644 --- a/crates/defguard_core/src/grpc/enrollment.rs +++ b/crates/defguard_core/src/grpc/enrollment.rs @@ -1,5 +1,16 @@ use std::collections::HashSet; +use defguard_common::{ + csv::AsCsv, + db::{ + Id, + models::{BiometricAuth, MFAMethod, Settings, settings::defaults::WELCOME_EMAIL_SUBJECT}, + }, +}; +use defguard_mail::{ + Mail, + templates::{self, TemplateLocation}, +}; use sqlx::{PgPool, Transaction, query_scalar}; use tokio::sync::{ broadcast::Sender, @@ -7,20 +18,11 @@ use tokio::sync::{ }; use tonic::Status; -use super::{ - InstanceInfo, - proto::proxy::{ - ActivateUserRequest, AdminInfo, Device as ProtoDevice, DeviceConfig as ProtoDeviceConfig, - DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, - InitialUserInfo, NewDevice, - }, -}; +use super::InstanceInfo; use crate::{ - AsCsv, db::{ - Device, GatewayEvent, Id, MFAMethod, Settings, User, WireguardNetwork, + Device, GatewayEvent, User, WireguardNetwork, models::{ - biometric_auth::BiometricAuth, device::{DeviceConfig, DeviceInfo, DeviceType}, enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError}, polling_token::PollingToken, @@ -33,14 +35,7 @@ use crate::{ limits::update_counts, }, events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, EnrollmentEvent}, - grpc::{ - proto::proxy::{ - CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, CodeMfaSetupStartRequest, - CodeMfaSetupStartResponse, LocationMfaMode as ProtoLocationMfaMode, MfaMethod, - RegisterMobileAuthRequest, - }, - utils::{build_device_config_response, new_polling_token, parse_client_info}, - }, + grpc::utils::{build_device_config_response, new_polling_token, parse_client_info}, handlers::{ mail::{ send_email_mfa_activation_email, send_mfa_configured_email, send_new_device_added_email, @@ -48,9 +43,14 @@ use crate::{ user::check_password_strength, }, headers::get_device_info, - mail::Mail, - server_config, - templates::{self, TemplateLocation}, + is_valid_phone_number, server_config, +}; +use defguard_proto::proxy::{ + ActivateUserRequest, AdminInfo, CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, + CodeMfaSetupStartRequest, CodeMfaSetupStartResponse, Device as ProtoDevice, + DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, + EnrollmentStartResponse, ExistingDevice, InitialUserInfo, + LocationMfaMode as ProtoLocationMfaMode, MfaMethod, NewDevice, RegisterMobileAuthRequest, }; pub(super) struct EnrollmentServer { @@ -130,7 +130,7 @@ impl EnrollmentServer { pub async fn start_enrollment( &self, request: EnrollmentStartRequest, - info: Option, + info: Option, ) -> Result { debug!("Starting enrollment session, request: {request:?}"); // fetch enrollment token @@ -230,7 +230,7 @@ impl EnrollmentServer { user.username, user.id ); let (username, user_id) = (user.username.clone(), user.id); - let user_info = InitialUserInfo::from_user(&self.pool, user) + let user_info = initial_info_from_user(&self.pool, user) .await .map_err(|err| { error!( @@ -263,14 +263,14 @@ impl EnrollmentServer { .fetch_one(&self.pool) .await .map_err(|_| Status::internal("Failed to read data".to_string()))?; - let enrollment_settings = super::proto::proxy::EnrollmentSettings { + let enrollment_settings = defguard_proto::proxy::EnrollmentSettings { vpn_setup_optional, smtp_configured, only_client_activation: enterprise_settings.only_client_activation, admin_device_management: enterprise_settings.admin_device_management, mfa_required: instance_has_internal_mfa, }; - let response = super::proto::proxy::EnrollmentStartResponse { + let response = defguard_proto::proxy::EnrollmentStartResponse { admin: admin_info, user: Some(user_info), deadline_timestamp: session_deadline.and_utc().timestamp(), @@ -343,14 +343,28 @@ impl EnrollmentServer { Ok(()) } + fn validate_activated_user(request: &ActivateUserRequest) -> Result<(), Status> { + if let Some(ref phone_number) = request.phone_number { + if !is_valid_phone_number(phone_number) { + return Err(Status::new( + tonic::Code::InvalidArgument, + "invalid phone number", + )); + } + } + + Ok(()) + } + #[instrument(skip_all)] pub async fn activate_user( &self, request: ActivateUserRequest, - req_device_info: Option, + req_device_info: Option, ) -> Result<(), Status> { - debug!("Activating user account: {request:?}"); + debug!("Activating user account"); let enrollment = self.validate_session(request.token.as_ref()).await?; + Self::validate_activated_user(&request)?; let ip_address; let device_info; @@ -469,9 +483,9 @@ impl EnrollmentServer { pub async fn create_device( &self, request: NewDevice, - req_device_info: Option, + req_device_info: Option, ) -> Result { - debug!("Adding new user device: {request:?}"); + debug!("Adding new user device"); let enrollment_token = self.validate_session(request.token.as_ref()).await?; // fetch related users @@ -840,7 +854,6 @@ impl EnrollmentServer { ), token: Some(token.token), }; - debug!("{response:?}."); // Prepare event context and push the event let (ip, user_agent) = parse_client_info(&req_device_info).map_err(Status::internal)?; @@ -1024,24 +1037,25 @@ impl From> for AdminInfo { } } -impl InitialUserInfo { - async fn from_user(pool: &PgPool, user: User) -> Result { - let enrolled = user.is_enrolled(); - let devices = user.user_devices(pool).await?; - let device_names = devices.into_iter().map(|dev| dev.device.name).collect(); - let is_admin = user.is_admin(pool).await?; - Ok(Self { - first_name: user.first_name, - last_name: user.last_name, - login: user.username, - email: user.email, - phone_number: user.phone, - is_active: user.is_active, - device_names, - enrolled, - is_admin, - }) - } +async fn initial_info_from_user( + pool: &PgPool, + user: User, +) -> Result { + let enrolled = user.is_enrolled(); + let devices = user.user_devices(pool).await?; + let device_names = devices.into_iter().map(|dev| dev.device.name).collect(); + let is_admin = user.is_admin(pool).await?; + Ok(InitialUserInfo { + first_name: user.first_name, + last_name: user.last_name, + login: user.username, + email: user.email, + phone_number: user.phone, + is_active: user.is_active, + device_names, + enrolled, + is_admin, + }) } impl From for ProtoDeviceConfig { @@ -1094,7 +1108,10 @@ impl Token { debug!("Sending welcome mail to {}", user.username); let mail = Mail { to: user.email.clone(), - subject: settings.enrollment_welcome_email_subject.clone().unwrap(), + subject: settings + .enrollment_welcome_email_subject + .clone() + .unwrap_or_else(|| WELCOME_EMAIL_SUBJECT.to_string()), content: self .get_welcome_email_content(&mut *transaction, ip_address, device_info) .await?, @@ -1129,8 +1146,8 @@ impl Token { to: admin.email.clone(), subject: "[defguard] User enrollment completed".into(), content: templates::enrollment_admin_notification( - user, - admin, + &user.clone().into(), + &admin.clone().into(), ip_address, device_info, )?, @@ -1152,3 +1169,121 @@ impl Token { } } } + +#[cfg(test)] +mod test { + use defguard_common::{ + config::{DefGuardConfig, SERVER_CONFIG}, + db::{ + models::{ + Settings, + settings::{defaults::WELCOME_EMAIL_SUBJECT, initialize_current_settings}, + }, + setup_pool, + }, + }; + use defguard_mail::Mail; + use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + use tokio::sync::mpsc::unbounded_channel; + + use crate::db::{ + User, + models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}, + }; + + #[sqlx::test] + async fn dg25_11_test_enrollment_welcome_email(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + // initialize server config + SERVER_CONFIG + .set(DefGuardConfig::new_test_config()) + .unwrap(); + + // setup mail channel + let (mail_tx, mut mail_rx) = unbounded_channel::(); + + // setup users + let admin = User::new( + "test_admin", + Some("pass123"), + "Test", + "Admin", + "admin@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + let user = User::new( + "test_user", + Some("pass123"), + "Test", + "User", + "user@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + // generate enrollment token + let token = Token::new( + user.id, + Some(admin.id), + Some(user.email.clone()), + 10, + Some(ENROLLMENT_TOKEN_TYPE.to_string()), + ); + + // initialize settings + Settings::init_defaults(&pool).await.unwrap(); + initialize_current_settings(&pool).await.unwrap(); + + let mut settings = Settings::get(&pool).await.unwrap().unwrap(); + + // send welcome email + let mut transaction = pool.begin().await.unwrap(); + token + .send_welcome_email( + &mut transaction, + &mail_tx, + &user, + &settings, + "127.0.0.1", + None, + ) + .await + .unwrap(); + + // check email content + let mail = mail_rx.recv().await.unwrap(); + assert_eq!(mail.to, user.email); + assert_eq!( + mail.subject, + settings.enrollment_welcome_email_subject.unwrap() + ); + + // set subject to None + settings.enrollment_welcome_email_subject = None; + + // send another welcome email + let mut transaction = pool.begin().await.unwrap(); + token + .send_welcome_email( + &mut transaction, + &mail_tx, + &user, + &settings, + "127.0.0.1", + None, + ) + .await + .unwrap(); + + // check email content + let mail = mail_rx.recv().await.unwrap(); + assert_eq!(mail.to, user.email); + assert_eq!(mail.subject, WELCOME_EMAIL_SUBJECT); + } +} diff --git a/crates/defguard_core/src/grpc/gateway/client_state.rs b/crates/defguard_core/src/grpc/gateway/client_state.rs index 76b5439306..1bc49a404c 100644 --- a/crates/defguard_core/src/grpc/gateway/client_state.rs +++ b/crates/defguard_core/src/grpc/gateway/client_state.rs @@ -1,11 +1,12 @@ use std::{collections::HashMap, net::SocketAddr}; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use defguard_common::db::Id; use thiserror::Error; use tonic::{Code, Status}; use crate::{ - db::{Device, Id, User, WireguardNetwork, models::wireguard_peer_stats::WireguardPeerStats}, + db::{Device, User, WireguardNetwork, models::wireguard_peer_stats::WireguardPeerStats}, events::GrpcRequestContext, }; diff --git a/crates/defguard_core/src/grpc/gateway/map.rs b/crates/defguard_core/src/grpc/gateway/map.rs index cef0016cdd..42e665784d 100644 --- a/crates/defguard_core/src/grpc/gateway/map.rs +++ b/crates/defguard_core/src/grpc/gateway/map.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use chrono::Utc; +use defguard_common::db::Id; use defguard_version::tracing::VersionInfo; use semver::Version; use sqlx::PgPool; @@ -9,7 +10,7 @@ use tokio::sync::mpsc::UnboundedSender; use uuid::Uuid; use super::state::GatewayState; -use crate::{db::Id, mail::Mail}; +use defguard_mail::Mail; /// Helper struct used to handle gateway state. Gateways are grouped by network. type GatewayHostname = String; diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 39f6fd283d..ca3d668283 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -7,6 +7,15 @@ use std::{ use chrono::{DateTime, TimeDelta, Utc}; use client_state::ClientMap; +use defguard_common::db::{Id, NoId}; +use defguard_mail::Mail; +use defguard_proto::{ + enterprise::firewall::FirewallConfig, + gateway::{ + Configuration, ConfigurationRequest, Peer, PeerStats, StatsUpdate, Update, + gateway_service_server, stats_update, update, + }, +}; use defguard_version::version_info_from_metadata; use semver::Version; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query}; @@ -23,18 +32,12 @@ use tokio_stream::Stream; use tonic::{Code, Request, Response, Status, metadata::MetadataMap}; use self::map::GatewayMap; -use super::proto::enterprise::firewall::FirewallConfig; -pub use crate::grpc::proto::gateway::{ - Configuration, ConfigurationRequest, Peer, PeerStats, StatsUpdate, Update, - gateway_service_server, stats_update, update, -}; use crate::{ db::{ - Device, GatewayEvent, Id, NoId, User, + Device, GatewayEvent, User, models::{wireguard::WireguardNetwork, wireguard_peer_stats::WireguardPeerStats}, }, events::{GrpcEvent, GrpcRequestContext}, - mail::Mail, }; pub mod client_state; diff --git a/crates/defguard_core/src/grpc/gateway/state.rs b/crates/defguard_core/src/grpc/gateway/state.rs index b44fd21c13..7219c30d16 100644 --- a/crates/defguard_core/src/grpc/gateway/state.rs +++ b/crates/defguard_core/src/grpc/gateway/state.rs @@ -1,6 +1,8 @@ use std::time::Duration; use chrono::NaiveDateTime; +use defguard_common::db::{Id, models::Settings}; +use defguard_mail::Mail; use defguard_version::{DefguardComponent, tracing::VersionInfo}; use semver::Version; use serde::Serialize; @@ -11,10 +13,8 @@ use utoipa::ToSchema; use uuid::Uuid; use crate::{ - db::{Id, Settings}, grpc::MIN_GATEWAY_VERSION, handlers::mail::{send_gateway_disconnected_email, send_gateway_reconnected_email}, - mail::Mail, }; #[derive(Clone, Debug, Serialize, ToSchema)] diff --git a/crates/defguard_core/src/grpc/interceptor.rs b/crates/defguard_core/src/grpc/interceptor.rs index 51e8a865d9..88acca2b32 100644 --- a/crates/defguard_core/src/grpc/interceptor.rs +++ b/crates/defguard_core/src/grpc/interceptor.rs @@ -1,9 +1,7 @@ +use defguard_common::auth::claims::{Claims, ClaimsType}; use tonic::{Status, service::Interceptor}; -use crate::{ - auth::{Claims, ClaimsType}, - grpc::{AUTHORIZATION_HEADER, HOSTNAME_HEADER}, -}; +use crate::grpc::{AUTHORIZATION_HEADER, HOSTNAME_HEADER}; /// Auth interceptor used by gRPC services. Verifies JWT token sent /// in gRPC metadata under "authorization" key. @@ -32,7 +30,7 @@ impl Interceptor for JwtInterceptor { warn!( "Failed to parse authorization header during handling gRPC request from \ hostname {hostname}. If you recognize this hostname, there may be an issue \ - with the token used for authorization. Cause of the failed parsing: {err:?}" + with the token used for authorization. Cause of the failed parsing: {err}" ); Status::unauthenticated("Invalid token") })?, @@ -68,7 +66,7 @@ impl Interceptor for JwtInterceptor { warn!( "Failed to authorize a gRPC request from hostname {hostname}. If you recognize \ this hostname, there may be an issue with the token used for authorization. \ - Cause of the failed authorization: {err:?}" + Cause of the failed authorization: {err}" ); Err(Status::unauthenticated("Invalid token")) } diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index c414f078f3..9ef9ae7199 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -3,14 +3,18 @@ use std::{ fs::read_to_string, time::{Duration, Instant}, }; -#[cfg(any(feature = "wireguard", feature = "worker"))] use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{Arc, Mutex, RwLock}, }; use axum::http::Uri; -#[cfg(feature = "wireguard")] +use defguard_common::{ + VERSION, + auth::claims::ClaimsType, + db::{Id, models::Settings}, +}; +use defguard_mail::Mail; use defguard_version::server::DefguardVersionLayer; use defguard_version::{ ComponentInfo, DefguardComponent, Version, client::ClientVersionInterceptor, @@ -19,7 +23,6 @@ use defguard_version::{ use openidconnect::{AuthorizationCode, Nonce, Scope, core::CoreAuthenticationFlow}; use reqwest::Url; use serde::Serialize; -#[cfg(feature = "worker")] use sqlx::PgPool; use tokio::{ sync::{ @@ -30,34 +33,25 @@ use tokio::{ }; use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{ - Code, Status, Streaming, + Code, Streaming, transport::{ Certificate, ClientTlsConfig, Endpoint, Identity, Server, ServerTlsConfig, server::Router, }, }; use tower::ServiceBuilder; -#[cfg(feature = "wireguard")] -use self::gateway::{GatewayServer, gateway_service_server::GatewayServiceServer}; +use self::gateway::GatewayServer; use self::{ - auth::{AuthServer, auth_service_server::AuthServiceServer}, - client_mfa::ClientMfaServer, - enrollment::EnrollmentServer, + auth::AuthServer, client_mfa::ClientMfaServer, enrollment::EnrollmentServer, password_reset::PasswordResetServer, - proto::proxy::core_response, -}; -#[cfg(feature = "worker")] -use self::{ - interceptor::JwtInterceptor, proto::worker::worker_service_server::WorkerServiceServer, - worker::WorkerServer, }; -#[cfg(feature = "wireguard")] +use self::{interceptor::JwtInterceptor, worker::WorkerServer}; +use crate::db::GatewayEvent; pub use crate::version::MIN_GATEWAY_VERSION; use crate::{ - VERSION, auth::failed_login::FailedLoginMap, db::{ - AppEvent, Id, Settings, + AppEvent, models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}, }, enterprise::{ @@ -72,53 +66,37 @@ use crate::{ }, events::{BidiStreamEvent, GrpcEvent}, grpc::gateway::{client_state::ClientMap, map::GatewayMap}, - mail::Mail, server_config, version::{IncompatibleComponents, IncompatibleProxyData, is_proxy_version_supported}, }; -#[cfg(feature = "worker")] -use crate::{auth::ClaimsType, db::GatewayEvent}; static VERSION_ZERO: Version = Version::new(0, 0, 0); mod auth; pub(crate) mod client_mfa; pub mod enrollment; -#[cfg(feature = "wireguard")] pub mod gateway; -#[cfg(any(feature = "wireguard", feature = "worker"))] mod interceptor; pub mod password_reset; pub(crate) mod utils; -#[cfg(feature = "worker")] pub mod worker; pub mod proto { - pub mod proxy { - tonic::include_proto!("defguard.proxy"); - } - pub mod gateway { - tonic::include_proto!("gateway"); - } - pub mod auth { - tonic::include_proto!("auth"); - } - pub mod worker { - tonic::include_proto!("worker"); - } pub mod enterprise { pub mod license { tonic::include_proto!("enterprise.license"); } - pub mod firewall { - tonic::include_proto!("enterprise.firewall"); - } } } -use proto::proxy::{ - AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, core_request, - proxy_client::ProxyClient, +use defguard_proto::{ + auth::auth_service_server::AuthServiceServer, + gateway::gateway_service_server::GatewayServiceServer, + proxy::{ + AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, core_request, + core_response, proxy_client::ProxyClient, + }, + worker::worker_service_server::WorkerServiceServer, }; // gRPC header for passing auth token from clients @@ -129,15 +107,6 @@ pub static HOSTNAME_HEADER: &str = "hostname"; const TEN_SECS: Duration = Duration::from_secs(10); -impl From for CoreError { - fn from(status: Status) -> Self { - Self { - status_code: status.code().into(), - message: status.message().into(), - } - } -} - struct ProxyMessageLoopContext<'a> { pool: PgPool, tx: UnboundedSender, @@ -162,8 +131,7 @@ async fn handle_proxy_message_loop( break 'message; } Ok(Some(received)) => { - info!("Received message from proxy."); - debug!("Received the following message from proxy: {received:?}"); + debug!("Received message from proxy; ID={}", received.id); let payload = match received.payload { // rpc CodeMfaSetupStart return (CodeMfaSetupStartResponse) Some(core_request::Payload::CodeMfaSetupStart(request)) => { @@ -192,7 +160,7 @@ async fn handle_proxy_message_loop( Some(core_response::Payload::CodeMfaSetupFinishResponse(response)) } Err(err) => { - error!("Register mfa finish error {err}"); + error!("Register MFA finish error {err}"); Some(core_response::Payload::CoreError(err.into())) } } @@ -485,7 +453,7 @@ async fn handle_proxy_message_loop( error!( "Failed to sync user groups for user {} with the \ directory while the user was logging in through an \ - external provider: {err:?}", + external provider: {err}", user.username, ); } else { @@ -650,9 +618,8 @@ pub async fn run_grpc_bidi_stream( // Sleep before trying to reconnect sleep(TEN_SECS).await; continue; - } else { - IncompatibleComponents::remove_proxy(&incompatible_components); } + IncompatibleComponents::remove_proxy(&incompatible_components); info!("Connected to proxy at {}", endpoint.uri()); let mut resp_stream = response.into_inner(); @@ -735,7 +702,6 @@ pub async fn build_grpc_service_router( ) -> Result { let auth_service = AuthServiceServer::new(AuthServer::new(pool.clone(), failed_logins)); - #[cfg(feature = "worker")] let worker_service = WorkerServiceServer::with_interceptor( WorkerServer::new(pool.clone(), worker_state), JwtInterceptor::new(ClaimsType::YubiBridge), @@ -752,7 +718,6 @@ pub async fn build_grpc_service_router( .add_service(health_service) .add_service(auth_service); - #[cfg(feature = "wireguard")] let router = { use crate::version::GatewayVersionInterceptor; @@ -779,13 +744,11 @@ pub async fn build_grpc_service_router( ) }; - #[cfg(feature = "worker")] let router = router.add_service(worker_service); Ok(router) } -#[cfg(feature = "worker")] pub struct Job { id: u32, first_name: String, @@ -794,7 +757,6 @@ pub struct Job { username: String, } -#[cfg(feature = "worker")] #[derive(Serialize)] pub struct JobResponse { pub success: bool, @@ -804,14 +766,12 @@ pub struct JobResponse { pub username: String, } -#[cfg(feature = "worker")] pub struct WorkerInfo { last_seen: Instant, ip: IpAddr, jobs: Vec, } -#[cfg(feature = "worker")] pub struct WorkerState { current_job_id: u32, workers: HashMap, @@ -819,7 +779,6 @@ pub struct WorkerState { webhook_tx: UnboundedSender, } -#[cfg(feature = "worker")] #[derive(Deserialize, Serialize)] pub struct WorkerDetail { id: String, @@ -864,7 +823,7 @@ impl InstanceInfo { } } -impl From for proto::proxy::InstanceInfo { +impl From for defguard_proto::proxy::InstanceInfo { fn from(instance: InstanceInfo) -> Self { Self { name: instance.name, diff --git a/crates/defguard_core/src/grpc/password_reset.rs b/crates/defguard_core/src/grpc/password_reset.rs index 6af863a8e8..9049fb449e 100644 --- a/crates/defguard_core/src/grpc/password_reset.rs +++ b/crates/defguard_core/src/grpc/password_reset.rs @@ -1,11 +1,8 @@ +use defguard_mail::Mail; use sqlx::PgPool; use tokio::sync::mpsc::{UnboundedSender, error::SendError}; use tonic::Status; -use super::proto::proxy::{ - DeviceInfo, PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, - PasswordResetStartResponse, -}; use crate::{ db::{ User, @@ -18,9 +15,13 @@ use crate::{ mail::{send_password_reset_email, send_password_reset_success_email}, user::check_password_strength, }, - mail::Mail, + headers::get_device_info, server_config, }; +use defguard_proto::proxy::{ + DeviceInfo, PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, + PasswordResetStartResponse, +}; pub(super) struct PasswordResetServer { pool: PgPool, @@ -99,13 +100,14 @@ impl PasswordResetServer { debug!("Starting password reset request"); let ip_address; - let user_agent; + let device_info; if let Some(ref info) = req_device_info { ip_address = info.ip_address.clone(); - user_agent = info.user_agent.clone().unwrap_or_default(); + let agent = info.user_agent.clone().unwrap_or_default(); + device_info = get_device_info(&agent); } else { ip_address = String::new(); - user_agent = String::new(); + device_info = String::new(); } let email = request.email; @@ -152,14 +154,13 @@ impl PasswordResetServer { error!("Failed to commit transaction"); Status::internal("unexpected error") })?; - send_password_reset_email( &user, &self.mail_tx, config.enrollment_url.clone(), &enrollment.id, Some(&ip_address), - Some(&user_agent), + Some(&device_info), )?; info!( @@ -251,17 +252,18 @@ impl PasswordResetServer { request: PasswordResetRequest, req_device_info: Option, ) -> Result<(), Status> { - debug!("Starting password reset: {request:?}"); + debug!("Starting password reset"); let enrollment = self.validate_session(request.token.as_ref()).await?; let ip_address; - let user_agent; + let device_info; if let Some(ref info) = req_device_info { ip_address = info.ip_address.clone(); - user_agent = info.user_agent.clone().unwrap_or_default(); + let agent = info.user_agent.clone().unwrap_or_default(); + device_info = get_device_info(&agent); } else { ip_address = String::new(); - user_agent = String::new(); + device_info = String::new(); } if let Err(err) = check_password_strength(&request.password) { @@ -302,7 +304,7 @@ impl PasswordResetServer { &user, &self.mail_tx, Some(&ip_address), - Some(&user_agent), + Some(&device_info), )?; // Prepare event context and push the event diff --git a/crates/defguard_core/src/grpc/utils.rs b/crates/defguard_core/src/grpc/utils.rs index 226abbb117..757b9d9490 100644 --- a/crates/defguard_core/src/grpc/utils.rs +++ b/crates/defguard_core/src/grpc/utils.rs @@ -1,16 +1,16 @@ use std::{net::IpAddr, str::FromStr}; +use defguard_common::{ + csv::AsCsv, + db::{Id, models::Settings}, +}; use sqlx::PgPool; use tonic::Status; -use super::{ - InstanceInfo, - proto::proxy::{DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, DeviceInfo}, -}; +use super::InstanceInfo; use crate::{ - AsCsv, db::{ - Device, Id, Settings, User, + Device, User, models::{ device::{DeviceType, WireguardNetworkDevice}, polling_token::PollingToken, @@ -20,7 +20,10 @@ use crate::{ enterprise::db::models::{ enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider, }, - grpc::proto::proxy::LocationMfaMode as ProtoLocationMfaMode, +}; +use defguard_proto::proxy::{ + DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, DeviceInfo, + LocationMfaMode as ProtoLocationMfaMode, }; // Create a new token for configuration polling. @@ -216,6 +219,7 @@ pub(crate) fn parse_client_info(info: &Option) -> Result<(IpAddr, St msg })?; let user_agent = info.user_agent.clone().unwrap_or_else(String::new); + let escaped_agent = tera::escape_html(&user_agent); - Ok((ip, user_agent)) + Ok((ip, escaped_agent)) } diff --git a/crates/defguard_core/src/grpc/worker.rs b/crates/defguard_core/src/grpc/worker.rs index 2b973fbcaf..c0acfa12ec 100644 --- a/crates/defguard_core/src/grpc/worker.rs +++ b/crates/defguard_core/src/grpc/worker.rs @@ -5,19 +5,15 @@ use std::{ time::Instant, }; +use defguard_common::db::models::{AuthenticationKey, AuthenticationKeyType}; use sqlx::{PgPool, query}; use tokio::sync::mpsc::UnboundedSender; use tonic::{Request, Response, Status}; use super::{Job, JobResponse, WorkerDetail, WorkerInfo, WorkerState}; -pub use crate::grpc::proto::worker::JobStatus; -use crate::{ - db::{ - AppEvent, HWKeyUserData, User, YubiKey, - models::authentication_key::{AuthenticationKey, AuthenticationKeyType}, - }, - grpc::proto::worker::{GetJobResponse, Worker, worker_service_server}, -}; +use crate::db::{AppEvent, HWKeyUserData, User, YubiKey}; +pub use defguard_proto::worker::JobStatus; +use defguard_proto::worker::{GetJobResponse, Worker, worker_service_server}; impl WorkerInfo { /// Create new `Worker` instance. diff --git a/crates/defguard_core/src/handlers/activity_log.rs b/crates/defguard_core/src/handlers/activity_log.rs index c14a68fa88..5485d0d8c0 100644 --- a/crates/defguard_core/src/handlers/activity_log.rs +++ b/crates/defguard_core/src/handlers/activity_log.rs @@ -3,6 +3,7 @@ use std::fmt::{self, Display, Formatter}; use axum::extract::State; use axum_extra::extract::Query; use chrono::{DateTime, NaiveDateTime, Utc}; +use defguard_common::db::Id; use ipnetwork::IpNetwork; use sqlx::{FromRow, Postgres, QueryBuilder, Type}; @@ -10,11 +11,7 @@ use super::{ DEFAULT_API_PAGE_SIZE, pagination::{PaginatedApiResponse, PaginatedApiResult, PaginationMeta, PaginationParams}, }; -use crate::{ - appstate::AppState, - auth::SessionInfo, - db::{Id, models::activity_log::ActivityLogModule}, -}; +use crate::{appstate::AppState, auth::SessionInfo, db::models::activity_log::ActivityLogModule}; #[derive(Debug, Deserialize, Default)] pub struct FilterParams { diff --git a/crates/defguard_core/src/handlers/app_info.rs b/crates/defguard_core/src/handlers/app_info.rs index bc67e8c17f..e53592f9c8 100644 --- a/crates/defguard_core/src/handlers/app_info.rs +++ b/crates/defguard_core/src/handlers/app_info.rs @@ -3,10 +3,9 @@ use serde_json::json; use super::{ApiResponse, ApiResult}; use crate::{ - VERSION, appstate::AppState, auth::SessionInfo, - db::{Settings, WireguardNetwork}, + db::WireguardNetwork, enterprise::{ db::models::openid_provider::OpenIdProvider, is_enterprise_enabled, is_enterprise_free, @@ -14,6 +13,7 @@ use crate::{ limits::{LimitsExceeded, get_counts}, }, }; +use defguard_common::{VERSION, db::models::Settings}; #[derive(Serialize)] struct LicenseInfo { diff --git a/crates/defguard_core/src/handlers/auth.rs b/crates/defguard_core/src/handlers/auth.rs index 4e629ee746..357d68c814 100644 --- a/crates/defguard_core/src/handlers/auth.rs +++ b/crates/defguard_core/src/handlers/auth.rs @@ -13,6 +13,11 @@ use axum_extra::{ }, headers::UserAgent, }; +use defguard_common::db::{ + Id, + models::{MFAMethod, Settings}, +}; +use defguard_mail::Mail; use serde_json::json; use sqlx::{PgPool, types::Uuid}; use time::Duration; @@ -31,7 +36,7 @@ use crate::{ SessionInfo, failed_login::{check_failed_logins, log_failed_login_attempt}, }, - db::{Id, MFAInfo, MFAMethod, Session, SessionState, Settings, User, UserInfo, WebAuthn}, + db::{MFAInfo, Session, SessionState, User, UserInfo, WebAuthn}, enterprise::ldap::utils::login_through_ldap, error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, @@ -43,7 +48,6 @@ use crate::{ user_for_admin_or_self, }, headers::{USER_AGENT_PARSER, check_new_device_login, get_user_agent_device}, - mail::Mail, server_config, }; @@ -87,7 +91,7 @@ pub(crate) async fn create_session( check_new_device_login( pool, mail_tx, - &session, + &session.clone().into(), user, ip_address.to_string(), login_event_type, @@ -112,7 +116,7 @@ pub(crate) async fn create_session( check_new_device_login( pool, mail_tx, - &session, + &session.clone().into(), user, ip_address.to_string(), login_event_type, @@ -143,7 +147,7 @@ pub(crate) async fn authenticate( let settings = Settings::get_current_settings(); - // attempt to find user first by username and then by email + // Attempt to find a user: first by username, and then by email. let mut conn = appstate.pool.acquire().await?; let mut user = if let Some(user) = User::find_by_username_or_email(&mut conn, &username_or_email).await? @@ -177,7 +181,7 @@ pub(crate) async fn authenticate( ), }), })?; - return Err(WebError::Authorization(ldap_err.to_string())); + return Err(WebError::Authentication); } } } else { @@ -196,7 +200,7 @@ pub(crate) async fn authenticate( ), }), })?; - return Err(WebError::Authorization(err.to_string())); + return Err(WebError::Authentication); } } } @@ -208,7 +212,7 @@ pub(crate) async fn authenticate( Err(err) => { info!("Failed to authenticate user {username_or_email} with LDAP: {err}"); log_failed_login_attempt(&appstate.failed_logins, &username_or_email); - return Err(WebError::Authorization(err.to_string())); + return Err(WebError::Authentication); } } }; @@ -216,7 +220,7 @@ pub(crate) async fn authenticate( // check if user account is active if !user.is_active { info!("Failed to authenticate user {username_or_email}: user is disabled"); - return Err(WebError::Authorization("user not found".into())); + return Err(WebError::Authentication); } let (session, user_info, mfa_info) = create_session( @@ -471,7 +475,7 @@ pub async fn webauthn_finish( .await?; if user.mfa_method == MFAMethod::None { send_mfa_configured_email( - Some(&session.session), + Some(&session.session.into()), &user, &MFAMethod::Webauthn, &appstate.mail_tx, @@ -640,7 +644,7 @@ pub async fn totp_enable( user.enable_totp(&appstate.pool).await?; if user.mfa_method == MFAMethod::None { send_mfa_configured_email( - Some(&session.session), + Some(&session.session.into()), &user, &MFAMethod::OneTimePassword, &appstate.mail_tx, @@ -789,7 +793,7 @@ pub async fn email_mfa_init(session: SessionInfo, State(appstate): State, - session: &Session, + session: &SessionContext, created: NaiveDateTime, ) -> Result<(), TemplateError> { debug!("User {user_email} new device login mail to {SUPPORT_EMAIL_ADDRESS}"); @@ -319,7 +322,7 @@ pub async fn send_new_device_ocid_login_email( user_email: &str, oauth2client_name: String, mail_tx: &UnboundedSender, - session: &Session, + session: &SessionContext, ) -> Result<(), TemplateError> { debug!("User {user_email} new device OCID login mail to {SUPPORT_EMAIL_ADDRESS}"); @@ -348,7 +351,7 @@ pub async fn send_new_device_ocid_login_email( } pub fn send_mfa_configured_email( - session: Option<&Session>, + session: Option<&SessionContext>, user: &User, mfa_method: &MFAMethod, mail_tx: &UnboundedSender, @@ -382,7 +385,7 @@ pub fn send_mfa_configured_email( pub fn send_email_mfa_activation_email( user: &User, mail_tx: &UnboundedSender, - session: Option<&Session>, + session: Option<&SessionContext>, ) -> Result<(), TemplateError> { debug!("Sending email MFA activation mail to {}", user.email); @@ -395,7 +398,7 @@ pub fn send_email_mfa_activation_email( let mail = Mail { to: user.email.clone(), subject: EMAIL_MFA_ACTIVATION_EMAIL_SUBJECT.into(), - content: templates::email_mfa_activation_mail(user, &code, session)?, + content: templates::email_mfa_activation_mail(&user.clone().into(), &code, session)?, attachments: Vec::new(), result_tx: None, }; @@ -417,7 +420,7 @@ pub fn send_email_mfa_activation_email( pub fn send_email_mfa_code_email( user: &User, mail_tx: &UnboundedSender, - session: Option<&Session>, + session: Option<&SessionContext>, ) -> Result<(), TemplateError> { debug!("Sending email MFA code mail to {}", user.email); @@ -430,7 +433,7 @@ pub fn send_email_mfa_code_email( let mail = Mail { to: user.email.clone(), subject: EMAIL_MFA_CODE_EMAIL_SUBJECT.into(), - content: templates::email_mfa_code_mail(user, &code, session)?, + content: templates::email_mfa_code_mail(&user.clone().into(), &code, session)?, attachments: Vec::new(), result_tx: None, }; @@ -461,7 +464,7 @@ pub fn send_password_reset_email( let mail = Mail { to: user.email.clone(), - subject: EMAIL_PASSOWRD_RESET_START_SUBJECT.into(), + subject: EMAIL_PASSWORD_RESET_START_SUBJECT.into(), content: templates::email_password_reset_mail(service_url, token, ip_address, device_info)?, attachments: Vec::new(), result_tx: None, @@ -491,7 +494,7 @@ pub fn send_password_reset_success_email( let mail = Mail { to: user.email.clone(), - subject: EMAIL_PASSOWRD_RESET_SUCCESS_SUBJECT.into(), + subject: EMAIL_PASSWORD_RESET_SUCCESS_SUBJECT.into(), content: templates::email_password_reset_success_mail(ip_address, device_info)?, attachments: Vec::new(), result_tx: None, diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index b088716a37..cbe8263013 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -6,17 +6,17 @@ use axum::{ }; use axum_client_ip::InsecureClientIp; use axum_extra::{TypedHeader, headers::UserAgent}; +use defguard_common::db::{Id, NoId}; use serde_json::{Value, json}; use sqlx::PgPool; use utoipa::ToSchema; use webauthn_rs::prelude::RegisterPublicKeyCredential; -#[cfg(feature = "wireguard")] use crate::db::Device; use crate::{ appstate::AppState, auth::SessionInfo, - db::{Id, NoId, User, UserInfo, WebHook}, + db::{User, UserInfo, WebHook}, enterprise::{db::models::acl::AclError, license::LicenseError}, error::WebError, events::ApiRequestContext, @@ -29,9 +29,7 @@ pub(crate) mod forward_auth; pub(crate) mod group; pub(crate) mod mail; pub mod network_devices; -#[cfg(feature = "openid")] pub(crate) mod openid_clients; -#[cfg(feature = "openid")] pub mod openid_flow; pub(crate) mod pagination; pub(crate) mod settings; @@ -40,9 +38,7 @@ pub(crate) mod support; pub(crate) mod updates; pub(crate) mod user; pub(crate) mod webhooks; -#[cfg(feature = "wireguard")] pub mod wireguard; -#[cfg(feature = "worker")] pub mod worker; pub(crate) mod yubikey; @@ -80,6 +76,7 @@ impl From for ApiResponse { error!(msg); ApiResponse::new(json!({ "msg": msg }), StatusCode::UNAUTHORIZED) } + WebError::Authentication => ApiResponse::new(json!({}), StatusCode::UNAUTHORIZED), WebError::Forbidden(msg) => { error!(msg); ApiResponse::new(json!({ "msg": msg }), StatusCode::FORBIDDEN) @@ -438,7 +435,6 @@ pub async fn user_for_admin_or_self( /// Try to fetch [`Device'] if the device.id is of the currently logged in user, or /// the logged in user is an admin. -#[cfg(feature = "wireguard")] pub async fn device_for_admin_or_self<'e, E: sqlx::PgExecutor<'e>>( executor: E, session: &SessionInfo, diff --git a/crates/defguard_core/src/handlers/network_devices.rs b/crates/defguard_core/src/handlers/network_devices.rs index 6f85448f06..b0b1863fd7 100644 --- a/crates/defguard_core/src/handlers/network_devices.rs +++ b/crates/defguard_core/src/handlers/network_devices.rs @@ -8,17 +8,18 @@ use axum::{ http::StatusCode, }; use chrono::NaiveDateTime; +use defguard_common::{csv::AsCsv, db::Id}; +use defguard_mail::templates::TemplateLocation; use ipnetwork::IpNetwork; use serde_json::json; use sqlx::PgConnection; use super::{ApiResponse, ApiResult, WebError}; use crate::{ - AsCsv, appstate::AppState, auth::{AdminRole, SessionInfo}, db::{ - Device, GatewayEvent, Id, User, WireguardNetwork, + Device, GatewayEvent, User, WireguardNetwork, models::{ device::{DeviceConfig, DeviceInfo, DeviceType, WireguardNetworkDevice}, wireguard::NetworkAddressError, @@ -28,7 +29,6 @@ use crate::{ events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::mail::send_new_device_added_email, server_config, - templates::TemplateLocation, }; #[derive(Serialize)] diff --git a/crates/defguard_core/src/handlers/openid_clients.rs b/crates/defguard_core/src/handlers/openid_clients.rs index cc6a0bd52e..e0a911590d 100644 --- a/crates/defguard_core/src/handlers/openid_clients.rs +++ b/crates/defguard_core/src/handlers/openid_clients.rs @@ -26,6 +26,16 @@ pub async fn add_openid_client( "User {} adding OpenID client {}", session.user.username, data.name ); + if ammonia::is_html(&data.name) { + warn!( + "User {} attempted to create openid client with name containing HTML: {}", + session.user.username, data.name + ); + return Ok(ApiResponse { + json: json!({"msg": "invalid name"}), + status: StatusCode::BAD_REQUEST, + }); + } let client = OAuth2Client::from_new(data).save(&appstate.pool).await?; info!( "User {} added OpenID client {}", @@ -89,6 +99,16 @@ pub async fn change_openid_client( "User {} updating OpenID client {client_id}...", session.user.username ); + if ammonia::is_html(&data.name) { + warn!( + "User {} attempted to edit openid client with name containing HTML: {}", + session.user.username, data.name + ); + return Ok(ApiResponse { + json: json!({"msg": "invalid name"}), + status: StatusCode::BAD_REQUEST, + }); + } let mut transaction = appstate.pool.begin().await?; let status = match OAuth2Client::find_by_client_id(&mut *transaction, &client_id).await? { Some(mut client) => { diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index dce8b6d569..cad6c47f7d 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -15,6 +15,7 @@ use axum::{ use axum_extra::extract::cookie::{Cookie, CookieJar, PrivateCookieJar, SameSite}; use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::Utc; +use defguard_common::db::{Id, NoId, models::AuthCode}; use openidconnect::{ AccessToken, AdditionalClaims, Audience, AuthUrl, AuthorizationCode, EmptyAdditionalProviderMetadata, EmptyExtraTokenFields, EndUserEmail, EndUserFamilyName, @@ -41,10 +42,10 @@ use time::Duration; use super::{ApiResponse, ApiResult, SESSION_COOKIE_NAME}; use crate::{ appstate::AppState, - auth::{AccessUserInfo, SessionInfo, UserClaims}, + auth::{SessionInfo, UserClaims}, db::{ - Id, OAuth2AuthorizedApp, OAuth2Token, Session, SessionState, User, - models::{auth_code::AuthCode, oauth2client::OAuth2Client}, + OAuth2AuthorizedApp, OAuth2Token, Session, SessionState, User, + models::oauth2client::OAuth2Client, }, error::WebError, handlers::{SIGN_IN_COOKIE_NAME, mail::send_new_device_ocid_login_email}, @@ -271,18 +272,7 @@ impl AuthenticationRequest { // assume `client_id` is the same here and in `oauth2client` - // check `redirect_uri` matches client config (ignoring trailing slashes) - let parsed_redirect_uris: Vec = oauth2client - .redirect_uri - .iter() - .map(|uri| uri.trim_end_matches('/').into()) - .collect(); - if self - .redirect_uri - .split(' ') - .map(|uri| uri.trim_end_matches('/')) - .all(|uri| !parsed_redirect_uris.iter().any(|u| u == uri)) - { + if !oauth2client.contains_redirect_url(&self.redirect_uri) { error!( "Invalid redirect_uri for client {}: {} not in [{}]", oauth2client.name, @@ -335,7 +325,7 @@ async fn generate_auth_code_redirect( if let Some(state) = data.state { query_pairs.append_pair("state", &state); } - }; + } Ok(url.to_string()) } @@ -392,11 +382,16 @@ pub async fn authorization( private_cookies: PrivateCookieJar, ) -> Result<(StatusCode, HeaderMap, PrivateCookieJar), WebError> { let error; + let mut is_redirect_allowed = false; if let Some(oauth2client) = OAuth2Client::find_by_client_id(&appstate.pool, &data.client_id).await? { - match data.validate_for_client(&oauth2client) { - Ok(()) => { + is_redirect_allowed = oauth2client.contains_redirect_url(&data.redirect_uri); + match ( + oauth2client.enabled, + data.validate_for_client(&oauth2client), + ) { + (true, Ok(())) => { match &data.prompt { Some(s) if s == "consent" => { info!( @@ -405,7 +400,7 @@ pub async fn authorization( ); // FIXME: do not panic return Ok(redirect_to( - format!("/consent?{}", serde_urlencoded::to_string(data).unwrap(),), + format!("/consent?{}", serde_urlencoded::to_string(data).unwrap()), private_cookies, )); } @@ -421,7 +416,8 @@ pub async fn authorization( // If session expired return login if session.expired() { info!( - "Session {} for user id {} has expired, redirecting to login", + "Session {} for user id {} has expired, redirecting to \ + login", session.id, session.user_id ); let _result = session.delete(&appstate.pool).await; @@ -436,8 +432,9 @@ pub async fn authorization( user.verify_mfa_state(&appstate.pool).await?; - // 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. + // 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 { @@ -448,8 +445,9 @@ pub async fn authorization( return Ok(login_redirect(&data, private_cookies)); } - // 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 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) = OAuth2AuthorizedApp::find_by_user_and_oauth2client_id( &appstate.pool, @@ -459,7 +457,8 @@ pub async fn authorization( .await? { info!( - "OAuth client id {} authorized by user id {}, returning auth code", + "OAuth client id {} authorized by user id {}, \ + returning auth code", app.oauth2client_id, session.user_id ); let private_cookies = private_cookies @@ -474,7 +473,8 @@ pub async fn authorization( } else { // If authorized app not found redirect to consent form info!( - "OAuth client id {} not yet authorized by user id {}, redirecting to consent form", + "OAuth client id {} not yet authorized by user id {}, \ + redirecting to consent form", oauth2client.id, session.user_id ); Ok(redirect_to( @@ -487,14 +487,13 @@ pub async fn authorization( } } } else { - // If session is not present in db redirect to login + // If session is not present in database, redirect to login. info!( "Session {} not found, redirecting to login page", session_cookie.value() ); Ok(login_redirect(&data, private_cookies)) } - // If no session cookie provided redirect to login } else { info!("Session cookie not provided, redirecting to login page"); @@ -503,22 +502,29 @@ pub async fn authorization( } } } - Err(err) => { + (true, Err(err)) => { error!( "OIDC login validation failed for client {}: {err:?}", data.client_id ); error = err; } + (false, _) => { + error!("OIDC client id {} is disabled", data.client_id); + error = CoreAuthErrorResponseType::UnauthorizedClient; + } } } else { error!("OIDC client id {} not found", data.client_id); error = CoreAuthErrorResponseType::UnauthorizedClient; } - let mut url = - Url::parse(&data.redirect_uri).map_err(|_| WebError::Http(StatusCode::BAD_REQUEST))?; - + let mut url = if is_redirect_allowed { + Url::parse(&data.redirect_uri).map_err(|_| WebError::Http(StatusCode::BAD_REQUEST))? + } else { + // Don't allow open redirects (DG25-17) + server_config().url.clone() + }; { let mut query_pairs = url.query_pairs_mut(); query_pairs.append_pair("error", error.as_ref()); @@ -538,7 +544,7 @@ pub struct GroupClaims { impl AdditionalClaims for GroupClaims {} -pub async fn get_group_claims(pool: &PgPool, user: &User) -> Result { +async fn get_group_claims(pool: &PgPool, user: &User) -> Result { let groups = user.member_of_names(pool).await?; Ok(GroupClaims { groups: Some(groups), @@ -552,15 +558,18 @@ pub async fn secure_authorization( Query(data): Query, private_cookies: PrivateCookieJar, ) -> Result<(StatusCode, HeaderMap, PrivateCookieJar), WebError> { - let mut url = - Url::parse(&data.redirect_uri).map_err(|_| WebError::Http(StatusCode::BAD_REQUEST))?; let error; - if data.allow { - if let Some(oauth2client) = - OAuth2Client::find_by_client_id(&appstate.pool, &data.client_id).await? - { - match data.validate_for_client(&oauth2client) { - Ok(()) => { + let mut is_redirect_allowed = false; + if let Some(oauth2client) = + OAuth2Client::find_by_client_id(&appstate.pool, &data.client_id).await? + { + is_redirect_allowed = oauth2client.contains_redirect_url(&data.redirect_uri); + if data.allow { + match ( + oauth2client.enabled, + data.validate_for_client(&oauth2client), + ) { + (true, Ok(())) => { if OAuth2AuthorizedApp::find_by_user_and_oauth2client_id( &appstate.pool, session_info.user.id, @@ -576,7 +585,7 @@ pub async fn secure_authorization( &session_info.user.email, oauth2client.name.to_string(), &appstate.mail_tx, - &session_info.session, + &session_info.session.into(), ) .await?; } @@ -593,29 +602,39 @@ pub async fn secure_authorization( ); return Ok(redirect_to(location, private_cookies)); } - Err(err) => { + (true, Err(err)) => { info!( "OIDC login validation failed for user {}, client {}", session_info.user.username, oauth2client.name ); error = err; } + (false, _) => { + error!("OIDC client id {} is disabled", oauth2client.name); + error = CoreAuthErrorResponseType::UnauthorizedClient; + } } } else { - error!( - "User {} tried to log in with non-existent OIDC client id {}", + info!( + "User {} denied OIDC login with app id {}", session_info.user.username, data.client_id ); - error = CoreAuthErrorResponseType::UnauthorizedClient; + error = CoreAuthErrorResponseType::AccessDenied; } } else { - info!( - "User {} denied OIDC login with app id {}", + error!( + "User {} tried to log in with non-existent OIDC client id {}", session_info.user.username, data.client_id ); - error = CoreAuthErrorResponseType::AccessDenied; + error = CoreAuthErrorResponseType::UnauthorizedClient; } + let mut url = if is_redirect_allowed { + Url::parse(&data.redirect_uri).map_err(|_| WebError::Http(StatusCode::BAD_REQUEST))? + } else { + // Don't allow open redirects (DG25-17) + server_config().url.clone() + }; { let mut query_pairs = url.query_pairs_mut(); query_pairs.append_pair("error", error.as_ref()); @@ -663,7 +682,7 @@ impl TokenRequest { fn authorization_code_flow( &self, - auth_code: &AuthCode, + auth_code: &AuthCode, token: &OAuth2Token, claims: StandardClaims, base_url: &Url, @@ -794,21 +813,30 @@ pub async fn token( // for logging let form_client_id = match &form.client_id { - Some(id) => id.clone(), - None => String::from("N/A"), + Some(id) => id, + None => "N/A", }; if let Some(code) = &form.code { - if let Some(stored_auth_code) = AuthCode::find_code(&appstate.pool, code).await? { - // copy data before removing used token - let auth_code = stored_auth_code.clone(); - // remove authorization_code from DB so it cannot be reused - debug!( - "Removing used authorization_code {code}, client_id `{}`", - form_client_id - ); - stored_auth_code.consume(&appstate.pool).await?; + // Look for `AuthCode`. If found, it will be deleted from the database to avoid + // concurrent requests that might return multiple tokens for the same code. + // This addresses DG25-24 and conforms to RFC 6749. + if let Some(auth_code) = AuthCode::find_code(&appstate.pool, code).await? { + debug!("Consumed authorization_code {code}, client_id `{form_client_id}`"); if let Some(client) = oauth2client.or(form.oauth2client(&appstate.pool).await) { + if !client.enabled { + error!("OAuth client id `{}` is disabled", client.name); + let response = StandardErrorResponse::::new( + CoreErrorResponseType::UnauthorizedClient, + None, + None, + ); + return Ok(ApiResponse { + json: json!(response), + status: StatusCode::BAD_REQUEST, + }); + } + if let Some(user) = User::find_by_id(&appstate.pool, auth_code.user_id).await? { @@ -824,7 +852,7 @@ pub async fn token( "Issuing new token for user {} client {}", user.username, client.name ); - // Remove existing token in case same client asks for new token + // Remove existing token in case the same client asks for new token. if let Some(token) = OAuth2Token::find_by_authorized_app_id( &appstate.pool, authorized_app.id, @@ -867,8 +895,8 @@ pub async fn token( } Err(err) => { error!( - "Error issuing new token for user {} client {}: {}", - user.username, client.name, err + "Error issuing new token for user {} client {}: {err}", + user.username, client.name ); let response = StandardErrorResponse::::new( @@ -882,7 +910,8 @@ pub async fn token( } } error!( - "Can't issue token - authorized app not found for user {}, client {}", + "Can't issue token - authorized app not found for user {}, client \ + {}", user.username, client.name ); } else { @@ -895,7 +924,7 @@ pub async fn token( error!("OAuth auth code not found"); } } else { - error!("No code provided in request for client id `{form_client_id}`",); + error!("No code provided in request for client id `{form_client_id}`"); } } "refresh_token" => { @@ -904,6 +933,31 @@ pub async fn token( if let Ok(Some(mut token)) = OAuth2Token::find_refresh_token(&appstate.pool, &refresh_token).await { + let Some(client) = OAuth2Client::find_by_token(&appstate.pool, &token).await? + else { + error!("OAuth client not found for provided refresh_token"); + let err = CoreErrorResponseType::InvalidClient; + let response = + StandardErrorResponse::::new(err, None, None); + return Ok(ApiResponse { + json: json!(response), + status: StatusCode::BAD_REQUEST, + }); + }; + + if !client.enabled { + error!("OAuth client id `{}` is disabled", client.name); + let response = StandardErrorResponse::::new( + CoreErrorResponseType::UnauthorizedClient, + None, + None, + ); + return Ok(ApiResponse { + json: json!(response), + status: StatusCode::BAD_REQUEST, + }); + } + token.refresh_and_save(&appstate.pool).await?; let response = TokenRequest::refresh_token_flow(&token); token.save(&appstate.pool).await?; @@ -925,10 +979,49 @@ pub async fn token( } /// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo -pub async fn userinfo(user_info: AccessUserInfo) -> ApiResult { - let userclaims = StandardClaims::::from(&user_info.0); +pub async fn userinfo(State(appstate): State, headers: HeaderMap) -> ApiResult { + let Some(token) = headers.get(AUTHORIZATION).and_then(|value| { + if let Ok(value) = value.to_str() { + if value.to_lowercase().starts_with("bearer ") { + value.get(7..) + } else { + None + } + } else { + None + } + }) else { + return Err(WebError::Authorization("Invalid session".into())); + }; + + let Some(oauth2token) = OAuth2Token::find_access_token(&appstate.pool, token).await? else { + return Err(WebError::Authorization("Invalid token".into())); + }; + + let Some(authorized_app) = + OAuth2AuthorizedApp::find_by_id(&appstate.pool, oauth2token.oauth2authorizedapp_id).await? + else { + return Err(WebError::Authorization("Authorized app not found".into())); + }; + + let Some(client) = + OAuth2Client::find_by_id(&appstate.pool, authorized_app.oauth2client_id).await? + else { + return Err(WebError::Authorization("OAuth2 client not found".into())); + }; + + if !client.enabled { + return Err(WebError::Authorization("OAuth2 client is disabled".into())); + } + + let Some(user) = User::find_by_id(&appstate.pool, authorized_app.user_id).await? else { + return Err(WebError::Authorization("User not found".into())); + }; + + let user_claims = UserClaims::from_user(&user, &client, &oauth2token); + Ok(ApiResponse { - json: json!(userclaims), + json: json!(StandardClaims::::from(&user_claims)), status: StatusCode::OK, }) } diff --git a/crates/defguard_core/src/handlers/settings.rs b/crates/defguard_core/src/handlers/settings.rs index ee7485bca9..6d4b3df06e 100644 --- a/crates/defguard_core/src/handlers/settings.rs +++ b/crates/defguard_core/src/handlers/settings.rs @@ -2,6 +2,10 @@ use axum::{ extract::{Json, Path, State}, http::StatusCode, }; +use defguard_common::db::models::{ + Settings, SettingsEssentials, + settings::{LdapSyncStatus, SettingsPatch, update_current_settings}, +}; use serde_json::json; use struct_patch::Patch; @@ -9,14 +13,7 @@ use super::{ApiResponse, ApiResult}; use crate::{ AppState, auth::{AdminRole, SessionInfo}, - db::{ - Settings, - models::settings::{SettingsEssentials, SettingsPatch, update_current_settings}, - }, - enterprise::{ - ldap::{LDAPConnection, sync::SyncStatus}, - license::update_cached_license, - }, + enterprise::{ldap::LDAPConnection, license::update_cached_license}, error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, }; @@ -131,10 +128,7 @@ pub async fn patch_settings( context: ApiRequestContext, Json(data): Json, ) -> ApiResult { - debug!( - "Admin {} patching settings with {data:?}", - session.user.username - ); + debug!("Admin {} patching settings", session.user.username); let mut settings = Settings::get_current_settings(); // prepare clone for emitting an event let before = settings.clone(); @@ -147,19 +141,19 @@ pub async fn patch_settings( if let Some(ldap_enabled) = data.ldap_enabled { if !ldap_enabled { - settings.ldap_sync_status = SyncStatus::OutOfSync; + settings.ldap_sync_status = LdapSyncStatus::OutOfSync; } } if let Some(ldap_authority) = data.ldap_is_authoritative { if settings.ldap_is_authoritative != ldap_authority { - settings.ldap_sync_status = SyncStatus::OutOfSync; + settings.ldap_sync_status = LdapSyncStatus::OutOfSync; } } if let Some(ldap_sync_groups) = &data.ldap_sync_groups { if &settings.ldap_sync_groups != ldap_sync_groups { - settings.ldap_sync_status = SyncStatus::OutOfSync; + settings.ldap_sync_status = LdapSyncStatus::OutOfSync; } } diff --git a/crates/defguard_core/src/handlers/ssh_authorized_keys.rs b/crates/defguard_core/src/handlers/ssh_authorized_keys.rs index 91d925453a..8b6be6d37e 100644 --- a/crates/defguard_core/src/handlers/ssh_authorized_keys.rs +++ b/crates/defguard_core/src/handlers/ssh_authorized_keys.rs @@ -3,6 +3,10 @@ use axum::{ extract::{Path, Query, State}, http::StatusCode, }; +use defguard_common::db::{ + Id, + models::{AuthenticationKey, AuthenticationKeyType}, +}; use serde_json::json; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query}; use ssh_key::PublicKey; @@ -11,10 +15,7 @@ use super::{ApiResponse, ApiResult, user_for_admin_or_self}; use crate::{ appstate::AppState, auth::SessionInfo, - db::{ - Group, Id, User, - models::authentication_key::{AuthenticationKey, AuthenticationKeyType}, - }, + db::{Group, User}, error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, }; diff --git a/crates/defguard_core/src/handlers/user.rs b/crates/defguard_core/src/handlers/user.rs index 909d109439..5ea0e977d7 100644 --- a/crates/defguard_core/src/handlers/user.rs +++ b/crates/defguard_core/src/handlers/user.rs @@ -4,11 +4,12 @@ use axum::{ extract::{Json, Path, State}, http::StatusCode, }; +use defguard_mail::{Mail, templates}; use serde_json::json; use super::{ AddUserData, ApiResponse, ApiResult, PasswordChange, PasswordChangeSelf, - StartEnrollmentRequest, Username, mail::EMAIL_PASSOWRD_RESET_START_SUBJECT, + StartEnrollmentRequest, Username, mail::EMAIL_PASSWORD_RESET_START_SUBJECT, user_for_admin_or_self, }; use crate::{ @@ -31,15 +32,18 @@ use crate::{ }, error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, - mail::Mail, - server_config, templates, + is_valid_phone_number, server_config, }; +/// The maximum length for the commonName (CN) attribute in LDAP schemas is commonly set to 64 +/// characters according to the X.520 standard and many LDAP implementations like Active Directory. +pub(crate) const MAX_USERNAME_CHARS: usize = 64; + /// Verify the given username /// /// To enable LDAP sync usernames need to avoid reserved characters. /// Username requirements: -/// - 1 - 64 characters long +/// - 1 - MAX_USERNAME_CHARS characters long /// - lowercase or uppercase latin alphabet letters (A-Z, a-z) /// - digits (0-9) /// - starts with non-special character @@ -48,7 +52,7 @@ use crate::{ pub fn check_username(username: &str) -> Result<(), WebError> { // check length let length = username.len(); - if !(1..64).contains(&length) { + if !(1..MAX_USERNAME_CHARS).contains(&length) { return Err(WebError::Serialization(format!( "Username ({username}) has incorrect length" ))); @@ -280,6 +284,7 @@ pub async fn add_user( status: StatusCode::BAD_REQUEST, }); } + // check if email doesn't already exist if User::find_by_email(&appstate.pool, &user_data.email) .await? @@ -291,6 +296,18 @@ pub async fn add_user( status: StatusCode::BAD_REQUEST, }); } + + // check phone number + if let Some(ref phone) = user_data.phone { + if !is_valid_phone_number(phone) { + debug!("Invalid phone number for new user {username}: {phone}"); + return Ok(ApiResponse { + json: json!({}), + status: StatusCode::BAD_REQUEST, + }); + } + } + let password = match &user_data.password { Some(password) => { // check password strength @@ -645,11 +662,12 @@ pub async fn modify_user( context: ApiRequestContext, State(appstate): State, Path(username): Path, - Json(mut user_info): Json, + Json(user_info): Json, ) -> ApiResult { debug!("User {} updating user {username}", session.user.username); let mut user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; let groups_before = UserInfo::from_user(&appstate.pool, &user).await?.groups; + // store user before mods let before = user.clone(); let old_username = user.username.clone(); @@ -660,6 +678,18 @@ pub async fn modify_user( status: StatusCode::BAD_REQUEST, }); } + + // check phone number + if let Some(ref phone) = user_info.phone { + if !is_valid_phone_number(phone) { + debug!("Invalid phone number for user {username}: {phone}"); + return Ok(ApiResponse { + json: json!({}), + status: StatusCode::BAD_REQUEST, + }); + } + } + let status_changing = user_info.is_active != user.is_active; let mut transaction = appstate.pool.begin().await?; @@ -1086,7 +1116,7 @@ pub async fn reset_password( let mail = Mail { to: user.email.clone(), - subject: EMAIL_PASSOWRD_RESET_START_SUBJECT.into(), + subject: EMAIL_PASSWORD_RESET_START_SUBJECT.into(), content: templates::email_password_reset_mail( config.enrollment_url.clone(), enrollment.id.clone().as_str(), diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 1508bc7cdc..466cc7cb62 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -11,6 +11,8 @@ use axum::{ http::StatusCode, }; use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc}; +use defguard_common::{csv::AsCsv, db::Id}; +use defguard_mail::templates::TemplateLocation; use ipnetwork::IpNetwork; use serde_json::{Value, json}; use sqlx::PgPool; @@ -19,11 +21,10 @@ use uuid::Uuid; use super::{ApiResponse, ApiResult, WebError, device_for_admin_or_self, user_for_admin_or_self}; use crate::{ - AsCsv, appstate::AppState, auth::{AdminRole, SessionInfo}, db::{ - AddDevice, Device, GatewayEvent, Id, WireguardNetwork, + AddDevice, Device, GatewayEvent, WireguardNetwork, models::{ device::{ DeviceConfig, DeviceInfo, DeviceNetworkInfo, DeviceType, ModifyDevice, @@ -45,7 +46,6 @@ use crate::{ grpc::gateway::map::GatewayMap, handlers::mail::send_new_device_added_email, server_config, - templates::TemplateLocation, wg_config::{ImportedDevice, parse_wireguard_config}, }; diff --git a/crates/defguard_core/src/handlers/worker.rs b/crates/defguard_core/src/handlers/worker.rs index 07f1388e1a..cab88e8989 100644 --- a/crates/defguard_core/src/handlers/worker.rs +++ b/crates/defguard_core/src/handlers/worker.rs @@ -4,12 +4,13 @@ use axum::{ extract::{Extension, Json, Path, State}, http::StatusCode, }; +use defguard_common::auth::claims::{Claims, ClaimsType}; use serde_json::json; use super::{ApiResponse, ApiResult}; use crate::{ appstate::AppState, - auth::{AdminRole, Claims, ClaimsType, SessionInfo}, + auth::{AdminRole, SessionInfo}, db::User, error::WebError, grpc::WorkerState, diff --git a/crates/defguard_core/src/headers.rs b/crates/defguard_core/src/headers.rs index c9b1bd796d..cb2fbbbd4c 100644 --- a/crates/defguard_core/src/headers.rs +++ b/crates/defguard_core/src/headers.rs @@ -1,16 +1,16 @@ use std::{borrow::Borrow, sync::LazyLock}; use axum::http::{HeaderName, HeaderValue}; +use defguard_common::db::{Id, models::DeviceLoginEvent}; +use defguard_mail::{ + Mail, + templates::{SessionContext, TemplateError}, +}; use sqlx::PgPool; use tokio::sync::mpsc::UnboundedSender; use uaparser::{Client, Parser, UserAgentParser}; -use crate::{ - db::{Id, Session, User, models::device_login::DeviceLoginEvent}, - handlers::mail::send_new_device_login_email, - mail::Mail, - templates::TemplateError, -}; +use crate::{db::User, handlers::mail::send_new_device_login_email}; pub(crate) const CONTENT_SECURITY_POLICY_HEADER_NAME: HeaderName = HeaderName::from_static("content-security-policy"); @@ -24,7 +24,8 @@ pub(crate) static USER_AGENT_PARSER: LazyLock = LazyLock::new(| #[must_use] pub(crate) fn get_device_info(user_agent: &str) -> String { - let client = USER_AGENT_PARSER.parse(user_agent); + let escaped = tera::escape_html(user_agent); + let client = USER_AGENT_PARSER.parse(&escaped); get_user_agent_device(&client) } @@ -90,7 +91,7 @@ fn get_user_agent_device_login_data( pub(crate) async fn check_new_device_login( pool: &PgPool, mail_tx: &UnboundedSender, - session: &Session, + session: &SessionContext, user: &User, ip_address: String, event_type: String, diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 68f82136ff..5e200f2725 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -3,7 +3,7 @@ #![allow(clippy::result_large_err)] use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, - sync::{Arc, Mutex, OnceLock, RwLock}, + sync::{Arc, LazyLock, Mutex, RwLock}, }; use crate::version::IncompatibleComponents; @@ -15,6 +15,13 @@ use axum::{ serve, }; use db::models::{device::DeviceType, wireguard::LocationMfaMode}; +use defguard_common::{ + VERSION, + auth::claims::{Claims, ClaimsType}, + config::{DefGuardConfig, InitVpnLocationArgs, server_config}, + db::init_db, +}; +use defguard_mail::Mail; use defguard_version::server::DefguardVersionLayer; use defguard_web_ui::{index, svg, web_asset}; use enterprise::{ @@ -60,6 +67,7 @@ use handlers::{ yubikey::{delete_yubikey, rename_yubikey}, }; use ipnetwork::IpNetwork; +use regex::Regex; use secrecy::ExposeSecret; use semver::Version; use sqlx::PgPool; @@ -81,18 +89,15 @@ use utoipa::{ }; use utoipa_swagger_ui::SwaggerUi; -#[cfg(feature = "wireguard")] use self::handlers::wireguard::{ add_device, add_user_devices, create_network, create_network_token, delete_device, delete_network, devices_stats, download_config, gateway_status, get_device, import_network, list_devices, list_networks, list_user_devices, modify_device, modify_network, network_details, network_stats, remove_gateway, }; -#[cfg(feature = "worker")] use self::handlers::worker::{ create_job, create_worker_token, job_status, list_workers, remove_worker, }; -#[cfg(feature = "openid")] use self::handlers::{ openid_clients::{ add_openid_client, change_openid_client, change_openid_client_state, delete_openid_client, @@ -104,10 +109,8 @@ use self::handlers::{ }; use self::{ appstate::AppState, - auth::{Claims, ClaimsType}, - config::{DefGuardConfig, InitVpnLocationArgs}, db::{ - AppEvent, Device, GatewayEvent, User, WireguardNetwork, init_db, + AppEvent, Device, GatewayEvent, User, WireguardNetwork, models::wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL}, }, handlers::{ @@ -140,9 +143,7 @@ use self::{ add_webhook, change_enabled, change_webhook, delete_webhook, get_webhook, list_webhooks, }, }, - mail::Mail, }; -#[cfg(any(feature = "openid", feature = "worker"))] use self::{ auth::failed_login::FailedLoginMap, db::models::oauth2client::OAuth2Client, @@ -152,21 +153,14 @@ use self::{ pub mod appstate; pub mod auth; -pub mod config; pub mod db; pub mod enterprise; mod error; pub mod events; -pub mod globals; pub mod grpc; pub mod handlers; pub mod headers; -pub mod hex; -pub mod mail; -pub(crate) mod random; -pub mod secret; pub mod support; -pub mod templates; pub mod updates; pub mod utility_thread; pub mod version; @@ -180,18 +174,10 @@ extern crate tracing; #[macro_use] extern crate serde; -// helper for easier migration handling with a custom `migration` folder location -// reference: https://docs.rs/sqlx/latest/sqlx/attr.test.html#automatic-migrations-requires-migrate-feature -pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../../migrations"); - -pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "+", env!("VERGEN_GIT_SHA")); -pub static SERVER_CONFIG: OnceLock = OnceLock::new(); - -pub(crate) fn server_config() -> &'static DefGuardConfig { - SERVER_CONFIG - .get() - .expect("Server configuration not set yet") -} +static PHONE_NUMBER_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^(\+?\d{1,3}\s?)?(\(\d{1,3}\)|\d{1,3})[-\s]?\d{1,4}[-\s]?\d{1,4}?$") + .expect("Failed to parse phone number regex") +}); // WireGuard key length in bytes. pub(crate) const KEY_LENGTH: usize = 32; @@ -537,7 +523,6 @@ pub fn build_webapp( ), ); - #[cfg(feature = "openid")] let webapp = webapp .nest( "/api/v1/oauth", @@ -581,7 +566,6 @@ pub fn build_webapp( .route("/alias/apply", put(apply_acl_aliases)), ); - #[cfg(feature = "wireguard")] let webapp = webapp.nest( "/api/v1", Router::new() @@ -655,7 +639,6 @@ pub fn build_webapp( .layer(Extension(gateway_state)), ); - #[cfg(feature = "worker")] let webapp = webapp.nest( "/api/v1/worker", Router::new() @@ -729,11 +712,12 @@ pub async fn run_web_server( incompatible_components, ); info!("Started web services"); + let server_config = server_config(); let addr = SocketAddr::new( - server_config() + server_config .http_bind_address .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)), - server_config().http_port, + server_config.http_port, ); let listener = TcpListener::bind(&addr).await?; serve( @@ -830,7 +814,6 @@ pub async fn init_dev_env(config: &DefGuardConfig) { .expect("Could not assign IP to device"); } - #[cfg(feature = "openid")] for app_id in 1..=3 { OAuth2Client::new( vec![format!("https://app-{app_id}.com")], @@ -947,20 +930,39 @@ pub async fn init_vpn_location( Ok(token) } -pub trait AsCsv { - fn as_csv(&self) -> String; +pub(crate) fn is_valid_phone_number(number: &str) -> bool { + PHONE_NUMBER_REGEX.is_match(number) } -impl AsCsv for I -where - I: ?Sized + std::iter::IntoIterator, - for<'a> &'a I: IntoIterator, - T: ToString, -{ - fn as_csv(&self) -> String { - self.into_iter() - .map(ToString::to_string) - .collect::>() - .join(",") +#[cfg(test)] +mod test { + + use super::is_valid_phone_number; + + #[test] + fn test_is_valid_phone_number_dg25_10() { + let valid_numbers = &[ + "+48 (91) 123-456", + "123 456 7890", + "+1 (202) 555-0173", + "91-1234-5678", + "(22) 567 890", + ]; + for number in valid_numbers { + assert!(is_valid_phone_number(number)); + } + + let invalid_numbers = &[ + "4*4", + "+48 123456789", + "123-456-789-0000", + "(+48) 123 456", + "202.555.0173", + "(12345) 6789", + "+48 (91) 123-456 000 111", + ]; + for number in invalid_numbers { + assert!(!is_valid_phone_number(number)); + } } } diff --git a/crates/defguard_core/src/support.rs b/crates/defguard_core/src/support.rs index fd2ccfa037..753daa5c5e 100644 --- a/crates/defguard_core/src/support.rs +++ b/crates/defguard_core/src/support.rs @@ -5,10 +5,13 @@ use serde_json::{Value, json, value::to_value}; use sqlx::PgPool; use crate::{ - VERSION, - db::{Id, Settings, User, WireguardNetwork, models::device::WireguardNetworkDevice}, + db::{User, WireguardNetwork, models::device::WireguardNetworkDevice}, server_config, }; +use defguard_common::{ + VERSION, + db::{Id, models::Settings}, +}; /// Unwraps the result returning a JSON representation of value or error fn unwrap_json(result: Result) -> Value { diff --git a/crates/defguard_core/src/updates.rs b/crates/defguard_core/src/updates.rs index 2cbfb96d71..e286c8ea8c 100644 --- a/crates/defguard_core/src/updates.rs +++ b/crates/defguard_core/src/updates.rs @@ -1,13 +1,11 @@ use std::{env, time::Duration}; use chrono::NaiveDate; +use defguard_common::{CARGO_VERSION, global_value}; use semver::Version; -use crate::global_value; - const PRODUCT_NAME: &str = "Defguard"; const UPDATES_URL: &str = "https://pkgs.defguard.net/api/update/check"; -const VERSION: &str = env!("CARGO_PKG_VERSION"); const REQUEST_TIMEOUT: Duration = Duration::from_secs(10); #[derive(Deserialize, Debug, Serialize)] @@ -26,7 +24,7 @@ global_value!(NEW_UPDATE, Option, None, set_update, get_update); async fn fetch_update() -> Result { let body = serde_json::json!({ "product": PRODUCT_NAME, - "client_version": VERSION, + "client_version": CARGO_VERSION, "operating_system": env::consts::OS, }); let response = reqwest::Client::new() @@ -41,7 +39,7 @@ async fn fetch_update() -> Result { pub(crate) async fn do_new_version_check() -> Result<(), anyhow::Error> { debug!("Checking for new version of Defguard."); let update = fetch_update().await?; - let current_version = Version::parse(VERSION)?; + let current_version = Version::parse(CARGO_VERSION)?; let new_version = Version::parse(&update.version)?; if new_version > current_version { if update.critical { diff --git a/crates/defguard_core/src/utility_thread.rs b/crates/defguard_core/src/utility_thread.rs index 0e608d1275..6533ff71c5 100644 --- a/crates/defguard_core/src/utility_thread.rs +++ b/crates/defguard_core/src/utility_thread.rs @@ -1,5 +1,6 @@ use std::{collections::HashSet, time::Duration}; +use defguard_common::db::Id; use sqlx::{PgPool, query_as}; use tokio::{ sync::broadcast::Sender, @@ -8,7 +9,7 @@ use tokio::{ use tracing::Instrument; use crate::{ - db::{GatewayEvent, Id, WireguardNetwork}, + db::{GatewayEvent, WireguardNetwork}, enterprise::{ db::models::acl::{AclRule, RuleState}, directory_sync::{do_directory_sync, get_directory_sync_interval}, diff --git a/crates/defguard_core/src/version.rs b/crates/defguard_core/src/version.rs index c4eda787d1..5285e2e2eb 100644 --- a/crates/defguard_core/src/version.rs +++ b/crates/defguard_core/src/version.rs @@ -229,6 +229,7 @@ impl Hash for IncompatibleGatewayData { } impl IncompatibleGatewayData { + #[must_use] pub fn new( version: Option, hostname: Option, @@ -238,8 +239,8 @@ impl IncompatibleGatewayData { Self { version, hostname, - created, network_id, + created, } } @@ -276,6 +277,7 @@ impl PartialEq for IncompatibleProxyData { impl Eq for IncompatibleProxyData {} impl IncompatibleProxyData { + #[must_use] pub fn new(version: Option) -> Self { let created = Utc::now().naive_utc(); Self { version, created } diff --git a/crates/defguard_core/src/wg_config.rs b/crates/defguard_core/src/wg_config.rs index 4dcdc47ff4..2a42ea68dd 100644 --- a/crates/defguard_core/src/wg_config.rs +++ b/crates/defguard_core/src/wg_config.rs @@ -171,7 +171,7 @@ pub(crate) fn parse_wireguard_config( #[cfg(test)] mod test { use super::*; - use crate::db::NoId; + use defguard_common::db::NoId; #[test] fn test_parse_config() { diff --git a/crates/defguard_core/src/wireguard_peer_disconnect.rs b/crates/defguard_core/src/wireguard_peer_disconnect.rs index 06be7734d9..537db19a30 100644 --- a/crates/defguard_core/src/wireguard_peer_disconnect.rs +++ b/crates/defguard_core/src/wireguard_peer_disconnect.rs @@ -11,6 +11,7 @@ use std::{ }; use chrono::NaiveDateTime; +use defguard_common::db::{Id, models::ModelError}; use sqlx::{Error as SqlxError, PgPool, query_as}; use thiserror::Error; use tokio::{ @@ -23,10 +24,9 @@ use tokio::{ use crate::{ db::{ - Device, GatewayEvent, Id, WireguardNetwork, + Device, GatewayEvent, WireguardNetwork, models::{ device::{DeviceInfo, DeviceNetworkInfo, DeviceType, WireguardNetworkDevice}, - error::ModelError, wireguard::{LocationMfaMode, WireguardNetworkError}, }, }, diff --git a/crates/defguard_core/tests/integration/api/acl.rs b/crates/defguard_core/tests/integration/api/acl.rs index 9bf2ffad6f..237b573fdd 100644 --- a/crates/defguard_core/tests/integration/api/acl.rs +++ b/crates/defguard_core/tests/integration/api/acl.rs @@ -1,10 +1,11 @@ -use defguard_core::{ +use defguard_common::{ config::DefGuardConfig, + db::{Id, models::settings::initialize_current_settings}, +}; +use defguard_core::{ db::{ - Device, Group, Id, User, WireguardNetwork, - models::{ - device::DeviceType, settings::initialize_current_settings, wireguard::LocationMfaMode, - }, + Device, Group, User, WireguardNetwork, + models::{device::DeviceType, wireguard::LocationMfaMode}, }, enterprise::{ db::models::acl::{AclAlias, AclRule, AliasKind, AliasState, RuleState}, diff --git a/crates/defguard_core/tests/integration/api/api_tokens.rs b/crates/defguard_core/tests/integration/api/api_tokens.rs index d707abe0c8..a692c156c2 100644 --- a/crates/defguard_core/tests/integration/api/api_tokens.rs +++ b/crates/defguard_core/tests/integration/api/api_tokens.rs @@ -12,7 +12,7 @@ use serde::Deserialize; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use super::common::{make_client, make_client_with_state, setup_pool}; +use super::common::{make_client, make_test_client, setup_pool}; use crate::api::common::fetch_user_details; #[sqlx::test] @@ -62,7 +62,7 @@ async fn test_normal_user_cannot_access_token_endpoints( async fn test_normal_user_cannot_use_token_auth(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, state) = make_client_with_state(pool).await; + let (client, state) = make_test_client(pool).await; // sidestep API access restrictions by creating a token manually let token_string = "test-token-string"; diff --git a/crates/defguard_core/tests/integration/api/auth.rs b/crates/defguard_core/tests/integration/api/auth.rs index f7150b9a36..ad43507125 100644 --- a/crates/defguard_core/tests/integration/api/auth.rs +++ b/crates/defguard_core/tests/integration/api/auth.rs @@ -2,11 +2,10 @@ use std::time::SystemTime; use chrono::DateTime; use claims::{assert_err, assert_ok}; +use defguard_common::db::models::{MFAMethod, Settings, settings::update_current_settings}; use defguard_core::{ auth::{TOTP_CODE_DIGITS, TOTP_CODE_VALIDITY_PERIOD}, - db::{ - MFAInfo, MFAMethod, Settings, User, UserDetails, models::settings::update_current_settings, - }, + db::{MFAInfo, User, UserDetails}, handlers::{Auth, AuthCode, AuthResponse, AuthTotp}, }; use reqwest::{StatusCode, header::USER_AGENT}; @@ -20,9 +19,11 @@ use totp_lite::{Sha1, totp_custom}; use webauthn_authenticator_rs::{WebauthnAuthenticator, prelude::Url, softpasskey::SoftPasskey}; use webauthn_rs::prelude::{CreationChallengeResponse, RequestChallengeResponse}; +use crate::api::common::client::TestResponse; + use super::common::{ - X_FORWARDED_FOR, fetch_user_details, make_client, make_client_with_db, make_client_with_state, - make_test_client, setup_pool, + X_FORWARDED_FOR, fetch_user_details, make_client, make_client_with_db, make_test_client, + setup_pool, }; static SESSION_COOKIE_NAME: &str = "defguard_session"; @@ -98,6 +99,57 @@ async fn test_login_bruteforce(_: PgPoolOptions, options: PgConnectOptions) { } } +#[sqlx::test] +async fn dg25_21_test_login_enumeration(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let client = make_client(pool).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 response = client.post("/api/v1/auth").json(&user_auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + async fn responses_eq(response1: TestResponse, response2: TestResponse) -> bool { + // omit date header + let mut headers1 = response1.headers().clone(); + headers1.remove("date"); + let mut headers2 = response2.headers().clone(); + headers2.remove("date"); + let headers = headers1 == headers2; + + let status = response1.status() == response2.status(); + let body = response1.bytes().await == response2.bytes().await; + + status && headers && body + } + + // regular user + let user_auth = Auth::new("hpotter", "invalid"); + let response_existing_user = client.post("/api/v1/auth").json(&user_auth).send().await; + let user_auth = Auth::new("nothpotter", "invalid"); + let response_nonexisting_user = client.post("/api/v1/auth").json(&user_auth).send().await; + assert!(responses_eq(response_existing_user, response_nonexisting_user).await); + + // admin user + let user_auth = Auth::new("admin", "invalid"); + let response_existing_user = client.post("/api/v1/auth").json(&user_auth).send().await; + let user_auth = Auth::new("notadmin", "invalid"); + let response_nonexisting_user = client.post("/api/v1/auth").json(&user_auth).send().await; + assert!(responses_eq(response_existing_user, response_nonexisting_user).await); + + // response for admin = response for regular user + let user_auth = Auth::new("admin", "invalid"); + let response_existing_user = client.post("/api/v1/auth").json(&user_auth).send().await; + let user_auth = Auth::new("hpotter", "invalid"); + let response_nonexisting_user = client.post("/api/v1/auth").json(&user_auth).send().await; + assert!(responses_eq(response_existing_user, response_nonexisting_user).await); +} + #[sqlx::test] async fn test_login_disabled(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; @@ -339,7 +391,7 @@ fn extract_email_code(content: &str) -> &str { async fn test_email_mfa(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, state) = make_client_with_state(pool).await; + let (client, state) = make_test_client(pool).await; let pool = state.pool; let mut mail_rx = state.mail_rx; @@ -485,7 +537,7 @@ async fn test_email_mfa(_: PgPoolOptions, options: PgConnectOptions) { async fn dg25_15_test_email_mfa_brute_force(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, state) = make_client_with_state(pool).await; + let (client, state) = make_test_client(pool).await; let pool = state.pool; let mut mail_rx = state.mail_rx; @@ -551,7 +603,7 @@ async fn test_webauthn(_: PgPoolOptions, options: PgConnectOptions) { let (client, pool) = make_client_with_db(pool).await; let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true)); - let origin = Url::parse("http://localhost:8000").unwrap(); + let origin = Url::parse(&client.base_url()).unwrap(); // login let auth = Auth::new("hpotter", "pass123"); @@ -670,7 +722,7 @@ async fn test_cannot_skip_security_key_by_adding_yubikey( let client = make_client(pool).await; let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true)); - let origin = Url::parse("http://localhost:8000").unwrap(); + let origin = Url::parse(&client.base_url()).unwrap(); // login let auth = Auth::new("hpotter", "pass123"); @@ -753,7 +805,7 @@ async fn test_mfa_method_is_updated_when_removing_last_webauthn_passkey( // WebAuthn registration let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true)); - let origin = Url::parse("http://localhost:8000").unwrap(); + let origin = Url::parse(&client.base_url()).unwrap(); let response = client.post("/api/v1/auth/webauthn/init").send().await; assert_eq!(response.status(), StatusCode::OK); diff --git a/crates/defguard_core/tests/integration/api/common/client.rs b/crates/defguard_core/tests/integration/api/common/client.rs index 61a45135dc..9292b31a3e 100644 --- a/crates/defguard_core/tests/integration/api/common/client.rs +++ b/crates/defguard_core/tests/integration/api/common/client.rs @@ -176,8 +176,6 @@ impl RequestBuilder { /// This is conventient for tests where panics are what you want. For access to /// non-panicking versions or the complete `Response` API use `into_inner()` or /// `as_ref()`. -#[derive(Debug)] - pub struct TestResponse { response: reqwest::Response, } diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index ea5b3393ab..669ecc9649 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -1,23 +1,26 @@ pub(crate) mod client; -use std::sync::{Arc, Mutex}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::{Arc, Mutex}, +}; -pub use defguard_core::db::setup_pool; -use defguard_core::{ +pub use defguard_common::db::setup_pool; +use defguard_common::{ VERSION, + config::DefGuardConfig, + db::{Id, NoId, models::settings::initialize_current_settings}, +}; +use defguard_core::{ auth::failed_login::FailedLoginMap, build_webapp, - config::DefGuardConfig, - db::{ - AppEvent, GatewayEvent, Id, NoId, User, UserDetails, - models::settings::initialize_current_settings, - }, + db::{AppEvent, GatewayEvent, User, UserDetails}, enterprise::license::{License, set_cached_license}, events::ApiEvent, grpc::{WorkerState, gateway::map::GatewayMap}, handlers::Auth, - mail::Mail, }; +use defguard_mail::Mail; use reqwest::{StatusCode, header::HeaderName}; use semver::Version; use serde::de::DeserializeOwned; @@ -140,10 +143,12 @@ pub(crate) async fn make_base_client( } pub(crate) async fn make_test_client(pool: PgPool) -> (TestClient, ClientState) { - let listener = TcpListener::bind("127.0.0.1:0") + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); + let listener = TcpListener::bind(addr) .await .expect("Could not bind ephemeral socket"); - let config = init_config(None); + let port = listener.local_addr().unwrap().port(); + let config = init_config(Some(&format!("http://localhost:{port}"))); initialize_users(&pool, &config).await; initialize_current_settings(&pool) .await @@ -236,11 +241,6 @@ pub(crate) async fn make_client_with_db(pool: PgPool) -> (TestClient, PgPool) { (client, client_state.pool) } -pub(crate) async fn make_client_with_state(pool: PgPool) -> (TestClient, ClientState) { - let (client, client_state) = make_test_client(pool).await; - (client, client_state) -} - pub(crate) async fn authenticate_admin(client: &TestClient) { let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; diff --git a/crates/defguard_core/tests/integration/api/forward_auth.rs b/crates/defguard_core/tests/integration/api/forward_auth.rs index 83b97c1ba9..7525383ca0 100644 --- a/crates/defguard_core/tests/integration/api/forward_auth.rs +++ b/crates/defguard_core/tests/integration/api/forward_auth.rs @@ -1,4 +1,5 @@ -use defguard_core::{SERVER_CONFIG, handlers::Auth}; +use defguard_common::config::SERVER_CONFIG; +use defguard_core::handlers::Auth; use reqwest::StatusCode; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; diff --git a/crates/defguard_core/tests/integration/api/mod.rs b/crates/defguard_core/tests/integration/api/mod.rs index 4891501678..218ff73cf8 100644 --- a/crates/defguard_core/tests/integration/api/mod.rs +++ b/crates/defguard_core/tests/integration/api/mod.rs @@ -19,3 +19,5 @@ mod wireguard_network_devices; mod wireguard_network_import; mod wireguard_network_stats; mod worker; + +const TEST_SERVER_URL: &str = "http://localhost:3000/"; diff --git a/crates/defguard_core/tests/integration/api/oauth.rs b/crates/defguard_core/tests/integration/api/oauth.rs index 06d86dbe3b..4e7329734f 100644 --- a/crates/defguard_core/tests/integration/api/oauth.rs +++ b/crates/defguard_core/tests/integration/api/oauth.rs @@ -1,8 +1,9 @@ use std::borrow::Cow; +use defguard_common::db::Id; use defguard_core::{ db::{ - Id, OAuth2AuthorizedApp, + OAuth2AuthorizedApp, models::{ NewOpenIDClient, oauth2client::{OAuth2Client, OAuth2ClientSafe}, diff --git a/crates/defguard_core/tests/integration/api/openid.rs b/crates/defguard_core/tests/integration/api/openid.rs index 7b701bba14..a53a0f0c84 100644 --- a/crates/defguard_core/tests/integration/api/openid.rs +++ b/crates/defguard_core/tests/integration/api/openid.rs @@ -2,9 +2,10 @@ use std::str::FromStr; use axum::http::header::ToStrError; use claims::assert_err; +use defguard_common::db::Id; use defguard_core::{ db::{ - Id, User, + User, models::{NewOpenIDClient, oauth2client::OAuth2Client}, }, handlers::Auth, @@ -19,15 +20,19 @@ use openidconnect::{ http::Method, }; use reqwest::{ - StatusCode, - header::{AUTHORIZATION, CONTENT_TYPE, HeaderName, USER_AGENT}, + StatusCode, Url, + header::{AUTHORIZATION, CONTENT_TYPE, HeaderName, LOCATION, USER_AGENT}, }; use rsa::RsaPrivateKey; use serde::Deserialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use super::common::{ - client::TestClient, make_client, make_client_with_state, make_test_client, setup_pool, +use super::{ + TEST_SERVER_URL, + common::{ + client::{TestClient, TestResponse}, + make_client, make_test_client, setup_pool, + }, }; #[derive(Deserialize)] @@ -48,7 +53,7 @@ async fn test_openid_client(_: PgPoolOptions, options: PgConnectOptions) { let mut openid_client = NewOpenIDClient { name: "Test".into(), - redirect_uri: vec!["http://localhost:3000/".into()], + redirect_uri: vec![TEST_SERVER_URL.into()], scope: vec!["openid".into()], enabled: true, }; @@ -101,13 +106,13 @@ async fn test_openid_client(_: PgPoolOptions, options: PgConnectOptions) { async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let (client, state) = make_test_client(pool).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 openid_client = NewOpenIDClient { name: "Test".into(), - redirect_uri: vec!["http://localhost:3000/".into()], + redirect_uri: vec![TEST_SERVER_URL.into(), "http://safe.net".into()], scope: vec!["openid".into()], enabled: true, }; @@ -125,6 +130,7 @@ async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { let response = client.get("/api/v1/oauth").send().await; assert_eq!(response.status(), StatusCode::OK); + // Try invalid request for `response_type = code id_token token`. let response = client .post(format!( "/api/v1/oauth/authorize?\ @@ -147,6 +153,7 @@ async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); assert!(location.contains("error=invalid_request")); + // Try invalid request for `response_type = id_token`. let response = client .post(format!( "/api/v1/oauth/authorize?\ @@ -193,11 +200,11 @@ async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { .to_str() .unwrap(); let (location, query) = location.split_once('?').unwrap(); - assert_eq!(location, "http://localhost:3000/"); + assert_eq!(location, TEST_SERVER_URL); let auth_response: AuthenticationResponse = serde_qs::from_str(query).unwrap(); assert_eq!(auth_response.state, "ABCDEF"); - // exchange wrong code for token should fail + // Exchanging a wrong code for a token should fail. let response = client .post("/api/v1/oauth/token") .header(CONTENT_TYPE, "application/x-www-form-urlencoded") @@ -213,23 +220,33 @@ async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { .await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); - // exchange correct code for token + // Exchange correct code for a token. + let token_body = format!( + "grant_type=authorization_code&\ + code={}&\ + redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&\ + client_id={}&\ + client_secret={}", + auth_response.code, openid_client.client_id, openid_client.client_secret + ); let response = client .post("/api/v1/oauth/token") .header(CONTENT_TYPE, "application/x-www-form-urlencoded") - .body(format!( - "grant_type=authorization_code&\ - code={}&\ - redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&\ - client_id={}&\ - client_secret={}", - auth_response.code, openid_client.client_id, openid_client.client_secret - )) + .body(token_body.clone()) .send() .await; assert_eq!(response.status(), StatusCode::OK); - // make sure access token cannot be used to manage defguard server itself + // Try to get another authentication code for the same code. + let another_response = client + .post("/api/v1/oauth/token") + .header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .body(token_body) + .send() + .await; + assert_eq!(another_response.status(), StatusCode::BAD_REQUEST); + + // Make sure access token cannot be used to manage Defguard server itself. client.post("/api/v1/auth/logout").send().await; let token_response: CoreTokenResponse = response.json().await; let bearer = format!("Bearer {}", token_response.access_token().secret()); @@ -247,10 +264,16 @@ async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // log back in - let auth = Auth::new("admin", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); + let fallback_url = state + .config + .url + .to_string() + .trim_end_matches('/') + .to_string(); + // check code cannot be reused let response = client .post("/api/v1/oauth/token") @@ -286,34 +309,89 @@ async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { .unwrap() .to_str() .unwrap(); + assert_eq!(response.status(), StatusCode::FOUND); + assert!(location.starts_with(&fallback_url)); assert!(location.contains("error")); - // test wrong invalid uri + // test invalid redirect uri let response = client - .post( + .post(format!( "/api/v1/oauth/authorize?\ response_type=code&\ - client_id=1&\ + client_id={}&\ redirect_uri=http%3A%2F%example%3A3000%2F&\ scope=openid&\ state=ABCDEF&\ nonce=blabla", - ) + openid_client.client_id + )) .send() .await; - assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.status(), StatusCode::FOUND); + assert!(location.starts_with(&fallback_url)); + + // test non-whitelisted uri + let response = client + .post(format!( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id={}&\ + redirect_uri=http%3A%2F%2Fexample%3A3000%3Fvalue1=one%26value2=two&\ + scope=openid&\ + state=ABCDEF&\ + nonce=blabla", + openid_client.client_id + )) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + let location = response + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap(); + assert!(location.starts_with(&fallback_url)); + assert!(location.contains("error=access_denied")); + + // test whitelisted uri, invalid scope + let response = client + .post(format!( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id={}&\ + redirect_uri=http://safe.net&\ + scope=profile&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + openid_client.client_id + )) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + let location = response + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap(); + assert!(location.starts_with("http://safe.net")); + assert!(location.contains("error=invalid_scope")); // test wrong redirect uri let response = client - .post( + .post(format!( "/api/v1/oauth/authorize?\ response_type=code&\ - client_id=1&\ + client_id={}&\ redirect_uri=http%3A%2F%2Fexample%3A3000%3Fvalue1=one%26value2=two&\ scope=openid&\ state=ABCDEF&\ + allow=true&\ nonce=blabla", - ) + openid_client.client_id + )) .send() .await; assert_eq!(response.status(), StatusCode::FOUND); @@ -323,9 +401,8 @@ async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { .unwrap() .to_str() .unwrap(); + assert!(location.starts_with(&fallback_url)); assert!(location.contains("error=access_denied")); - assert!(location.contains("value1=")); - assert!(location.contains("value2=")); // test allow false let response = client @@ -349,6 +426,7 @@ async fn test_openid_flow(_: PgPoolOptions, options: PgConnectOptions) { .unwrap() .to_str() .unwrap(); + assert!(location.starts_with(TEST_SERVER_URL)); assert!(location.contains("error=access_denied")); } @@ -398,7 +476,7 @@ static FAKE_REDIRECT_URI: &str = "http://test.server.tnt:12345/"; async fn test_openid_authorization_code(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, state) = make_client_with_state(pool).await; + let (client, state) = make_test_client(pool).await; let config = state.config; let issuer_url = IssuerUrl::from_url(config.url.clone()); @@ -496,11 +574,249 @@ async fn test_openid_authorization_code(_: PgPoolOptions, options: PgConnectOpti assert!(refresh_response.refresh_token().is_some()); } +#[sqlx::test] +async fn dg25_20_test_openid_disabled_client_doesnt_generate_code( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let (client, state) = make_test_client(pool).await; + let config = state.config; + + let issuer_url = IssuerUrl::from_url(config.url.clone()); + + // discover OpenID service + let provider_metadata = + CoreProviderMetadata::discover_async(issuer_url, &|r| http_client(r, &client)) + .await + .unwrap(); + + // create OAuth2 client (initially enabled) + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + let oauth2client = NewOpenIDClient { + name: "My test client".into(), + redirect_uri: vec![FAKE_REDIRECT_URI.into()], + scope: vec!["openid".into()], + enabled: true, + }; + let response = client + .post("/api/v1/oauth") + .json(&oauth2client) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let oauth2client: OAuth2Client = response.json().await; + assert_eq!(oauth2client.name, "My test client"); + assert_eq!(oauth2client.scope[0], "openid"); + assert_eq!(oauth2client.client_id.len(), 16); + assert_eq!(oauth2client.client_secret.len(), 32); + + let client_id = ClientId::new(oauth2client.client_id.clone()); + let client_secret = ClientSecret::new(oauth2client.client_secret); + let core_client = + CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) + .set_redirect_uri(RedirectUrl::new(FAKE_REDIRECT_URI.into()).unwrap()); + let (authorize_url, _csrf_state, _nonce) = core_client + .authorize_url( + AuthenticationFlow::::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("profile".to_string())) + .url(); + assert_eq!(authorize_url.scheme(), "http"); + assert_eq!(authorize_url.host_str(), Some("localhost")); + assert_eq!(authorize_url.path(), "/api/v1/oauth/authorize"); + + // verify that authorization works when client is enabled + let uri = format!( + "{}?allow=true&{}", + authorize_url.path(), + authorize_url.query().unwrap() + ); + let response = client.post(uri.clone()).send().await; + assert_eq!(response.status(), StatusCode::FOUND); + let location = response + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap(); + let (location, query) = location.split_once('?').unwrap(); + assert_eq!(location, FAKE_REDIRECT_URI); + let auth_response: AuthenticationResponse = serde_qs::from_str(query).unwrap(); + // Verify we got a valid authorization code + assert!(!auth_response.code.is_empty()); + + // Now disable the OAuth2 client + let disabled_oauth2client = NewOpenIDClient { + name: "My test client".into(), + redirect_uri: vec![FAKE_REDIRECT_URI.into()], + scope: vec!["openid".into()], + enabled: false, + }; + let response = client + .put(format!("/api/v1/oauth/{}", oauth2client.client_id)) + .json(&disabled_oauth2client) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + let response = client.post(uri).send().await; + assert_eq!(response.status(), StatusCode::FOUND); + let location = response + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap(); + + assert!(location.contains("error=unauthorized_client")); +} + +#[sqlx::test] +async fn dg25_25_openid_disabled_client_userinfo_fails( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let (client, state) = make_test_client(pool).await; + let mut config = state.config; + + let mut rng = rand::thread_rng(); + config.openid_signing_key = RsaPrivateKey::new(&mut rng, 2048).ok(); + + let issuer_url = IssuerUrl::from_url(config.url.clone()); + + // discover OpenID service + let provider_metadata = + CoreProviderMetadata::discover_async(issuer_url, &|r| http_client(r, &client)) + .await + .unwrap(); + + // create OAuth2 client (initially enabled) + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + let oauth2client = NewOpenIDClient { + name: "My test client".into(), + redirect_uri: vec![FAKE_REDIRECT_URI.into()], + scope: vec!["openid".into(), "email".into(), "profile".into()], + enabled: true, + }; + let response = client + .post("/api/v1/oauth") + .json(&oauth2client) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let oauth2client: OAuth2Client = response.json().await; + assert_eq!(oauth2client.name, "My test client"); + + // start the Authorization Code Flow with PKCE + let client_id = ClientId::new(oauth2client.client_id.clone()); + let client_secret = ClientSecret::new(oauth2client.client_secret); + let core_client = + CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) + .set_redirect_uri(RedirectUrl::new(FAKE_REDIRECT_URI.into()).unwrap()); + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + let (authorize_url, _csrf_state, nonce) = core_client + .authorize_url( + AuthenticationFlow::::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("profile".to_string())) + .set_pkce_challenge(pkce_challenge) + .url(); + + // obtain authorization code while client is enabled + let uri = format!( + "{}?allow=true&{}", + authorize_url.path(), + authorize_url.query().unwrap() + ); + let response = client.post(uri).send().await; + assert_eq!(response.status(), StatusCode::FOUND); + let location = response + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap(); + let (location, query) = location.split_once('?').unwrap(); + assert_eq!(location, FAKE_REDIRECT_URI); + let auth_response: AuthenticationResponse = serde_qs::from_str(query).unwrap(); + + // exchange authorization code for token while client is enabled + let token_response = core_client + .exchange_code(AuthorizationCode::new(auth_response.code.into())) + .unwrap() + .set_pkce_verifier(pkce_verifier) + .request_async(&|r| http_client(r, &client)) + .await + .unwrap(); + + // verify id token works while client is enabled + let id_token_verifier = core_client.id_token_verifier(); + let _id_token_claims = token_response + .extra_fields() + .id_token() + .expect("Server did not return an ID token") + .claims(&id_token_verifier, &nonce) + .unwrap(); + + // verify userinfo works while client is enabled + let userinfo_claims: UserInfoClaims = core_client + .user_info(token_response.access_token().clone(), None) + .expect("Missing info endpoint") + .request_async(&|r| http_client(r, &client)) + .await + .unwrap(); + + // Verify we got valid userinfo + assert!(userinfo_claims.email().is_some()); + + // Now disable the OAuth2 client + let disabled_oauth2client = NewOpenIDClient { + name: "My test client".into(), + redirect_uri: vec![FAKE_REDIRECT_URI.into()], + scope: vec!["openid".into(), "email".into(), "profile".into()], + enabled: false, + }; + let response = client + .put(format!("/api/v1/oauth/{}", oauth2client.client_id)) + .json(&disabled_oauth2client) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Try to access userinfo with disabled client, should fail + let userinfo_result: Result, _> = + core_client + .user_info(token_response.access_token().clone(), None) + .expect("Missing info endpoint") + .request_async(&|r| http_client(r, &client)) + .await; + + // The userinfo request should fail when client is disabled + assert!( + userinfo_result.is_err(), + "Userinfo should fail when client is disabled" + ); +} + #[sqlx::test] async fn test_openid_authorization_code_with_pkce(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, state) = make_client_with_state(pool).await; + let (client, state) = make_test_client(pool).await; let mut config = state.config; let mut rng = rand::thread_rng(); @@ -616,7 +932,7 @@ async fn dg25_23_test_openid_client_scope_change_clears_authorizations( options: PgConnectOptions, ) { let pool = setup_pool(options).await; - let (client, state) = make_client_with_state(pool).await; + let (client, state) = make_test_client(pool).await; let admin = User::find_by_username(&state.pool, "admin") .await .unwrap() @@ -630,7 +946,7 @@ async fn dg25_23_test_openid_client_scope_change_clears_authorizations( // Create OAuth2 client with initial scopes let oauth2client = NewOpenIDClient { name: "Test Client".into(), - redirect_uri: vec!["http://localhost:3000/".into()], + redirect_uri: vec![TEST_SERVER_URL.into()], scope: vec!["openid".into(), "email".into()], enabled: true, }; @@ -677,7 +993,7 @@ async fn dg25_23_test_openid_client_scope_change_clears_authorizations( // Update the client with different scopes let updated_client = NewOpenIDClient { name: "Test Client".into(), - redirect_uri: vec!["http://localhost:3000/".into()], + redirect_uri: vec![TEST_SERVER_URL.into()], scope: vec!["openid".into(), "profile".into()], // Changed from email to profile enabled: true, }; @@ -737,7 +1053,7 @@ async fn dg25_23_test_openid_client_scope_change_clears_authorizations( // Update the client without changing scopes (only name) let same_scope_update = NewOpenIDClient { name: "Test Client Updated Name".into(), - redirect_uri: vec!["http://localhost:3000/".into()], + redirect_uri: vec![TEST_SERVER_URL.into()], scope: vec!["openid".into(), "profile".into()], // Same scopes enabled: true, }; @@ -763,6 +1079,187 @@ async fn dg25_23_test_openid_client_scope_change_clears_authorizations( ); } +#[sqlx::test] +async fn dg25_17_test_openid_open_redirects(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (client, state) = make_test_client(pool).await; + let _admin = User::find_by_username(&state.pool, "admin") + .await + .unwrap() + .unwrap(); + + // Authenticate admin + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create OAuth2 client + let oauth2client = NewOpenIDClient { + name: "Test Client".into(), + redirect_uri: vec![TEST_SERVER_URL.into(), "http://safe.net/".into()], + scope: vec!["openid".into(), "email".into()], + enabled: true, + }; + + let response = client + .post("/api/v1/oauth") + .json(&oauth2client) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let oauth2client: OAuth2Client = response.json().await; + + fn redirect_url(response: &TestResponse) -> String { + Url::parse(response.headers().get(LOCATION).unwrap().to_str().unwrap()) + .unwrap() + .origin() + .ascii_serialization() + } + + let fallback_url = state + .config + .url + .to_string() + .trim_end_matches('/') + .to_string(); + + // Try to authorize with allowed redirect url - invalid client id + let response = client + .post( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id=xxx&\ + redirect_uri=http://localhost:3000&\ + scope=openid email&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + ) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(redirect_url(&response), fallback_url); + + let response = client + .get( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id=xxx&\ + redirect_uri=http://localhost:3000&\ + scope=openid email&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + ) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(redirect_url(&response), fallback_url); + + // Try to authorize with forbidden redirect url - invalid client id + let response = client + .post( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id=xxx&\ + redirect_uri=http://isec.pl&\ + scope=openid email&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + ) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(redirect_url(&response), fallback_url); + + let response = client + .get( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id=xxx&\ + redirect_uri=http://isec.pl&\ + scope=openid email&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + ) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(redirect_url(&response), fallback_url); + + // Try to authorize with forbidden redirect url - invalid scope + let response = client + .post(format!( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id={}&\ + redirect_uri=http://isec.pl&\ + scope=profile&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + oauth2client.client_id + )) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(redirect_url(&response), fallback_url); + + let response = client + .get(format!( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id={}&\ + redirect_uri=http://isec.pl&\ + scope=profile&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + oauth2client.client_id + )) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(redirect_url(&response), fallback_url); + + // Same with allowed redirect_uri + let response = client + .post(format!( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id={}&\ + redirect_uri=http://safe.net&\ + scope=profile&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + oauth2client.client_id + )) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(redirect_url(&response), "http://safe.net"); + + let response = client + .get(format!( + "/api/v1/oauth/authorize?\ + response_type=code&\ + client_id={}&\ + redirect_uri=http://safe.net&\ + scope=profile&\ + state=ABCDEF&\ + allow=true&\ + nonce=blabla", + oauth2client.client_id + )) + .send() + .await; + assert_eq!(response.status(), StatusCode::FOUND); + assert_eq!(redirect_url(&response), "http://safe.net"); +} + #[sqlx::test] async fn dg25_22_test_respect_openid_scope_in_userinfo( _: PgPoolOptions, @@ -770,7 +1267,7 @@ async fn dg25_22_test_respect_openid_scope_in_userinfo( ) { let pool = setup_pool(options).await; - let (client, state) = make_client_with_state(pool).await; + let (client, state) = make_test_client(pool).await; let mut config = state.config; let mut admin = User::find_by_username(&state.pool, "admin") @@ -943,6 +1440,74 @@ async fn dg25_22_test_respect_openid_scope_in_userinfo( assert!(claims.phone_number().is_none()); } +#[sqlx::test] +async fn dg25_21_test_openid_html_injection(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let client = make_client(pool).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 invalid_names = &[ + "Test Click", + "Test Click", + "Test ", + "Test