diff --git a/.sqlx/query-46fb95097c4cf79cdea7ef38c10cf8ca71f37bd60944f1b133a1ccd3a5fef98b.json b/.sqlx/query-46fb95097c4cf79cdea7ef38c10cf8ca71f37bd60944f1b133a1ccd3a5fef98b.json new file mode 100644 index 0000000000..9efca2de1b --- /dev/null +++ b/.sqlx/query-46fb95097c4cf79cdea7ef38c10cf8ca71f37bd60944f1b133a1ccd3a5fef98b.json @@ -0,0 +1,129 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, smtp_authentication = $15, smtp_oauth_issuer_url = $16, smtp_oauth_client_id = $17, smtp_oauth_client_secret = $18, smtp_oauth_refresh_token = $19, enrollment_vpn_step_optional = $20, enrollment_welcome_message = $21, enrollment_welcome_email = $22, enrollment_welcome_email_subject = $23, enrollment_use_welcome_message_as_email = $24, enrollment_send_welcome_email = $25, uuid = $26, ldap_url = $27, ldap_bind_username = $28, ldap_bind_password = $29, ldap_group_search_base = $30, ldap_user_search_base = $31, ldap_user_obj_class = $32, ldap_group_obj_class = $33, ldap_username_attr = $34, ldap_groupname_attr = $35, ldap_group_member_attr = $36, ldap_member_attr = $37, ldap_use_starttls = $38, ldap_tls_verify_cert = $39, openid_create_account = $40, license = $41, gateway_disconnect_notifications_enabled = $42, gateway_disconnect_notifications_inactivity_threshold = $43, gateway_disconnect_notifications_reconnect_notification_enabled = $44, ldap_sync_status = $45, ldap_enabled = $46, ldap_sync_enabled = $47, ldap_is_authoritative = $48, ldap_sync_interval = $49, ldap_user_auxiliary_obj_classes = $50, ldap_uses_ad = $51, ldap_user_rdn_attr = $52, ldap_sync_groups = $53, ldap_remote_enrollment_enabled = $54, ldap_remote_enrollment_send_invite = $55, openid_username_handling = $56, defguard_url = $57, default_admin_group_name = $58, authentication_period_days = $59, mfa_code_timeout_seconds = $60, public_proxy_url = $61, default_admin_id = $62, secret_key = $63, openid_signing_key_der = $64, enable_stats_purge = $65, stats_purge_frequency_hours = $66, stats_purge_threshold_days = $67, enrollment_token_timeout_hours = $68, password_reset_token_timeout_hours = $69, enrollment_session_timeout_minutes = $70, password_reset_session_timeout_minutes = $71, ldap_sync_account_status = $72 WHERE id = 1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Bool", + "Bool", + "Bool", + "Text", + "Text", + "Text", + "Text", + "Text", + "Int4", + { + "Custom": { + "name": "smtp_encryption", + "kind": { + "Enum": [ + "none", + "starttls", + "implicittls", + "xoauth2" + ] + } + } + }, + "Text", + "Text", + "Text", + { + "Custom": { + "name": "smtp_authentication", + "kind": { + "Enum": [ + "none", + "login", + "xoauth2" + ] + } + } + }, + "Text", + "Text", + "Text", + "Text", + "Bool", + "Text", + "Text", + "Text", + "Bool", + "Bool", + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Bool", + "Bool", + "Bool", + "Text", + "Bool", + "Int4", + "Bool", + { + "Custom": { + "name": "ldap_sync_status", + "kind": { + "Enum": [ + "insync", + "outofsync" + ] + } + } + }, + "Bool", + "Bool", + "Bool", + "Int4", + "TextArray", + "Bool", + "Text", + "TextArray", + "Bool", + "Bool", + { + "Custom": { + "name": "openid_username_handling", + "kind": { + "Enum": [ + "remove_forbidden", + "replace_forbidden", + "prune_email_domain" + ] + } + } + }, + "Text", + "Text", + "Int4", + "Int4", + "Text", + "Int8", + "Text", + "Bytea", + "Bool", + "Int4", + "Int4", + "Int4", + "Int4", + "Int4", + "Int4", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "46fb95097c4cf79cdea7ef38c10cf8ca71f37bd60944f1b133a1ccd3a5fef98b" +} diff --git a/.sqlx/query-7e9680742e7869c6dabf6bcf0e980aa2c2a233dfa1572df856646fb29d586ba4.json b/.sqlx/query-7e9680742e7869c6dabf6bcf0e980aa2c2a233dfa1572df856646fb29d586ba4.json deleted file mode 100644 index 9ee7c1929b..0000000000 --- a/.sqlx/query-7e9680742e7869c6dabf6bcf0e980aa2c2a233dfa1572df856646fb29d586ba4.json +++ /dev/null @@ -1,448 +0,0 @@ -{ - "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, enrollment_send_welcome_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_sync_account_status, ldap_user_rdn_attr, ldap_sync_groups, ldap_remote_enrollment_enabled, ldap_remote_enrollment_send_invite, openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", defguard_url, default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, public_proxy_url, default_admin_id, secret_key, openid_signing_key_der, enable_stats_purge, stats_purge_frequency_hours, stats_purge_threshold_days, enrollment_token_timeout_hours, password_reset_token_timeout_hours, enrollment_session_timeout_minutes, password_reset_session_timeout_minutes FROM \"settings\" WHERE id = 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "openid_enabled", - "type_info": "Bool" - }, - { - "ordinal": 1, - "name": "wireguard_enabled", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "webhooks_enabled", - "type_info": "Bool" - }, - { - "ordinal": 3, - "name": "worker_enabled", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "challenge_template", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "instance_name", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "main_logo_url", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "nav_logo_url", - "type_info": "Text" - }, - { - "ordinal": 8, - "name": "smtp_server", - "type_info": "Text" - }, - { - "ordinal": 9, - "name": "smtp_port", - "type_info": "Int4" - }, - { - "ordinal": 10, - "name": "smtp_encryption: _", - "type_info": { - "Custom": { - "name": "smtp_encryption", - "kind": { - "Enum": [ - "none", - "starttls", - "implicittls" - ] - } - } - } - }, - { - "ordinal": 11, - "name": "smtp_user", - "type_info": "Text" - }, - { - "ordinal": 12, - "name": "smtp_password?: SecretStringWrapper", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "smtp_sender", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "enrollment_vpn_step_optional", - "type_info": "Bool" - }, - { - "ordinal": 15, - "name": "enrollment_welcome_message", - "type_info": "Text" - }, - { - "ordinal": 16, - "name": "enrollment_welcome_email", - "type_info": "Text" - }, - { - "ordinal": 17, - "name": "enrollment_welcome_email_subject", - "type_info": "Text" - }, - { - "ordinal": 18, - "name": "enrollment_use_welcome_message_as_email", - "type_info": "Bool" - }, - { - "ordinal": 19, - "name": "enrollment_send_welcome_email", - "type_info": "Bool" - }, - { - "ordinal": 20, - "name": "uuid", - "type_info": "Uuid" - }, - { - "ordinal": 21, - "name": "ldap_url", - "type_info": "Text" - }, - { - "ordinal": 22, - "name": "ldap_bind_username", - "type_info": "Text" - }, - { - "ordinal": 23, - "name": "ldap_bind_password?: SecretStringWrapper", - "type_info": "Text" - }, - { - "ordinal": 24, - "name": "ldap_group_search_base", - "type_info": "Text" - }, - { - "ordinal": 25, - "name": "ldap_user_search_base", - "type_info": "Text" - }, - { - "ordinal": 26, - "name": "ldap_user_obj_class", - "type_info": "Text" - }, - { - "ordinal": 27, - "name": "ldap_group_obj_class", - "type_info": "Text" - }, - { - "ordinal": 28, - "name": "ldap_username_attr", - "type_info": "Text" - }, - { - "ordinal": 29, - "name": "ldap_groupname_attr", - "type_info": "Text" - }, - { - "ordinal": 30, - "name": "ldap_group_member_attr", - "type_info": "Text" - }, - { - "ordinal": 31, - "name": "ldap_member_attr", - "type_info": "Text" - }, - { - "ordinal": 32, - "name": "openid_create_account", - "type_info": "Bool" - }, - { - "ordinal": 33, - "name": "license", - "type_info": "Text" - }, - { - "ordinal": 34, - "name": "gateway_disconnect_notifications_enabled", - "type_info": "Bool" - }, - { - "ordinal": 35, - "name": "ldap_use_starttls", - "type_info": "Bool" - }, - { - "ordinal": 36, - "name": "ldap_tls_verify_cert", - "type_info": "Bool" - }, - { - "ordinal": 37, - "name": "gateway_disconnect_notifications_inactivity_threshold", - "type_info": "Int4" - }, - { - "ordinal": 38, - "name": "gateway_disconnect_notifications_reconnect_notification_enabled", - "type_info": "Bool" - }, - { - "ordinal": 39, - "name": "ldap_sync_status: LdapSyncStatus", - "type_info": { - "Custom": { - "name": "ldap_sync_status", - "kind": { - "Enum": [ - "insync", - "outofsync" - ] - } - } - } - }, - { - "ordinal": 40, - "name": "ldap_enabled", - "type_info": "Bool" - }, - { - "ordinal": 41, - "name": "ldap_sync_enabled", - "type_info": "Bool" - }, - { - "ordinal": 42, - "name": "ldap_is_authoritative", - "type_info": "Bool" - }, - { - "ordinal": 43, - "name": "ldap_sync_interval", - "type_info": "Int4" - }, - { - "ordinal": 44, - "name": "ldap_user_auxiliary_obj_classes", - "type_info": "TextArray" - }, - { - "ordinal": 45, - "name": "ldap_uses_ad", - "type_info": "Bool" - }, - { - "ordinal": 46, - "name": "ldap_sync_account_status", - "type_info": "Bool" - }, - { - "ordinal": 47, - "name": "ldap_user_rdn_attr", - "type_info": "Text" - }, - { - "ordinal": 48, - "name": "ldap_sync_groups", - "type_info": "TextArray" - }, - { - "ordinal": 49, - "name": "ldap_remote_enrollment_enabled", - "type_info": "Bool" - }, - { - "ordinal": 50, - "name": "ldap_remote_enrollment_send_invite", - "type_info": "Bool" - }, - { - "ordinal": 51, - "name": "openid_username_handling: OpenIdUsernameHandling", - "type_info": { - "Custom": { - "name": "openid_username_handling", - "kind": { - "Enum": [ - "remove_forbidden", - "replace_forbidden", - "prune_email_domain" - ] - } - } - } - }, - { - "ordinal": 52, - "name": "defguard_url", - "type_info": "Text" - }, - { - "ordinal": 53, - "name": "default_admin_group_name", - "type_info": "Text" - }, - { - "ordinal": 54, - "name": "authentication_period_days", - "type_info": "Int4" - }, - { - "ordinal": 55, - "name": "mfa_code_timeout_seconds", - "type_info": "Int4" - }, - { - "ordinal": 56, - "name": "public_proxy_url", - "type_info": "Text" - }, - { - "ordinal": 57, - "name": "default_admin_id", - "type_info": "Int8" - }, - { - "ordinal": 58, - "name": "secret_key", - "type_info": "Text" - }, - { - "ordinal": 59, - "name": "openid_signing_key_der", - "type_info": "Bytea" - }, - { - "ordinal": 60, - "name": "enable_stats_purge", - "type_info": "Bool" - }, - { - "ordinal": 61, - "name": "stats_purge_frequency_hours", - "type_info": "Int4" - }, - { - "ordinal": 62, - "name": "stats_purge_threshold_days", - "type_info": "Int4" - }, - { - "ordinal": 63, - "name": "enrollment_token_timeout_hours", - "type_info": "Int4" - }, - { - "ordinal": 64, - "name": "password_reset_token_timeout_hours", - "type_info": "Int4" - }, - { - "ordinal": 65, - "name": "enrollment_session_timeout_minutes", - "type_info": "Int4" - }, - { - "ordinal": 66, - "name": "password_reset_session_timeout_minutes", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false, - true, - true, - true, - false, - true, - true, - true, - false, - false, - false, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - false, - false, - false, - false - ] - }, - "hash": "7e9680742e7869c6dabf6bcf0e980aa2c2a233dfa1572df856646fb29d586ba4" -} diff --git a/.sqlx/query-76553e0ae70872ba36db324a5e2b19f0b841e426d876eba72ae5c4f5b5b54513.json b/.sqlx/query-985c077436165c3f123f39c59e7629cef8fcc2029cc6fe4255dd11258ad9dbe2.json similarity index 85% rename from .sqlx/query-76553e0ae70872ba36db324a5e2b19f0b841e426d876eba72ae5c4f5b5b54513.json rename to .sqlx/query-985c077436165c3f123f39c59e7629cef8fcc2029cc6fe4255dd11258ad9dbe2.json index 0793719fb8..ec4520f2ea 100644 --- a/.sqlx/query-76553e0ae70872ba36db324a5e2b19f0b841e426d876eba72ae5c4f5b5b54513.json +++ b/.sqlx/query-985c077436165c3f123f39c59e7629cef8fcc2029cc6fe4255dd11258ad9dbe2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n 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, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE ldap_user_path IS NULL\n ", + "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, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE ldap_user_path IS NULL", "describe": { "columns": [ { @@ -154,5 +154,5 @@ false ] }, - "hash": "76553e0ae70872ba36db324a5e2b19f0b841e426d876eba72ae5c4f5b5b54513" + "hash": "985c077436165c3f123f39c59e7629cef8fcc2029cc6fe4255dd11258ad9dbe2" } diff --git a/.sqlx/query-a5e740909398983446b7e0f7ee908910083e75320665f764f3de7c1e15a8b8eb.json b/.sqlx/query-a5e740909398983446b7e0f7ee908910083e75320665f764f3de7c1e15a8b8eb.json deleted file mode 100644 index 9591c56a70..0000000000 --- a/.sqlx/query-a5e740909398983446b7e0f7ee908910083e75320665f764f3de7c1e15a8b8eb.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, enrollment_send_welcome_email = $20, uuid = $21, ldap_url = $22, ldap_bind_username = $23, ldap_bind_password = $24, ldap_group_search_base = $25, ldap_user_search_base = $26, ldap_user_obj_class = $27, ldap_group_obj_class = $28, ldap_username_attr = $29, ldap_groupname_attr = $30, ldap_group_member_attr = $31, ldap_member_attr = $32, ldap_use_starttls = $33, ldap_tls_verify_cert = $34, openid_create_account = $35, license = $36, gateway_disconnect_notifications_enabled = $37, gateway_disconnect_notifications_inactivity_threshold = $38, gateway_disconnect_notifications_reconnect_notification_enabled = $39, ldap_sync_status = $40, ldap_enabled = $41, ldap_sync_enabled = $42, ldap_is_authoritative = $43, ldap_sync_interval = $44, ldap_user_auxiliary_obj_classes = $45, ldap_uses_ad = $46, ldap_user_rdn_attr = $47, ldap_sync_groups = $48, ldap_remote_enrollment_enabled = $49, ldap_remote_enrollment_send_invite = $50, openid_username_handling = $51, defguard_url = $52, default_admin_group_name = $53, authentication_period_days = $54, mfa_code_timeout_seconds = $55, public_proxy_url = $56, default_admin_id = $57, secret_key = $58, openid_signing_key_der = $59, enable_stats_purge = $60, stats_purge_frequency_hours = $61, stats_purge_threshold_days = $62, enrollment_token_timeout_hours = $63, password_reset_token_timeout_hours = $64, enrollment_session_timeout_minutes = $65, password_reset_session_timeout_minutes = $66, ldap_sync_account_status = $67 WHERE id = 1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Bool", - "Bool", - "Bool", - "Bool", - "Text", - "Text", - "Text", - "Text", - "Text", - "Int4", - { - "Custom": { - "name": "smtp_encryption", - "kind": { - "Enum": [ - "none", - "starttls", - "implicittls" - ] - } - } - }, - "Text", - "Text", - "Text", - "Bool", - "Text", - "Text", - "Text", - "Bool", - "Bool", - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Bool", - "Bool", - "Bool", - "Text", - "Bool", - "Int4", - "Bool", - { - "Custom": { - "name": "ldap_sync_status", - "kind": { - "Enum": [ - "insync", - "outofsync" - ] - } - } - }, - "Bool", - "Bool", - "Bool", - "Int4", - "TextArray", - "Bool", - "Text", - "TextArray", - "Bool", - "Bool", - { - "Custom": { - "name": "openid_username_handling", - "kind": { - "Enum": [ - "remove_forbidden", - "replace_forbidden", - "prune_email_domain" - ] - } - } - }, - "Text", - "Text", - "Int4", - "Int4", - "Text", - "Int8", - "Text", - "Bytea", - "Bool", - "Int4", - "Int4", - "Int4", - "Int4", - "Int4", - "Int4", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "a5e740909398983446b7e0f7ee908910083e75320665f764f3de7c1e15a8b8eb" -} diff --git a/.sqlx/query-e8e6527d865def2163dbbcaf7136f71c3b80b5b1c6d61046c8253403cf819853.json b/.sqlx/query-e8e6527d865def2163dbbcaf7136f71c3b80b5b1c6d61046c8253403cf819853.json new file mode 100644 index 0000000000..8884a1bd42 --- /dev/null +++ b/.sqlx/query-e8e6527d865def2163dbbcaf7136f71c3b80b5b1c6d61046c8253403cf819853.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE settings SET smtp_oauth_refresh_token = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "e8e6527d865def2163dbbcaf7136f71c3b80b5b1c6d61046c8253403cf819853" +} diff --git a/Cargo.lock b/Cargo.lock index 753dac07a9..34e6f38f05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -603,9 +603,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" dependencies = [ "serde_core", ] @@ -765,9 +765,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.62" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -798,9 +798,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1614,6 +1614,7 @@ dependencies = [ "image", "lettre", "mrml", + "openidconnect", "pulldown-cmark", "qrforge", "reqwest", @@ -1932,15 +1933,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -2569,7 +2570,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "libc", "libgit2-sys", "log", @@ -2595,7 +2596,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "ignore", "walkdir", ] @@ -2871,9 +2872,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -3284,7 +3285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba781c43eb46c3bbf5bfda541139eed9a52b78d7c3aa74d516918885ecd63c40" dependencies = [ "base64 0.22.1", - "bitflags 2.11.1", + "bitflags 2.12.1", "num-bigint", "serde", "serde_json", @@ -3429,9 +3430,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" dependencies = [ "arbitrary", "cc", @@ -3439,9 +3440,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.18.4+1.9.3" +version = "0.18.5+1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b26f66f35e1871b22efcf7191564123d2a446ca0538cde63c23adfefa9b15b7" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" dependencies = [ "cc", "libc", @@ -3457,14 +3458,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "libc", "plain", - "redox_syscall 0.7.5", + "redox_syscall 0.8.1", ] [[package]] @@ -3479,9 +3480,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "libc", @@ -3681,9 +3682,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -3756,11 +3757,11 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nix" -version = "0.30.1" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "cfg_aliases", "libc", @@ -3972,7 +3973,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-foundation", ] @@ -3993,7 +3994,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "dispatch2", "objc2", ] @@ -4004,7 +4005,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -4037,7 +4038,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -4055,7 +4056,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "libc", "objc2", @@ -4068,7 +4069,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", ] @@ -4079,7 +4080,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -4091,7 +4092,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "objc2", "objc2-cloud-kit", @@ -4201,7 +4202,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "foreign-types", "libc", @@ -4259,9 +4260,9 @@ dependencies = [ [[package]] name = "os_info" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" dependencies = [ "android_system_properties", "log", @@ -4700,7 +4701,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "crc32fast", "fdeflate", "flate2", @@ -4886,7 +4887,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "getopts", "memchr", "pulldown-cmark-escape", @@ -5156,7 +5157,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -5199,16 +5200,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] name = "redox_syscall" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -5452,7 +5453,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys", @@ -5477,9 +5478,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -5600,7 +5601,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5623,7 +5624,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cssparser 0.36.0", "derive_more 2.1.1", "log", @@ -5778,9 +5779,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", "bs58", @@ -5798,9 +5799,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -5928,9 +5929,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -6033,9 +6034,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -6166,7 +6167,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.1", + "bitflags 2.12.1", "byteorder", "bytes", "chrono", @@ -6210,7 +6211,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.1", + "bitflags 2.12.1", "byteorder", "chrono", "crc", @@ -6383,18 +6384,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "struct-patch" -version = "0.10.5" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d4caaaccd69c9b56c5f5b33d4dca462464d3275230e4d2d3739ba6d4bf5bcb" +checksum = "7d0dafd40527e4b2dd640c4272d51d0e2e838e9345db4fab06c86c501fe8bc5e" dependencies = [ "struct-patch-derive", ] [[package]] name = "struct-patch-derive" -version = "0.10.5" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1671c6f0992b1b4cb4f5f8ea4a58f9a5f7f895a7638ef9690633dcec0aa67944" +checksum = "970378c47245454d5899f4aae1ed2ad9698f756ebbfc8589bd63b54f56af86ff" dependencies = [ "proc-macro2", "quote", @@ -6465,7 +6466,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6756,9 +6757,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap 2.14.0", "toml_datetime", @@ -6896,7 +6897,7 @@ version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "futures-core", "futures-util", @@ -7037,9 +7038,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uaparser" @@ -7096,9 +7097,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -7217,9 +7218,9 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -7439,7 +7440,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -8011,7 +8012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.12.1", "indexmap 2.14.0", "log", "serde", @@ -8130,9 +8131,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -8153,18 +8154,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 43a9f94190..caee3fdd06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ edition = "2024" license-file = "LICENSE.md" homepage = "https://defguard.net/" repository = "https://github.com/DefGuard/defguard" -rust-version = "1.87.0" +rust-version = "1.91.0" [workspace] members = ["crates/*", "tools/*"] @@ -71,7 +71,7 @@ md4 = "0.10" openidconnect = { version = "4.0", default-features = false, features = [ "reqwest", ] } -os_info = "3.12" +os_info = "3.15" parse_link_header = "0.4" paste = "1.0" pgp = { version = "0.19", default-features = false } @@ -103,7 +103,7 @@ sqlx = { version = "0.8", features = [ "uuid", ] } ssh-key = "0.6" -struct-patch = "0.10" +struct-patch = { version = "0.12", default-features = false, features = ["nesting"] } strum = { version = "0.28", features = ["derive"] } strum_macros = "0.28" tera = "1.20" diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 92cf9d494d..611cddf3e4 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -146,10 +146,10 @@ async fn main() -> Result<(), anyhow::Error> { match wizard.active_wizard { ActiveWizard::None => {} ActiveWizard::Initial | ActiveWizard::AutoAdoption => { - if wizard.active_wizard == ActiveWizard::AutoAdoption { - if let Err(err) = attempt_auto_adoption(&pool, &config).await { - warn!("Failed to store startup auto-adoption states: {err}"); - } + if wizard.active_wizard == ActiveWizard::AutoAdoption + && let Err(err) = attempt_auto_adoption(&pool, &config).await + { + warn!("Failed to store startup auto-adoption states: {err}"); } if let Err(err) = diff --git a/crates/defguard_common/src/auth/claims.rs b/crates/defguard_common/src/auth/claims.rs index bd1e36809c..2a5fb27832 100644 --- a/crates/defguard_common/src/auth/claims.rs +++ b/crates/defguard_common/src/auth/claims.rs @@ -10,7 +10,7 @@ use crate::db::models::{Settings, settings::SettingsInitializationError}; pub static JWT_ISSUER: &str = "DefGuard"; -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)] pub enum ClaimsType { #[default] Auth, diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index 137f04f6ce..bb5b245087 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -295,9 +295,9 @@ impl DefGuardConfig { cookie_domain: None, cookie_insecure: Some(false), cmd: None, - check_period: std::time::Duration::from_secs(12 * 3600).into(), - check_period_no_license: std::time::Duration::from_secs(24 * 3600).into(), - check_period_renewal_window: std::time::Duration::from_secs(3600).into(), + check_period: std::time::Duration::from_hours(12).into(), + check_period_no_license: std::time::Duration::from_hours(24).into(), + check_period_renewal_window: std::time::Duration::from_hours(1).into(), http_bind_address: None, grpc_bind_address: None, adopt_gateway: None, diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index a109ab76e6..8c2b2d7971 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -1024,10 +1024,10 @@ impl Device { } pub fn validate_pubkey(pubkey: &str) -> Result<(), String> { - if let Ok(key) = BASE64_STANDARD.decode(pubkey) { - if key.len() == KEY_LENGTH { - return Ok(()); - } + if let Ok(key) = BASE64_STANDARD.decode(pubkey) + && key.len() == KEY_LENGTH + { + return Ok(()); } Err(format!("{pubkey} is not a valid pubkey")) diff --git a/crates/defguard_common/src/db/models/initial_setup_wizard.rs b/crates/defguard_common/src/db/models/initial_setup_wizard.rs index 03e46b6279..36d6d9fcc7 100644 --- a/crates/defguard_common/src/db/models/initial_setup_wizard.rs +++ b/crates/defguard_common/src/db/models/initial_setup_wizard.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, Type, query, query_scalar, types::Json}; -#[derive(Clone, Debug, Copy, Eq, PartialEq, Deserialize, Serialize, Default, Type, PartialOrd)] +#[derive(Clone, Debug, Copy, PartialEq, Deserialize, Serialize, Default, Type, PartialOrd)] #[serde(rename_all = "snake_case")] #[sqlx(type_name = "initial_setup_step", rename_all = "snake_case")] pub enum InitialSetupStep { diff --git a/crates/defguard_common/src/db/models/oauth2token.rs b/crates/defguard_common/src/db/models/oauth2token.rs index 3af29fbf22..fc30965eb4 100644 --- a/crates/defguard_common/src/db/models/oauth2token.rs +++ b/crates/defguard_common/src/db/models/oauth2token.rs @@ -20,7 +20,7 @@ impl OAuth2Token { pub fn new(oauth2authorizedapp_id: Id, redirect_uri: String, scope: String) -> Self { let settings = Settings::get_current_settings(); let timeout = settings.authentication_timeout(); - let expiration = Utc::now() + TimeDelta::seconds(timeout.as_secs() as i64); + let expiration = Utc::now() + TimeDelta::seconds(timeout.as_secs().cast_signed()); Self { oauth2authorizedapp_id, access_token: gen_alphanumeric(24), @@ -37,7 +37,7 @@ impl OAuth2Token { let timeout = settings.authentication_timeout(); let new_access_token = gen_alphanumeric(24); let new_refresh_token = gen_alphanumeric(24); - let expiration = Utc::now() + TimeDelta::seconds(timeout.as_secs() as i64); + let expiration = Utc::now() + TimeDelta::seconds(timeout.as_secs().cast_signed()); self.expires_in = expiration.timestamp(); query!( @@ -62,16 +62,18 @@ impl OAuth2Token { /// Store data in the database. pub async fn save(&self, pool: &PgPool) -> sqlx::Result<()> { query!( - "INSERT INTO oauth2token (oauth2authorizedapp_id, access_token, refresh_token, redirect_uri, scope, expires_in) \ + "INSERT INTO oauth2token (oauth2authorizedapp_id, access_token, refresh_token, \ + redirect_uri, scope, expires_in) \ VALUES ($1, $2, $3, $4, $5, $6)", self.oauth2authorizedapp_id, self.access_token, self.refresh_token, self.redirect_uri, self.scope, - self.expires_in) - .execute(pool) - .await?; + self.expires_in + ) + .execute(pool) + .await?; Ok(()) } @@ -94,7 +96,8 @@ impl OAuth2Token { ) -> sqlx::Result> { match query_as!( Self, - "SELECT oauth2authorizedapp_id, access_token, refresh_token, redirect_uri, scope, expires_in \ + "SELECT oauth2authorizedapp_id, access_token, refresh_token, redirect_uri, scope, \ + expires_in \ FROM oauth2token WHERE access_token = $1", access_token ) @@ -121,7 +124,8 @@ impl OAuth2Token { ) -> sqlx::Result> { match query_as!( Self, - "SELECT oauth2authorizedapp_id, access_token, refresh_token, redirect_uri, scope, expires_in \ + "SELECT oauth2authorizedapp_id, access_token, refresh_token, redirect_uri, scope, \ + expires_in \ FROM oauth2token WHERE refresh_token = $1", refresh_token ) @@ -148,7 +152,8 @@ impl OAuth2Token { ) -> sqlx::Result> { match query_as!( Self, - "SELECT oauth2authorizedapp_id, access_token, refresh_token, redirect_uri, scope, expires_in \ + "SELECT oauth2authorizedapp_id, access_token, refresh_token, redirect_uri, scope, \ + expires_in \ FROM oauth2token WHERE oauth2authorizedapp_id = $1", oauth2authorizedapp_id, ) diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings/mod.rs similarity index 90% rename from crates/defguard_common/src/db/models/settings.rs rename to crates/defguard_common/src/db/models/settings/mod.rs index bde8447b40..85dfc9ee69 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings/mod.rs @@ -11,7 +11,7 @@ use rsa::{ }; use secrecy::ExposeSecret; use serde::{Deserialize, Deserializer, Serialize}; -use sqlx::{PgExecutor, PgPool, Type, query, query_as}; +use sqlx::{FromRow, PgExecutor, PgPool, Type, query, query_as}; use struct_patch::Patch; use thiserror::Error; use tracing::{debug, info, warn}; @@ -20,10 +20,13 @@ use utoipa::ToSchema; use uuid::Uuid; use webauthn_rs::prelude::WebauthnBuilder; +use self::smtp::{SmtpAuthentication, SmtpEncryption, SmtpSettings, SmtpSettingsPatch}; use crate::{ config::DefGuardConfig, db::Id, global_value, secret::SecretStringWrapper, types::AuthFlowType, }; +pub mod smtp; + global_value!(SETTINGS, Option, None, set_settings, get_settings); pub const OPENID_KEY_SIZE: usize = 2048; @@ -82,7 +85,7 @@ pub enum SettingsInitializationError { Invalid(&'static str, &'static str), } -#[derive(Error, Debug, Clone)] +#[derive(Error, Debug)] pub enum SettingsUrlError { #[error("Unable to parse defguard_url `{0}`")] InvalidDefguardUrl(String), @@ -110,16 +113,7 @@ pub enum SettingsSaveError { Validation(#[from] SettingsValidationError), } -#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Type, Debug, Default)] -#[sqlx(type_name = "smtp_encryption", rename_all = "lowercase")] -pub enum SmtpEncryption { - #[default] - None, - StartTls, - ImplicitTls, -} - -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Serialize, PartialEq, ToSchema, Type)] +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, ToSchema, Type)] #[sqlx(type_name = "openid_username_handling", rename_all = "snake_case")] pub enum OpenIdUsernameHandling { #[default] @@ -131,7 +125,7 @@ pub enum OpenIdUsernameHandling { PruneEmailDomain, } -#[derive(Clone, Debug, Copy, Eq, PartialEq, Deserialize, Serialize, Default, Type)] +#[derive(Clone, Debug, Copy, PartialEq, Deserialize, Serialize, Default, Type)] #[sqlx(type_name = "ldap_sync_status", rename_all = "lowercase")] pub enum LdapSyncStatus { InSync, @@ -167,8 +161,8 @@ where Ok(Some(Option::deserialize(deserializer)?)) } -#[derive(Clone, Deserialize, PartialEq, Patch, Serialize, Default)] -#[patch(attribute(derive(Deserialize, Serialize, Debug)))] +#[derive(Clone, Default, Deserialize, FromRow, PartialEq, Patch, Serialize)] +#[patch(attribute(derive(Deserialize, Serialize)))] pub struct Settings { // Modules pub openid_enabled: bool, @@ -182,14 +176,10 @@ pub struct Settings { pub main_logo_url: String, pub nav_logo_url: String, // SMTP - pub smtp_server: Option, - pub smtp_port: Option, - pub smtp_encryption: SmtpEncryption, - #[patch(attribute(serde(deserialize_with = "deserialize_optional_field", default)))] - pub smtp_user: Option, - #[patch(attribute(serde(deserialize_with = "deserialize_optional_field", default)))] - pub smtp_password: Option, - pub smtp_sender: Option, + #[patch(nesting, attribute(serde(flatten)))] + #[serde(flatten)] + #[sqlx(flatten)] + pub smtp: SmtpSettings, // Enrollment pub enrollment_vpn_step_optional: bool, pub enrollment_welcome_message: Option, @@ -271,12 +261,7 @@ impl fmt::Debug for Settings { .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("smtp", &self.smtp) .field( "enrollment_vpn_step_optional", &self.enrollment_vpn_step_optional, @@ -491,34 +476,30 @@ impl Settings { where E: PgExecutor<'e>, { - query_as!( - Self, + query_as( "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, \ + smtp_port, smtp_encryption, smtp_user, smtp_password, smtp_sender, \ + smtp_authentication, smtp_oauth_issuer_url, smtp_oauth_client_id, \ + smtp_oauth_client_secret, smtp_oauth_refresh_token, \ enrollment_vpn_step_optional, enrollment_welcome_message, \ enrollment_welcome_email, enrollment_welcome_email_subject, \ enrollment_use_welcome_message_as_email, enrollment_send_welcome_email, \ - uuid, ldap_url, ldap_bind_username, \ - ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", \ + uuid, ldap_url, ldap_bind_username, ldap_bind_password, \ 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_status, ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, \ ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, \ - ldap_sync_account_status, \ - ldap_user_rdn_attr, ldap_sync_groups, ldap_remote_enrollment_enabled, ldap_remote_enrollment_send_invite, \ - openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", \ - defguard_url, \ + ldap_sync_account_status, ldap_user_rdn_attr, ldap_sync_groups, \ + ldap_remote_enrollment_enabled, ldap_remote_enrollment_send_invite, \ + openid_username_handling, defguard_url, \ default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, \ - public_proxy_url, \ - default_admin_id, secret_key, openid_signing_key_der, enable_stats_purge, \ - stats_purge_frequency_hours, stats_purge_threshold_days, \ + public_proxy_url, default_admin_id, secret_key, openid_signing_key_der, \ + enable_stats_purge, stats_purge_frequency_hours, stats_purge_threshold_days, \ enrollment_token_timeout_hours, password_reset_token_timeout_hours, \ enrollment_session_timeout_minutes, password_reset_session_timeout_minutes \ FROM \"settings\" WHERE id = 1", @@ -589,59 +570,64 @@ impl Settings { smtp_user = $12, \ smtp_password = $13, \ smtp_sender = $14, \ - enrollment_vpn_step_optional = $15, \ - enrollment_welcome_message = $16, \ - enrollment_welcome_email = $17, \ - enrollment_welcome_email_subject = $18, \ - enrollment_use_welcome_message_as_email = $19, \ - enrollment_send_welcome_email = $20, \ - uuid = $21, \ - ldap_url = $22, \ - ldap_bind_username = $23, \ - ldap_bind_password = $24, \ - ldap_group_search_base = $25, \ - ldap_user_search_base = $26, \ - ldap_user_obj_class = $27, \ - ldap_group_obj_class = $28, \ - ldap_username_attr = $29, \ - ldap_groupname_attr = $30, \ - ldap_group_member_attr = $31, \ - ldap_member_attr = $32, \ - ldap_use_starttls = $33, \ - ldap_tls_verify_cert = $34, \ - openid_create_account = $35, \ - license = $36, \ - gateway_disconnect_notifications_enabled = $37, \ - gateway_disconnect_notifications_inactivity_threshold = $38, \ - gateway_disconnect_notifications_reconnect_notification_enabled = $39, \ - ldap_sync_status = $40, \ - ldap_enabled = $41, \ - ldap_sync_enabled = $42, \ - ldap_is_authoritative = $43, \ - ldap_sync_interval = $44, \ - ldap_user_auxiliary_obj_classes = $45, \ - ldap_uses_ad = $46, \ - ldap_user_rdn_attr = $47, \ - ldap_sync_groups = $48, \ - ldap_remote_enrollment_enabled = $49, \ - ldap_remote_enrollment_send_invite = $50, \ - openid_username_handling = $51, \ - defguard_url = $52, \ - default_admin_group_name = $53, \ - authentication_period_days = $54, \ - mfa_code_timeout_seconds = $55, \ - public_proxy_url = $56, \ - default_admin_id = $57, \ - secret_key = $58, \ - openid_signing_key_der = $59, \ - enable_stats_purge = $60, \ - stats_purge_frequency_hours = $61, \ - stats_purge_threshold_days = $62, \ - enrollment_token_timeout_hours = $63, \ - password_reset_token_timeout_hours = $64, \ - enrollment_session_timeout_minutes = $65, \ - password_reset_session_timeout_minutes = $66, \ - ldap_sync_account_status = $67 \ + smtp_authentication = $15, \ + smtp_oauth_issuer_url = $16, \ + smtp_oauth_client_id = $17, \ + smtp_oauth_client_secret = $18, \ + smtp_oauth_refresh_token = $19, \ + enrollment_vpn_step_optional = $20, \ + enrollment_welcome_message = $21, \ + enrollment_welcome_email = $22, \ + enrollment_welcome_email_subject = $23, \ + enrollment_use_welcome_message_as_email = $24, \ + enrollment_send_welcome_email = $25, \ + uuid = $26, \ + ldap_url = $27, \ + ldap_bind_username = $28, \ + ldap_bind_password = $29, \ + ldap_group_search_base = $30, \ + ldap_user_search_base = $31, \ + ldap_user_obj_class = $32, \ + ldap_group_obj_class = $33, \ + ldap_username_attr = $34, \ + ldap_groupname_attr = $35, \ + ldap_group_member_attr = $36, \ + ldap_member_attr = $37, \ + ldap_use_starttls = $38, \ + ldap_tls_verify_cert = $39, \ + openid_create_account = $40, \ + license = $41, \ + gateway_disconnect_notifications_enabled = $42, \ + gateway_disconnect_notifications_inactivity_threshold = $43, \ + gateway_disconnect_notifications_reconnect_notification_enabled = $44, \ + ldap_sync_status = $45, \ + ldap_enabled = $46, \ + ldap_sync_enabled = $47, \ + ldap_is_authoritative = $48, \ + ldap_sync_interval = $49, \ + ldap_user_auxiliary_obj_classes = $50, \ + ldap_uses_ad = $51, \ + ldap_user_rdn_attr = $52, \ + ldap_sync_groups = $53, \ + ldap_remote_enrollment_enabled = $54, \ + ldap_remote_enrollment_send_invite = $55, \ + openid_username_handling = $56, \ + defguard_url = $57, \ + default_admin_group_name = $58, \ + authentication_period_days = $59, \ + mfa_code_timeout_seconds = $60, \ + public_proxy_url = $61, \ + default_admin_id = $62, \ + secret_key = $63, \ + openid_signing_key_der = $64, \ + enable_stats_purge = $65, \ + stats_purge_frequency_hours = $66, \ + stats_purge_threshold_days = $67, \ + enrollment_token_timeout_hours = $68, \ + password_reset_token_timeout_hours = $69, \ + enrollment_session_timeout_minutes = $70, \ + password_reset_session_timeout_minutes = $71, \ + ldap_sync_account_status = $72 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -651,12 +637,17 @@ impl Settings { self.instance_name, self.main_logo_url, self.nav_logo_url, - self.smtp_server, - self.smtp_port, - &self.smtp_encryption as &SmtpEncryption, - self.smtp_user, - &self.smtp_password as &Option, - self.smtp_sender, + self.smtp.server, + self.smtp.port, + &self.smtp.encryption as &SmtpEncryption, + self.smtp.user, + &self.smtp.password as &Option, + self.smtp.sender, + &self.smtp.authentication as &SmtpAuthentication, + self.smtp.oauth_issuer_url, + self.smtp.oauth_client_id, + &self.smtp.oauth_client_secret as &Option, + self.smtp.oauth_refresh_token, self.enrollment_vpn_step_optional, self.enrollment_welcome_message, self.enrollment_welcome_email, @@ -780,11 +771,11 @@ impl Settings { /// Meant to be used to check if sending emails is enabled in current instance. #[must_use] pub fn smtp_configured(&self) -> bool { - self.smtp_server.is_some() - && self.smtp_port.is_some() - && self.smtp_sender.is_some() - && self.smtp_server != Some(String::new()) - && self.smtp_sender != Some(String::new()) + self.smtp.server.is_some() + && self.smtp.port.is_some() + && self.smtp.sender.is_some() + && self.smtp.server != Some(String::new()) + && self.smtp.sender != Some(String::new()) } /// Check if all required LDAP options are configured. @@ -833,37 +824,37 @@ impl Settings { #[must_use] pub fn authentication_timeout(&self) -> Duration { - Duration::from_secs(self.authentication_period_days as u64 * 24 * 3600) + Duration::from_hours(self.authentication_period_days as u64 * 24) } #[must_use] pub fn stats_purge_frequency(&self) -> Duration { - Duration::from_secs(self.stats_purge_frequency_hours as u64 * 3600) + Duration::from_hours(self.stats_purge_frequency_hours as u64) } #[must_use] pub fn stats_purge_threshold(&self) -> Duration { - Duration::from_secs(self.stats_purge_threshold_days as u64 * 24 * 3600) + Duration::from_hours(self.stats_purge_threshold_days as u64 * 24) } #[must_use] pub fn enrollment_token_timeout(&self) -> Duration { - Duration::from_secs(self.enrollment_token_timeout_hours as u64 * 3600) + Duration::from_hours(self.enrollment_token_timeout_hours as u64) } #[must_use] pub fn password_reset_token_timeout(&self) -> Duration { - Duration::from_secs(self.password_reset_token_timeout_hours as u64 * 3600) + Duration::from_hours(self.password_reset_token_timeout_hours as u64) } #[must_use] pub fn enrollment_session_timeout(&self) -> Duration { - Duration::from_secs(self.enrollment_session_timeout_minutes as u64 * 60) + Duration::from_mins(self.enrollment_session_timeout_minutes as u64) } #[must_use] pub fn password_reset_session_timeout(&self) -> Duration { - Duration::from_secs(self.password_reset_session_timeout_minutes as u64 * 60) + Duration::from_mins(self.password_reset_session_timeout_minutes as u64) } pub fn secret_key_required(&self) -> Result<&str, SettingsInitializationError> { @@ -1108,21 +1099,21 @@ mod test { assert!(!settings.smtp_configured()); // incomplete SMTP config - settings.smtp_server = Some("localhost".into()); - settings.smtp_port = Some(587); + settings.smtp.server = Some("localhost".into()); + settings.smtp.port = Some(587); assert!(!settings.smtp_configured()); // no-auth SMTP config - settings.smtp_sender = Some("no-reply@defguard.net".into()); + settings.smtp.sender = Some("no-reply@defguard.net".into()); assert!(settings.smtp_configured()); // add non-default encryption - settings.smtp_encryption = SmtpEncryption::StartTls; + settings.smtp.encryption = SmtpEncryption::StartTls; assert!(settings.smtp_configured()); // add auth info - settings.smtp_user = Some("smtp_user".into()); - settings.smtp_password = Some(SecretStringWrapper::from_str("hunter2").unwrap()); + settings.smtp.user = Some("smtp_user".into()); + settings.smtp.password = Some(SecretStringWrapper::from_str("hunter2").unwrap()); assert!(settings.smtp_configured()); } diff --git a/crates/defguard_common/src/db/models/settings/smtp.rs b/crates/defguard_common/src/db/models/settings/smtp.rs new file mode 100644 index 0000000000..c8e5982acb --- /dev/null +++ b/crates/defguard_common/src/db/models/settings/smtp.rs @@ -0,0 +1,124 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, PgExecutor, Type, query}; +use struct_patch::Patch; + +use super::deserialize_optional_field; +use crate::secret::SecretStringWrapper; + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Type)] +#[sqlx(type_name = "smtp_authentication", rename_all = "lowercase")] +pub enum SmtpAuthentication { + #[default] + None, + Login, + XOAuth2, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Type)] +#[sqlx(type_name = "smtp_encryption", rename_all = "lowercase")] +pub enum SmtpEncryption { + #[default] + None, + StartTls, + ImplicitTls, +} + +#[derive(Clone, Default, Deserialize, FromRow, PartialEq, Patch, Serialize)] +#[patch(attribute(derive(Deserialize, Serialize)))] +pub struct SmtpSettings { + #[serde(rename = "smtp_server")] + #[sqlx(rename = "smtp_server")] + #[patch(attribute(serde(rename = "smtp_server")))] + pub server: Option, + #[serde(rename = "smtp_port")] + #[sqlx(rename = "smtp_port")] + #[patch(attribute(serde(rename = "smtp_port")))] + pub port: Option, + #[serde(rename = "smtp_encryption")] + #[sqlx(rename = "smtp_encryption")] + #[patch(attribute(serde(rename = "smtp_encryption")))] + pub encryption: SmtpEncryption, + #[serde(rename = "smtp_user")] + #[sqlx(rename = "smtp_user")] + #[patch(attribute(serde( + rename = "smtp_user", + deserialize_with = "deserialize_optional_field", + default + )))] + pub user: Option, + #[serde(rename = "smtp_password")] + #[sqlx(rename = "smtp_password")] + #[patch(attribute(serde( + rename = "smtp_password", + deserialize_with = "deserialize_optional_field", + default + )))] + pub password: Option, + #[serde(rename = "smtp_sender")] + #[sqlx(rename = "smtp_sender")] + #[patch(attribute(serde(rename = "smtp_sender")))] + pub sender: Option, + + // For XOAUTH2 authentication. + #[serde(rename = "smtp_authentication")] + #[sqlx(rename = "smtp_authentication")] + #[patch(attribute(serde(rename = "smtp_authentication")))] + pub authentication: SmtpAuthentication, + #[serde(rename = "smtp_oauth_issuer_url")] + #[sqlx(rename = "smtp_oauth_issuer_url")] + #[patch(attribute(serde(rename = "smtp_oauth_issuer_url")))] + pub oauth_issuer_url: Option, + #[serde(rename = "smtp_oauth_client_id")] + #[sqlx(rename = "smtp_oauth_client_id")] + #[patch(attribute(serde(rename = "smtp_oauth_client_id")))] + pub oauth_client_id: Option, + #[serde(rename = "smtp_oauth_client_secret")] + #[sqlx(rename = "smtp_oauth_client_secret")] + #[patch(attribute(serde(rename = "smtp_oauth_client_secret")))] + pub oauth_client_secret: Option, + #[serde(rename = "smtp_oauth_refresh_token")] + #[sqlx(rename = "smtp_oauth_refresh_token")] + #[patch(attribute(serde(rename = "smtp_oauth_refresh_token")))] + pub oauth_refresh_token: Option, +} + +impl SmtpSettings { + /// Setter for `oauth_refresh_token`. + pub async fn set_oauth_refresh_token<'e, E>( + &mut self, + executor: E, + refresh_token: String, + ) -> sqlx::Result<()> + where + E: PgExecutor<'e>, + { + query!( + "UPDATE settings SET smtp_oauth_refresh_token = $1", + refresh_token + ) + .execute(executor) + .await?; + + self.oauth_refresh_token = Some(refresh_token); + + Ok(()) + } +} + +// Implement manually to avoid exposing secrets. +impl fmt::Debug for SmtpSettings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SmtpSettings") + .field("server", &self.server) + .field("port", &self.port) + .field("encryption", &self.encryption) + .field("user", &self.user) + .field("sender", &self.sender) + .field("authentication", &self.authentication) + .field("oauth_issuer_url", &self.oauth_issuer_url) + .field("oauth_client_id", &self.oauth_client_id) + .finish_non_exhaustive() + } +} diff --git a/crates/defguard_common/src/db/models/setup_auto_adoption.rs b/crates/defguard_common/src/db/models/setup_auto_adoption.rs index f9d0ebb18f..9252d66bd4 100644 --- a/crates/defguard_common/src/db/models/setup_auto_adoption.rs +++ b/crates/defguard_common/src/db/models/setup_auto_adoption.rs @@ -31,14 +31,14 @@ pub enum AutoAdoptionWizardStep { Finished, } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct AutoAdoptionComponentResult { pub success: bool, pub logs: Vec, pub updated_at: NaiveDateTime, } -#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct AutoAdoptionWizardState { #[serde(default)] pub step: AutoAdoptionWizardStep, diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs index e18125abda..bd8a7cb09e 100644 --- a/crates/defguard_common/src/db/models/user.rs +++ b/crates/defguard_common/src/db/models/user.rs @@ -694,16 +694,16 @@ impl User { /// Check if TOTP `code` is valid. #[must_use] pub fn verify_totp_code(&self, code: &str) -> bool { - if let Some(totp_secret) = &self.totp_secret { - if let Ok(timestamp) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - let expected_code = totp_custom::( - TOTP_CODE_VALIDITY_PERIOD, - TOTP_CODE_DIGITS, - totp_secret, - timestamp.as_secs(), - ); - return code == expected_code; - } + if let Some(totp_secret) = &self.totp_secret + && let Ok(timestamp) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) + { + let expected_code = totp_custom::( + TOTP_CODE_VALIDITY_PERIOD, + TOTP_CODE_DIGITS, + totp_secret, + timestamp.as_secs(), + ); + return code == expected_code; } false @@ -1721,9 +1721,9 @@ mod test { // Feature enabled, ldap_remote_enrollment_completed=false // → LDAP user is NOT yet enrolled. let mut settings = Settings::get_current_settings(); - settings.smtp_server = Some("smtp.example.com".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("noreply@example.com".into()); + settings.smtp.server = Some("smtp.example.com".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("noreply@example.com".into()); settings.ldap_url = Some("ldap://localhost".into()); settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); diff --git a/crates/defguard_common/src/types/mod.rs b/crates/defguard_common/src/types/mod.rs index 2b5d8ad81e..5cb6bb9e17 100644 --- a/crates/defguard_common/src/types/mod.rs +++ b/crates/defguard_common/src/types/mod.rs @@ -4,7 +4,7 @@ pub mod user_info; pub type UrlParseError = url::ParseError; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum AuthFlowType { Enrollment, Mfa, diff --git a/crates/defguard_core/src/auth/failed_login.rs b/crates/defguard_core/src/auth/failed_login.rs index 1b3b963975..18b34c9bd8 100644 --- a/crates/defguard_core/src/auth/failed_login.rs +++ b/crates/defguard_core/src/auth/failed_login.rs @@ -102,13 +102,13 @@ impl FailedLoginMap { // Check if user can proceed with login process or should be locked out pub fn verify_username(&mut self, username: &str) -> Result<(), FailedLoginError> { debug!("Checking if user {username} can proceed with login"); - if let Some(failed_login) = self.0.get_mut(username) { - if failed_login.should_prevent_login() { - debug!("Preventing user {username} from logging in"); - // log a failed attempt to prolong timeout - failed_login.increment(); - return Err(FailedLoginError); - } + if let Some(failed_login) = self.0.get_mut(username) + && failed_login.should_prevent_login() + { + debug!("Preventing user {username} from logging in"); + // log a failed attempt to prolong timeout + failed_login.increment(); + return Err(FailedLoginError); } Ok(()) } diff --git a/crates/defguard_core/src/auth/mod.rs b/crates/defguard_core/src/auth/mod.rs index 8d84fab75a..f262e40a4f 100644 --- a/crates/defguard_core/src/auth/mod.rs +++ b/crates/defguard_core/src/auth/mod.rs @@ -233,10 +233,10 @@ where return Err(WebError::Forbidden("user is disabled")); } let settings = Settings::get_current_settings(); - if let Some(default_admin_id) = settings.default_admin_id { - if session_info.user.id == default_admin_id { - return Ok(Self {}); - } + if let Some(default_admin_id) = settings.default_admin_id + && session_info.user.id == default_admin_id + { + return Ok(Self {}); } let pool = extract_pool(parts, state).await?; let groups_with_permission = 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 75b9eb887f..753eb84dae 100644 --- a/crates/defguard_core/src/db/models/activity_log/metadata.rs +++ b/crates/defguard_core/src/db/models/activity_log/metadata.rs @@ -8,7 +8,7 @@ use defguard_common::db::{ group::Group, oauth2client::OAuth2Client, proxy::Proxy, - settings::{LdapSyncStatus, OpenIdUsernameHandling, SmtpEncryption}, + settings::{LdapSyncStatus, OpenIdUsernameHandling, smtp::SmtpEncryption}, user::User, }, }; @@ -413,11 +413,11 @@ impl From for SettingsNoSecrets { instance_name: value.instance_name, main_logo_url: value.main_logo_url, nav_logo_url: value.nav_logo_url, - smtp_server: value.smtp_server, - smtp_port: value.smtp_port, - smtp_encryption: value.smtp_encryption, - smtp_user: value.smtp_user, - smtp_sender: value.smtp_sender, + smtp_server: value.smtp.server, + smtp_port: value.smtp.port, + smtp_encryption: value.smtp.encryption, + smtp_user: value.smtp.user, + smtp_sender: value.smtp.sender, enrollment_vpn_step_optional: value.enrollment_vpn_step_optional, enrollment_welcome_message: value.enrollment_welcome_message, enrollment_welcome_email: value.enrollment_welcome_email, diff --git a/crates/defguard_core/src/enrollment_management.rs b/crates/defguard_core/src/enrollment_management.rs index 3246c93055..46f0f0f740 100644 --- a/crates/defguard_core/src/enrollment_management.rs +++ b/crates/defguard_core/src/enrollment_management.rs @@ -50,25 +50,23 @@ pub async fn start_user_enrollment( user.enrollment_pending = true; user.save(&mut *conn).await?; - if send_user_notification { - if let Some(email) = email { - debug!("Sending an enrollment mail for user {user} to {email}."); - let base_message_context = enrollment.get_welcome_message_context(&mut *conn).await?; - let result = new_account_mail( - &email, - conn, - base_message_context, - enrollment_service_url, - &enrollment.id, - ) - .await; - match result { - Ok(()) => { - info!("Sent enrollment start mail for user {user} to {email}"); - } - Err(err) => { - error!("Error sending mail: {err}"); - } + if send_user_notification && let Some(email) = email { + debug!("Sending an enrollment mail for user {user} to {email}."); + let base_message_context = enrollment.get_welcome_message_context(&mut *conn).await?; + let result = new_account_mail( + &email, + conn, + base_message_context, + enrollment_service_url, + &enrollment.id, + ) + .await; + match result { + Ok(()) => { + info!("Sent enrollment start mail for user {user} to {email}"); + } + Err(err) => { + error!("Error sending mail: {err}"); } } } @@ -134,29 +132,27 @@ pub async fn start_desktop_configuration( desktop_configuration.id, user.username ); - if send_user_notification { - if let Some(email) = email { + if send_user_notification && let Some(email) = email { + debug!( + "Sending a desktop configuration mail for user {} to {email}", + user.username + ); + let base_message_context = desktop_configuration + .get_welcome_message_context(&mut *conn) + .await?; + let result = desktop_start_mail( + &email, + conn, + base_message_context, + &enrollment_service_url, + &desktop_configuration.id, + ) + .await; + if let Err(err) = result { debug!( - "Sending a desktop configuration mail for user {} to {email}", - user.username + "Cannot send an email to the user {} due to the error {err}.", + user.username, ); - let base_message_context = desktop_configuration - .get_welcome_message_context(&mut *conn) - .await?; - let result = desktop_start_mail( - &email, - conn, - base_message_context, - &enrollment_service_url, - &desktop_configuration.id, - ) - .await; - if let Err(err) = result { - debug!( - "Cannot send an email to the user {} due to the error {err}.", - user.username, - ); - } } } info!( diff --git a/crates/defguard_core/src/enterprise/activity_log_stream/activity_log_stream_manager.rs b/crates/defguard_core/src/enterprise/activity_log_stream/activity_log_stream_manager.rs index 0588987a89..07852e0e43 100644 --- a/crates/defguard_core/src/enterprise/activity_log_stream/activity_log_stream_manager.rs +++ b/crates/defguard_core/src/enterprise/activity_log_stream/activity_log_stream_manager.rs @@ -14,7 +14,7 @@ use crate::enterprise::{ }; // Every minute, check if enterprise features are enabled. -const ENTERPRISE_CHECK_PERIOD: Duration = Duration::from_secs(60); +const ENTERPRISE_CHECK_PERIOD: Duration = Duration::from_mins(1); #[instrument(skip_all)] pub async fn run_activity_log_stream_manager( 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 491d70d9c3..ca4d71d1f5 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 @@ -102,19 +102,19 @@ fn build_client(config: &HttpActivityLogStreamConfig) -> Result { - client = client.add_root_certificate(parsed_cert); - } - Err(e) => { - error!( - "Failed to add root certificate for {} activity log stream. Reason: {e}", - config.stream_name - ); - return Err(e); - } + if let Some(cert) = &config.cert + && config.url.contains("https") + { + match tls::Certificate::from_pem(cert.as_bytes()) { + Ok(parsed_cert) => { + client = client.add_root_certificate(parsed_cert); + } + Err(e) => { + error!( + "Failed to add root certificate for {} activity log stream. Reason: {e}", + config.stream_name + ); + return Err(e); } } } diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index 9329442102..134669263f 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -145,7 +145,7 @@ impl From for PgRange { /// Applied state does NOT guarantee that all locations have received the rule /// and performed appropriate operations, only that the next time configuration /// is being sent it will include this rule. -#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Serialize, PartialEq, ToSchema, Type)] +#[derive(Clone, Debug, Default, Deserialize, Hash, Serialize, PartialEq, ToSchema, Type)] #[sqlx(type_name = "aclrule_state", rename_all = "lowercase")] pub enum RuleState { #[default] @@ -244,7 +244,7 @@ impl AclRuleInfo { /// Those objects have their dedicated tables and structures so we provide /// [`AclRuleInfo`] and [`ApiAclRule`] structs that implement appropriate methods /// to combine all the related objects for easier downstream processing. -#[derive(Clone, Debug, Eq, FromRow, Model, PartialEq, ToSchema)] +#[derive(Clone, Debug, FromRow, Model, PartialEq, ToSchema)] pub struct AclRule { pub id: I, // if present points to the original rule before modification / deletion @@ -679,13 +679,13 @@ pub fn parse_ports(ports: &str) -> Result, AclError> { /// Maps [`sqlx::Error`] to [`AclError`] while checking for [`ErrorKind::ForeignKeyViolation`]. fn map_relation_error(err: sqlx::Error, class: &str, id: Id) -> AclError { - if let sqlx::Error::Database(dberror) = &err { - if dberror.kind() == ErrorKind::ForeignKeyViolation { - error!( - "Failed to create ACL related object, foreign key violation: {class}({id}): {dberror}" - ); - return AclError::InvalidRelationError(format!("{class}({id})")); - } + if let sqlx::Error::Database(dberror) = &err + && dberror.kind() == ErrorKind::ForeignKeyViolation + { + error!( + "Failed to create ACL related object, foreign key violation: {class}({id}): {dberror}" + ); + return AclError::InvalidRelationError(format!("{class}({id})")); } error!("Failed to create ACL related object: {err}"); AclError::DbError(err) @@ -1338,7 +1338,7 @@ impl AclRuleInfo { /// Returns the list of explicitly configured allowed network devices or /// a list of all devices if 'allow_all_network_devices' flag is enabled. - pub(crate) async fn get_all_allowed_devices<'e, E: sqlx::PgExecutor<'e>>( + pub(crate) async fn get_all_allowed_devices<'e, E: PgExecutor<'e>>( &self, executor: E, location_id: Id, @@ -1370,7 +1370,7 @@ impl AclRuleInfo { /// Returns the list of explicitly configured denied network devices or /// a list of all devices if 'deny_all_network_devices' flag is enabled. - pub(crate) async fn get_all_denied_devices<'e, E: sqlx::PgExecutor<'e>>( + pub(crate) async fn get_all_denied_devices<'e, E: PgExecutor<'e>>( &self, executor: E, location_id: Id, @@ -1479,7 +1479,7 @@ pub enum AliasState { /// - Destination: the alias defines a complete destination that an ACL rule applies to /// - Component: the alias defines parts of a destination and will be combined with other parts /// manually defined in an ACL rule -#[derive(Clone, Debug, Default, Deserialize, Eq, Serialize, PartialEq, ToSchema, Type)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, ToSchema, Type)] #[sqlx(type_name = "aclalias_kind", rename_all = "lowercase")] pub enum AliasKind { #[default] @@ -1742,11 +1742,11 @@ impl TryFrom<&EditAclAlias> for AclAlias { impl AclAlias { /// Fetch [`AclAlias`] of a given kind. - pub async fn all_of_kind<'e, E>(executor: E, kind: AliasKind) -> Result, sqlx::Error> + pub async fn all_of_kind<'e, E>(executor: E, kind: AliasKind) -> sqlx::Result> where E: PgExecutor<'e>, { - sqlx::query_as::<_, Self>( + query_as::<_, Self>( "SELECT id, parent_id, name, kind, state, addresses, ports, protocols, any_address, \ any_port, any_protocol, modified_at, modified_by \ FROM aclalias WHERE kind = $1", @@ -1760,11 +1760,11 @@ impl AclAlias { executor: E, id: Id, kind: AliasKind, - ) -> Result, sqlx::Error> + ) -> sqlx::Result> where - E: sqlx::PgExecutor<'e>, + E: PgExecutor<'e>, { - sqlx::query_as::<_, Self>( + query_as::<_, Self>( "SELECT id, parent_id, name, kind, state, addresses, ports, protocols, any_address, \ any_port, any_protocol, modified_at, modified_by \ FROM aclalias WHERE id = $1 AND kind = $2", diff --git a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs index 916417a973..d0183e3298 100644 --- a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs +++ b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs @@ -73,7 +73,7 @@ impl EnterpriseSettings { } /// Describes allowed traffic options for clients connecting to the instance. -#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Type, Debug, Default, Copy)] +#[derive(Clone, Deserialize, Serialize, PartialEq, Type, Debug, Default, Copy)] #[sqlx(type_name = "client_traffic_policy", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ClientTrafficPolicy { diff --git a/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs b/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs index 5eae0e6ba8..4722beb7f1 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs @@ -25,7 +25,7 @@ pub(crate) fn api_host_for(base_url: &str) -> &'static str { DEFAULT_API_HOST } -#[derive(Debug, Deserialize, PartialEq, Eq)] +#[derive(Debug, Deserialize, PartialEq)] #[serde(rename_all = "UPPERCASE")] enum UserState { Staged, @@ -572,12 +572,12 @@ impl DirectorySync for JumpCloudDirectorySync { user_email: &str, ) -> Result, DirectorySyncError> { debug!("Getting groups of user {user_email}"); - if let Some(user) = self.get_user_by_email(user_email).await? { - if let Some(user_id) = user.id { - let response = self.query_user_groups(&user_id).await?; - debug!("Got groups response for user {user_id}"); - return Ok(response.into_iter().map(Into::into).collect()); - } + if let Some(user) = self.get_user_by_email(user_email).await? + && let Some(user_id) = user.id + { + let response = self.query_user_groups(&user_id).await?; + debug!("Got groups response for user {user_id}"); + return Ok(response.into_iter().map(Into::into).collect()); } debug!("No user found with email {user_email}, returning an error."); diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index bf4b793c83..7884d631fe 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -678,12 +678,12 @@ fn find_largest_ipv4_subnet_in_range(start: Ipv4Addr, end: Ipv4Addr) -> Option= start_bits && broadcast_addr <= end_bits { - if let Ok(network) = + if network_addr >= start_bits + && broadcast_addr <= end_bits + && let Ok(network) = IpNetwork::new(IpAddr::V4(Ipv4Addr::from(network_addr)), prefix_len) - { - return Some(network); - } + { + return Some(network); } } @@ -723,12 +723,12 @@ fn find_largest_ipv6_subnet_in_range(start: Ipv6Addr, end: Ipv6Addr) -> Option= start_bits && broadcast_addr <= end_bits { - if let Ok(network) = + if network_addr >= start_bits + && broadcast_addr <= end_bits + && let Ok(network) = IpNetwork::new(IpAddr::V6(Ipv6Addr::from(network_addr)), prefix_len) - { - return Some(network); - } + { + return Some(network); } } diff --git a/crates/defguard_core/src/enterprise/handlers/openid_login.rs b/crates/defguard_core/src/enterprise/handlers/openid_login.rs index cbd5928cb1..70cf11039a 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_login.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_login.rs @@ -128,13 +128,14 @@ async fn get_provider_metadata(url: &str) -> Result 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}", - ))), - } + CoreProviderMetadata::discover_async(issuer_url, &async_http_client) + .await + .map_err(|err| { + WebError::Authorization(format!( + "Failed to discover provider metadata, make sure the URL is correct: {url}. \ + Error details: {err}", + )) + }) } /// Build a state with optional embedded data. Useful for passing additional information around the @@ -206,9 +207,7 @@ pub async fn user_from_claims( callback_url: Url, ) -> Result, WebError> { let Some(provider) = OpenIdProvider::get_current(pool).await? else { - return Err(WebError::ObjectNotFound( - "OpenID provider not set".to_string(), - )); + return Err(WebError::ObjectNotFound("OpenID provider not set".into())); }; let (client_id, core_client) = make_oidc_client(callback_url, &provider).await?; let async_http_client = get_async_http_client()?; @@ -285,7 +284,7 @@ pub async fn user_from_claims( "Email not found in the information returned from provider. Make sure your provider is \ configured correctly and that you have granted the necessary permissions to retrieve \ such information." - .to_string(), + .into(), ))?; // Get the *sub* claim from the token. @@ -355,7 +354,7 @@ pub async fn user_from_claims( ); // 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(), + "Failed to extract username from email address".into(), ))?; debug!("Username extracted from email ({email:?}): {username})"); username @@ -494,9 +493,7 @@ pub async fn get_auth_info( ) -> Result<(PrivateCookieJar, ApiResponse), WebError> { let provider = OpenIdProvider::get_current(&appstate.pool).await?; let Some(provider) = provider else { - return Err(WebError::ObjectNotFound( - "OpenID provider not set".to_string(), - )); + return Err(WebError::ObjectNotFound("OpenID provider not set".into())); }; let config = server_config(); diff --git a/crates/defguard_core/src/enterprise/ldap/model.rs b/crates/defguard_core/src/enterprise/ldap/model.rs index e3d0ebbb10..369a5fd670 100644 --- a/crates/defguard_core/src/enterprise/ldap/model.rs +++ b/crates/defguard_core/src/enterprise/ldap/model.rs @@ -5,7 +5,7 @@ use defguard_common::db::{ models::{Settings, User}, }; use ldap3::{Mod, ResultEntry, SearchEntry}; -use sqlx::PgExecutor; +use sqlx::{PgExecutor, query_as}; use super::{ LDAPConfig, @@ -76,10 +76,11 @@ pub(crate) fn user_from_searchentry( ); user.from_ldap = true; // Missing/unparseable userAccountControl falls through with the User::new default (active). - if config.ldap_uses_ad && config.ldap_sync_account_status { - if let Some(uac) = uac_from_entry(entry) { - user.is_active = uac_is_active(uac); - } + if config.ldap_uses_ad + && config.ldap_sync_account_status + && let Some(uac) = uac_from_entry(entry) + { + user.is_active = uac_is_active(uac); } if let Some(rdn) = extract_rdn_value(&entry.dn) { user.ldap_rdn = Some(rdn); @@ -239,10 +240,10 @@ pub(crate) fn user_as_ldap_attrs<'a, I>( attrs.push(("uid", hashset![user.username.as_str()])); } - if let Some(phone) = &user.phone { - if !phone.is_empty() { - attrs.push(("mobile", hashset![phone.as_str()])); - } + if let Some(phone) = &user.phone + && !phone.is_empty() + { + attrs.push(("mobile", hashset![phone.as_str()])); } } if object_classes.contains(UserObjectClass::SimpleSecurityObject.name()) { @@ -309,15 +310,13 @@ pub(super) async fn get_users_without_ldap_path<'e, E>(executor: E) -> sqlx::Res where E: PgExecutor<'e>, { - sqlx::query_as!( + query_as!( User, - " - 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, ldap_remote_enrollment_completed, enrollment_pending \ - FROM \"user\" WHERE ldap_user_path IS NULL - ", + "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, ldap_remote_enrollment_completed, enrollment_pending \ + FROM \"user\" WHERE ldap_user_path IS NULL", ) .fetch_all(executor) .await diff --git a/crates/defguard_core/src/enterprise/ldap/test_client.rs b/crates/defguard_core/src/enterprise/ldap/test_client.rs index 6ea03311ee..77de19b519 100644 --- a/crates/defguard_core/src/enterprise/ldap/test_client.rs +++ b/crates/defguard_core/src/enterprise/ldap/test_client.rs @@ -440,13 +440,11 @@ impl LDAPConnection { // Reflect account status writes in the stored object so reads see writes, like real LDAP. if let Some(Object::User(user)) = self.test_client.objects.get_mut(old_dn) { for modification in &mods { - if let Mod::Replace(attr, values) = modification { - if attr == "userAccountControl" { - if let Some(uac) = values.iter().next().and_then(|v| v.parse::().ok()) - { - user.is_active = uac_is_active(uac); - } - } + if let Mod::Replace(attr, values) = modification + && attr == "userAccountControl" + && let Some(uac) = values.iter().next().and_then(|v| v.parse::().ok()) + { + user.is_active = uac_is_active(uac); } } } diff --git a/crates/defguard_core/src/enterprise/ldap/tests.rs b/crates/defguard_core/src/enterprise/ldap/tests.rs index b4f4c21407..1e7580f0c0 100644 --- a/crates/defguard_core/src/enterprise/ldap/tests.rs +++ b/crates/defguard_core/src/enterprise/ldap/tests.rs @@ -34,6 +34,7 @@ use crate::{ }; const PASSWORD: &str = "test_password"; +const PROXY_URL: &str = "http://proxy.example.com"; fn make_test_user( username: &str, @@ -69,9 +70,9 @@ async fn make_test_admin(pool: &sqlx::PgPool, username: &str) -> User { } fn configure_smtp_and_ldap(settings: &mut Settings) { - settings.smtp_server = Some("smtp.example.com".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("noreply@example.com".into()); + settings.smtp.server = Some("smtp.example.com".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("noreply@example.com".into()); settings.ldap_url = Some("ldap://localhost".into()); settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); @@ -3655,7 +3656,7 @@ async fn test_sync_sends_invite_when_flags_enabled(_: PgPoolOptions, options: Pg settings.ldap_remote_enrollment_enabled = true; settings.ldap_remote_enrollment_send_invite = true; // Provide a valid proxy URL so proxy_public_url() succeeds. - settings.public_proxy_url = "http://proxy.example.com".into(); + settings.public_proxy_url = PROXY_URL.into(); update_current_settings(&pool, settings).await.unwrap(); make_test_admin(&pool, "sync_admin_invite").await; @@ -3719,7 +3720,7 @@ async fn test_sync_invite_skipped_when_no_admin_exists( configure_smtp_and_ldap(&mut settings); settings.ldap_remote_enrollment_enabled = true; settings.ldap_remote_enrollment_send_invite = true; - settings.public_proxy_url = "http://proxy.example.com".into(); + settings.public_proxy_url = PROXY_URL.into(); update_current_settings(&pool, settings).await.unwrap(); // Deliberately do NOT create any admin user. @@ -3768,7 +3769,7 @@ async fn test_ldap_login_sends_invite_when_flags_enabled( configure_smtp_and_ldap(&mut settings); settings.ldap_remote_enrollment_enabled = true; settings.ldap_remote_enrollment_send_invite = true; - settings.public_proxy_url = "http://proxy.example.com".into(); + settings.public_proxy_url = PROXY_URL.into(); update_current_settings(&pool, settings).await.unwrap(); make_test_admin(&pool, "login_admin_invite").await; @@ -3840,7 +3841,7 @@ async fn test_ldap_login_does_not_send_invite_for_existing_user( configure_smtp_and_ldap(&mut settings); settings.ldap_remote_enrollment_enabled = true; settings.ldap_remote_enrollment_send_invite = true; - settings.public_proxy_url = "http://proxy.example.com".into(); + settings.public_proxy_url = PROXY_URL.into(); update_current_settings(&pool, settings).await.unwrap(); make_test_admin(&pool, "login_admin_existing").await; diff --git a/crates/defguard_core/src/enterprise/snat/handlers.rs b/crates/defguard_core/src/enterprise/snat/handlers.rs index a591ac6410..46d12eda15 100644 --- a/crates/defguard_core/src/enterprise/snat/handlers.rs +++ b/crates/defguard_core/src/enterprise/snat/handlers.rs @@ -152,18 +152,17 @@ pub async fn create_snat_binding( // trigger firewall config update on relevant gateways let mut conn = appstate.pool.acquire().await?; - if let Some(location) = WireguardNetwork::find_by_id(&appstate.pool, location.id).await? { - if let Some(firewall_config) = + if let Some(location) = WireguardNetwork::find_by_id(&appstate.pool, location.id).await? + && let Some(firewall_config) = try_get_location_firewall_config(&location, &mut conn).await? - { - debug!( - "Sending firewall config update for location {location} affected by adding new SNAT binding" - ); - appstate.send_wireguard_event(GatewayEvent::FirewallConfigChanged( - location.id, - firewall_config, - )); - } + { + debug!( + "Sending firewall config update for location {location} affected by adding new SNAT binding" + ); + appstate.send_wireguard_event(GatewayEvent::FirewallConfigChanged( + location.id, + firewall_config, + )); } Ok(ApiResponse::json(binding, StatusCode::CREATED)) @@ -252,18 +251,17 @@ pub async fn modify_snat_binding( // trigger firewall config update on relevant gateways let mut conn = appstate.pool.acquire().await?; - if let Some(location) = WireguardNetwork::find_by_id(&appstate.pool, location_id).await? { - if let Some(firewall_config) = + if let Some(location) = WireguardNetwork::find_by_id(&appstate.pool, location_id).await? + && let Some(firewall_config) = try_get_location_firewall_config(&location, &mut conn).await? - { - debug!( - "Sending firewall config update for location {location} affected by adding new SNAT binding" - ); - appstate.send_wireguard_event(GatewayEvent::FirewallConfigChanged( - location_id, - firewall_config, - )); - } + { + debug!( + "Sending firewall config update for location {location} affected by adding new SNAT binding" + ); + appstate.send_wireguard_event(GatewayEvent::FirewallConfigChanged( + location_id, + firewall_config, + )); } Ok(ApiResponse::json(snat_binding, StatusCode::OK)) @@ -337,18 +335,17 @@ pub async fn delete_snat_binding( // trigger firewall config update on relevant gateways let mut conn = appstate.pool.acquire().await?; - if let Some(location) = WireguardNetwork::find_by_id(&appstate.pool, location_id).await? { - if let Some(firewall_config) = + if let Some(location) = WireguardNetwork::find_by_id(&appstate.pool, location_id).await? + && let Some(firewall_config) = try_get_location_firewall_config(&location, &mut conn).await? - { - debug!( - "Sending firewall config update for location {location} affected by adding new SNAT binding" - ); - appstate.send_wireguard_event(GatewayEvent::FirewallConfigChanged( - location_id, - firewall_config, - )); - } + { + debug!( + "Sending firewall config update for location {location} affected by adding new SNAT binding" + ); + appstate.send_wireguard_event(GatewayEvent::FirewallConfigChanged( + location_id, + firewall_config, + )); } Ok(ApiResponse::default()) diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa.rs b/crates/defguard_core/src/grpc/proxy/client_mfa.rs index 5f5c6dc8e2..0736456a1e 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa.rs @@ -51,7 +51,7 @@ use crate::{ const CLIENT_SESSION_TIMEOUT: u64 = 60 * 5; // 10 minutes // How much time the user has to approve remote MFA with mobile device -const REMOTE_AUTH_TIMEOUT: Duration = Duration::from_secs(60); +const REMOTE_AUTH_TIMEOUT: Duration = Duration::from_mins(1); #[derive(Debug, Error)] pub enum ClientMfaServerError { diff --git a/crates/defguard_core/src/grpc/worker.rs b/crates/defguard_core/src/grpc/worker.rs index 2bc001adca..fab7653379 100644 --- a/crates/defguard_core/src/grpc/worker.rs +++ b/crates/defguard_core/src/grpc/worker.rs @@ -250,57 +250,57 @@ impl worker_service_server::WorkerService for WorkerServer { } }; - if let Some(username) = username { - if message.success { - match User::find_by_username(&self.pool, &username).await { - // TODO: Create respectable Authentication KEYS and Add yubikey entry to DB table "yubikey" - Ok(Some(user)) => { - // create yubikey - // FIXME: pass name from user input this is temporary solution - let yubi_count_res = query!( - "SELECT COUNT(*) FROM \"yubikey\" WHERE user_id = $1", - user.id - ) - .fetch_one(&self.pool) + if let Some(username) = username + && message.success + { + match User::find_by_username(&self.pool, &username).await { + // TODO: Create respectable Authentication KEYS and Add yubikey entry to DB table "yubikey" + Ok(Some(user)) => { + // create yubikey + // FIXME: pass name from user input this is temporary solution + let yubi_count_res = query!( + "SELECT COUNT(*) FROM \"yubikey\" WHERE user_id = $1", + user.id + ) + .fetch_one(&self.pool) + .await + .map_err(|_| Status::internal("Failed to count keys"))?; + // FIXME: names may collide + let name = match yubi_count_res.count { + Some(count) => { + let name = format!("YubiKey {}", count + 1); + name + } + None => "YubiKey".to_string(), + }; + let new_yubi = YubiKey::new(name, message.yubikey_serial, user.id) + .save(&self.pool) .await - .map_err(|_| Status::internal("Failed to count keys"))?; - // FIXME: names may collide - let name = match yubi_count_res.count { - Some(count) => { - let name = format!("YubiKey {}", count + 1); - name - } - None => "YubiKey".to_string(), - }; - let new_yubi = YubiKey::new(name, message.yubikey_serial, user.id) - .save(&self.pool) - .await - .map_err(|_| Status::internal("Failed to save YubiKey"))?; - let key_id = new_yubi.id; - let ssh = AuthenticationKey::new( - user.id, - message.ssh_key, - None, - AuthenticationKeyType::Ssh, - Some(key_id), - ); - let gpg = AuthenticationKey::new( - user.id, - message.public_key, - None, - AuthenticationKeyType::Gpg, - Some(key_id), - ); - ssh.save(&self.pool) - .await - .map_err(|_| Status::internal("Failed to save auth key"))?; - gpg.save(&self.pool) - .await - .map_err(|_| Status::internal("Failed to save auth key"))?; - } - Ok(None) => info!("User {username} not found"), - Err(err) => error!("Error {err}"), + .map_err(|_| Status::internal("Failed to save YubiKey"))?; + let key_id = new_yubi.id; + let ssh = AuthenticationKey::new( + user.id, + message.ssh_key, + None, + AuthenticationKeyType::Ssh, + Some(key_id), + ); + let gpg = AuthenticationKey::new( + user.id, + message.public_key, + None, + AuthenticationKeyType::Gpg, + Some(key_id), + ); + ssh.save(&self.pool) + .await + .map_err(|_| Status::internal("Failed to save auth key"))?; + gpg.save(&self.pool) + .await + .map_err(|_| Status::internal("Failed to save auth key"))?; } + Ok(None) => info!("User {username} not found"), + Err(err) => error!("Error {err}"), } } diff --git a/crates/defguard_core/src/handlers/forward_auth.rs b/crates/defguard_core/src/handlers/forward_auth.rs index 5dc93bfff5..915d1be53c 100644 --- a/crates/defguard_core/src/handlers/forward_auth.rs +++ b/crates/defguard_core/src/handlers/forward_auth.rs @@ -95,22 +95,22 @@ fn login_redirect(headers: ForwardAuthHeaders) -> Result>( } /// Validate name provided by user +#[must_use] pub fn validate_name(name: &str) -> bool { if name.is_empty() { return false; diff --git a/crates/defguard_core/src/handlers/network_devices.rs b/crates/defguard_core/src/handlers/network_devices.rs index 4b3f1d11d3..c6ad131871 100644 --- a/crates/defguard_core/src/handlers/network_devices.rs +++ b/crates/defguard_core/src/handlers/network_devices.rs @@ -183,14 +183,13 @@ pub(crate) async fn get_network_device( ); let device = Device::find_by_id(&appstate.pool, device_id).await?; - if let Some(device) = device { - if device.device_type == DeviceType::Network { - let mut transaction = appstate.pool.begin().await?; - let network_device_info = - NetworkDeviceInfo::from_device(device, &mut transaction).await?; - transaction.commit().await?; - return Ok(ApiResponse::json(network_device_info, StatusCode::OK)); - } + if let Some(device) = device + && device.device_type == DeviceType::Network + { + let mut transaction = appstate.pool.begin().await?; + let network_device_info = NetworkDeviceInfo::from_device(device, &mut transaction).await?; + transaction.commit().await?; + return Ok(ApiResponse::json(network_device_info, StatusCode::OK)); } error!( "Failed to retrieve network device with id: {device_id}, such network device doesn't exist." @@ -781,15 +780,14 @@ pub async fn modify_network_device( appstate.send_wireguard_event(GatewayEvent::DeviceModified(device_info)); // send firewall update event if ACLs are enabled - if device_network.acl_enabled { - if let Some(firewall_config) = + if device_network.acl_enabled + && let Some(firewall_config) = try_get_location_firewall_config(&device_network, &mut transaction).await? - { - appstate.send_wireguard_event(GatewayEvent::FirewallConfigChanged( - device_network.id, - firewall_config, - )); - } + { + appstate.send_wireguard_event(GatewayEvent::FirewallConfigChanged( + device_network.id, + firewall_config, + )); } info!( diff --git a/crates/defguard_core/src/handlers/openid_flow.rs b/crates/defguard_core/src/handlers/openid_flow.rs index 84890afa46..b593125365 100644 --- a/crates/defguard_core/src/handlers/openid_flow.rs +++ b/crates/defguard_core/src/handlers/openid_flow.rs @@ -54,38 +54,38 @@ use crate::{ }; /// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims -impl From<&UserClaims> for StandardClaims { - fn from(user_claims: &UserClaims) -> StandardClaims { - let mut claims = StandardClaims::new(SubjectIdentifier::new(user_claims.sub.clone())); +impl From for StandardClaims { + fn from(user_claims: UserClaims) -> Self { + let mut claims = Self::new(SubjectIdentifier::new(user_claims.sub)); - if let Some(name) = &user_claims.name { + if let Some(name) = user_claims.name { let mut localized_claim = LocalizedClaim::new(); - localized_claim.insert(None, EndUserName::new(name.clone())); + localized_claim.insert(None, EndUserName::new(name)); claims = claims.set_name(Some(localized_claim)); } - if let Some(given_name) = &user_claims.given_name { + if let Some(given_name) = user_claims.given_name { let mut localized_claim = LocalizedClaim::new(); - localized_claim.insert(None, EndUserGivenName::new(given_name.clone())); + localized_claim.insert(None, EndUserGivenName::new(given_name)); claims = claims.set_given_name(Some(localized_claim)); } - if let Some(family_name) = &user_claims.family_name { + if let Some(family_name) = user_claims.family_name { let mut localized_claim = LocalizedClaim::new(); - localized_claim.insert(None, EndUserFamilyName::new(family_name.clone())); + localized_claim.insert(None, EndUserFamilyName::new(family_name)); claims = claims.set_family_name(Some(localized_claim)); } - if let Some(email) = &user_claims.email { - claims = claims.set_email(Some(EndUserEmail::new(email.clone()))); + if let Some(email) = user_claims.email { + claims = claims.set_email(Some(EndUserEmail::new(email))); } - if let Some(phone_number) = &user_claims.phone_number { - claims = claims.set_phone_number(Some(EndUserPhoneNumber::new(phone_number.clone()))); + if let Some(phone_number) = user_claims.phone_number { + claims = claims.set_phone_number(Some(EndUserPhoneNumber::new(phone_number))); } - if let Some(username) = &user_claims.preferred_username { - claims = claims.set_preferred_username(Some(EndUserUsername::new(username.clone()))); + if let Some(username) = user_claims.preferred_username { + claims = claims.set_preferred_username(Some(EndUserUsername::new(username))); } claims @@ -139,24 +139,23 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { if let Some(basic_auth) = parts.headers.get(AUTHORIZATION).and_then(|value| { - if let Ok(value) = value.to_str() { - if value.starts_with("Basic ") { - return value.get(6..); - } + if let Ok(value) = value.to_str() + && value.starts_with("Basic ") + { + return value.get(6..); } None }) { - if let Ok(decoded) = BASE64_STANDARD.decode(basic_auth) { - if let Ok(auth_pair) = String::from_utf8(decoded) { - if let Some((client_id, client_secret)) = auth_pair.split_once(':') { - let appstate = AppState::from_ref(state); - return Ok(Self( - OAuth2Client::find_by_auth(&appstate.pool, client_id, client_secret) - .await - .map_err(WebError::from)?, - )); - } - } + if let Ok(decoded) = BASE64_STANDARD.decode(basic_auth) + && let Ok(auth_pair) = String::from_utf8(decoded) + && let Some((client_id, client_secret)) = auth_pair.split_once(':') + { + let appstate = AppState::from_ref(state); + return Ok(Self( + OAuth2Client::find_by_auth(&appstate.pool, client_id, client_secret) + .await + .map_err(WebError::from)?, + )); } Err(WebError::Authorization("Invalid credentials".into())) } else { @@ -719,7 +718,7 @@ pub async fn secure_authorization( /// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest #[derive(Deserialize)] pub struct TokenRequest { - grant_type: String, + grant_type: CoreGrantType, // grant_type == "authorization_code" code: Option, redirect_uri: Option, @@ -878,8 +877,8 @@ pub async fn token( Form(form): Form, ) -> ApiResult { // TODO: cleanup branches - match form.grant_type.as_str() { - "authorization_code" => { + match form.grant_type { + CoreGrantType::AuthorizationCode => { debug!("Staring authorization_code flow"); // for logging @@ -897,7 +896,7 @@ pub async fn token( 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( + let response = StandardErrorResponse::new( CoreErrorResponseType::UnauthorizedClient, None, None, @@ -946,7 +945,7 @@ pub async fn token( match form.authorization_code_flow( &auth_code, &token, - (&user_claims).into(), + user_claims.into(), &base_url, client.client_secret, openid_key, @@ -965,10 +964,7 @@ pub async fn token( "Error issuing new token for user {} client {}: {err}", user.username, client.name ); - let response = - StandardErrorResponse::::new( - err, None, None, - ); + let response = StandardErrorResponse::new(err, None, None); return Ok(ApiResponse::json( response, StatusCode::BAD_REQUEST, @@ -994,42 +990,40 @@ pub async fn token( error!("No code provided in request for client id `{form_client_id}`"); } } - "refresh_token" => { + CoreGrantType::RefreshToken => { debug!("Starting refresh_token flow"); - if let Some(refresh_token) = form.refresh_token { - if let Ok(Some(mut token)) = + if let Some(refresh_token) = form.refresh_token + && 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(response, 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(response, StatusCode::BAD_REQUEST)); - } + { + 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(response, StatusCode::BAD_REQUEST)); + }; - token.refresh_and_save(&appstate.pool).await?; - let response = TokenRequest::refresh_token_flow(&token); - token.save(&appstate.pool).await?; - return Ok(ApiResponse::json(response, StatusCode::OK)); + if !client.enabled { + error!("OAuth client id `{}` is disabled", client.name); + let response = StandardErrorResponse::new( + CoreErrorResponseType::UnauthorizedClient, + None, + None, + ); + return Ok(ApiResponse::json(response, StatusCode::BAD_REQUEST)); } + + token.refresh_and_save(&appstate.pool).await?; + let response = TokenRequest::refresh_token_flow(&token); + token.save(&appstate.pool).await?; + return Ok(ApiResponse::json(response, StatusCode::OK)); } } _ => (), // TODO: Err(CoreErrorResponseType::UnsupportedGrantType), } let err = CoreErrorResponseType::UnsupportedGrantType; - let response = StandardErrorResponse::::new(err, None, None); + let response = StandardErrorResponse::new(err, None, None); Ok(ApiResponse::json(response, StatusCode::BAD_REQUEST)) } @@ -1076,7 +1070,7 @@ pub async fn userinfo(State(appstate): State, headers: HeaderMap) -> A let user_claims = UserClaims::from_user(&user, &client, &oauth2token); Ok(ApiResponse::json( - StandardClaims::::from(&user_claims), + StandardClaims::from(user_claims), StatusCode::OK, )) } diff --git a/crates/defguard_core/src/handlers/settings.rs b/crates/defguard_core/src/handlers/settings.rs index 5af53c323a..374c13f7c0 100644 --- a/crates/defguard_core/src/handlers/settings.rs +++ b/crates/defguard_core/src/handlers/settings.rs @@ -131,20 +131,20 @@ pub async fn patch_settings( let license = data.license.clone(); // update LDAP sync status if relevant settings have been changed - if let Some(ldap_enabled) = data.ldap_enabled { - if !ldap_enabled { - settings.ldap_sync_status = LdapSyncStatus::OutOfSync; - } + if let Some(ldap_enabled) = data.ldap_enabled + && !ldap_enabled + { + 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 = LdapSyncStatus::OutOfSync; - } + if let Some(ldap_authority) = data.ldap_is_authoritative + && settings.ldap_is_authoritative != ldap_authority + { + 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 = LdapSyncStatus::OutOfSync; - } + if let Some(ldap_sync_groups) = &data.ldap_sync_groups + && &settings.ldap_sync_groups != ldap_sync_groups + { + settings.ldap_sync_status = LdapSyncStatus::OutOfSync; } settings.apply(data); diff --git a/crates/defguard_core/src/handlers/user.rs b/crates/defguard_core/src/handlers/user.rs index df73788eb7..177d9fe09c 100644 --- a/crates/defguard_core/src/handlers/user.rs +++ b/crates/defguard_core/src/handlers/user.rs @@ -92,12 +92,12 @@ pub fn check_username(username: &str) -> Result<(), WebError> { } // check first character is a letter or digit - if let Some(first_char) = username.chars().next() { - if !first_char.is_ascii_alphanumeric() { - return Err(WebError::Serialization( - "Username must not start with a special character".into(), - )); - } + if let Some(first_char) = username.chars().next() + && !first_char.is_ascii_alphanumeric() + { + return Err(WebError::Serialization( + "Username must not start with a special character".into(), + )); } // check if username contains only valid characters @@ -532,11 +532,11 @@ pub(crate) async fn add_user( } // 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::with_status(StatusCode::BAD_REQUEST)); - } + if let Some(ref phone) = user_data.phone + && !is_valid_phone_number(phone) + { + debug!("Invalid phone number for new user {username}: {phone}"); + return Ok(ApiResponse::with_status(StatusCode::BAD_REQUEST)); } let password = match &user_data.password { @@ -904,11 +904,11 @@ pub(crate) async fn modify_user( } // 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::with_status(StatusCode::BAD_REQUEST)); - } + if let Some(ref phone) = user_info.phone + && !is_valid_phone_number(phone) + { + debug!("Invalid phone number for user {username}: {phone}"); + return Ok(ApiResponse::with_status(StatusCode::BAD_REQUEST)); } let status_changing = user_info.is_active != user.is_active; diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index d7b03b4246..f4be047252 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -874,19 +874,17 @@ pub(crate) async fn add_device( // if they have ACL enabled & enterprise features are active for location_id in affected_location_ids { if let Some(location) = WireguardNetwork::find_by_id(&mut *transaction, location_id).await? - { - if let Some(firewall_config) = + && let Some(firewall_config) = try_get_location_firewall_config(&location, &mut transaction).await? - { - debug!( - "Sending firewall config update for location {location} affected by adding new \ + { + debug!( + "Sending firewall config update for location {location} affected by adding new \ user {username} devices" - ); - events.push(GatewayEvent::FirewallConfigChanged( - location_id, - firewall_config, - )); - } + ); + events.push(GatewayEvent::FirewallConfigChanged( + location_id, + firewall_config, + )); } } @@ -1169,18 +1167,16 @@ pub(crate) async fn delete_device( for info in &device_info.network_info { if let Some(location) = WireguardNetwork::find_by_id(&mut *transaction, info.network_id).await? - { - if let Some(firewall_config) = + && let Some(firewall_config) = try_get_location_firewall_config(&location, &mut transaction).await? - { - debug!( - "Sending firewall config update for location {location} affected by deleting user {username} device" - ); - events.push(GatewayEvent::FirewallConfigChanged( - location.id, - firewall_config, - )); - } + { + debug!( + "Sending firewall config update for location {location} affected by deleting user {username} device" + ); + events.push(GatewayEvent::FirewallConfigChanged( + location.id, + firewall_config, + )); } } diff --git a/crates/defguard_core/src/letsencrypt.rs b/crates/defguard_core/src/letsencrypt.rs index 8368618a0f..703f718c39 100644 --- a/crates/defguard_core/src/letsencrypt.rs +++ b/crates/defguard_core/src/letsencrypt.rs @@ -28,7 +28,7 @@ use tonic::{Request, service::Interceptor}; /// Maximum time (seconds) allowed for the ACME flow to complete end-to-end. #[cfg(not(test))] -pub const ACME_TIMEOUT: Duration = Duration::from_secs(300); +pub const ACME_TIMEOUT: Duration = Duration::from_mins(5); #[cfg(test)] pub const ACME_TIMEOUT: Duration = Duration::from_secs(1); const LETSENCRYPT_EXPIRY_THRESHOLD: TimeDelta = TimeDelta::days(14); @@ -540,11 +540,11 @@ mod tests { .expect("failed to initialize settings"); let mut settings = Settings::get_current_settings(); settings.public_proxy_url = format!("https://{hostname}"); - settings.smtp_server = Some("smtp.example.com".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("noreply@example.com".into()); - settings.smtp_user = Some(String::new()); - settings.smtp_password = Some(SecretStringWrapper::from_str("").unwrap()); + settings.smtp.server = Some("smtp.example.com".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("noreply@example.com".into()); + settings.smtp.user = Some(String::new()); + settings.smtp.password = Some(SecretStringWrapper::from_str("").unwrap()); defguard_common::db::models::settings::set_settings(Some(settings)); } diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index f770ee7db8..92d2a38f97 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -228,7 +228,7 @@ const NETWORK_IMPORT_BODY_LIMIT: usize = 4 * 1024 * 1024; // 4 MB const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); /// How often the rate limiter evicts stale per-IP entries from its in-memory store. -const RATE_LIMITER_CLEANUP_PERIOD: Duration = Duration::from_secs(60); +const RATE_LIMITER_CLEANUP_PERIOD: Duration = Duration::from_mins(1); 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}?$") diff --git a/crates/defguard_core/src/support.rs b/crates/defguard_core/src/support.rs index 3b26f3370f..0e806f768b 100644 --- a/crates/defguard_core/src/support.rs +++ b/crates/defguard_core/src/support.rs @@ -29,7 +29,7 @@ pub(crate) async fn dump_config(conn: &mut PgConnection) -> Result { - settings.smtp_password = None; + settings.smtp.password = None; settings.ldap_bind_password = None; json!(settings) } diff --git a/crates/defguard_core/src/user_management.rs b/crates/defguard_core/src/user_management.rs index 1fc5d7cea9..b5793af9ab 100644 --- a/crates/defguard_core/src/user_management.rs +++ b/crates/defguard_core/src/user_management.rs @@ -42,18 +42,17 @@ pub async fn delete_user_and_cleanup_devices( // send firewall config updates to affected locations // if they have ACL enabled & enterprise features are active for location_id in affected_location_ids { - if let Some(location) = WireguardNetwork::find_by_id(&mut *conn, location_id).await? { - if let Some(firewall_config) = + if let Some(location) = WireguardNetwork::find_by_id(&mut *conn, location_id).await? + && let Some(firewall_config) = try_get_location_firewall_config(&location, &mut *conn).await? - { - debug!( - "Sending firewall config update for location {location} affected by deleting user {username} devices" - ); - events.push(GatewayEvent::FirewallConfigChanged( - location_id, - firewall_config, - )); - } + { + debug!( + "Sending firewall config update for location {location} affected by deleting user {username} devices" + ); + events.push(GatewayEvent::FirewallConfigChanged( + location_id, + firewall_config, + )); } } diff --git a/crates/defguard_core/src/utility_thread.rs b/crates/defguard_core/src/utility_thread.rs index 4a5767bf9f..2df5af5ac1 100644 --- a/crates/defguard_core/src/utility_thread.rs +++ b/crates/defguard_core/src/utility_thread.rs @@ -397,72 +397,68 @@ async fn check_certificates( }; // Email notifications for custom uploaded certs - if let ProxyCertSource::Custom = cert.proxy_http_cert_source { - if let Some(proxy_http_cert_expiry) = cert.proxy_http_cert_expiry { - expiry_check(&mut conn, "Edge HTTPS", proxy_http_cert_expiry).await; - } + if let ProxyCertSource::Custom = cert.proxy_http_cert_source + && let Some(proxy_http_cert_expiry) = cert.proxy_http_cert_expiry + { + expiry_check(&mut conn, "Edge HTTPS", proxy_http_cert_expiry).await; } - if let CoreCertSource::Custom = cert.core_http_cert_source { - if let Some(core_http_cert_expiry) = cert.core_http_cert_expiry { - expiry_check(&mut conn, "Core HTTPS", core_http_cert_expiry).await; - } + if let CoreCertSource::Custom = cert.core_http_cert_source + && let Some(core_http_cert_expiry) = cert.core_http_cert_expiry + { + expiry_check(&mut conn, "Core HTTPS", core_http_cert_expiry).await; } // Auto-refresh self-signed certs when close to expiry let now = Utc::now().naive_utc(); - if let CoreCertSource::SelfSigned = cert.core_http_cert_source { - if let Some(expiry) = cert.core_http_cert_expiry { - let expire_in = expiry - now; - if expire_in <= SELF_SIGNED_REFRESH_THRESHOLD { - info!( - "Core self-signed HTTPS certificate expires in {} days, refreshing", - expire_in.num_days() - ); - match refresh_core_self_signed_cert(pool).await { - Ok((_, _, new_expiry)) => { - info!( - "Core self-signed HTTPS certificate refreshed, new expiry: {new_expiry}" - ); - if let Err(err) = web_reload_tx.send(()) { - error!("Failed to trigger core web server reload: {err}"); - } - } - Err(err) => { - error!("Failed to refresh Core self-signed HTTPS certificate: {err}"); + if let CoreCertSource::SelfSigned = cert.core_http_cert_source + && let Some(expiry) = cert.core_http_cert_expiry + { + let expire_in = expiry - now; + if expire_in <= SELF_SIGNED_REFRESH_THRESHOLD { + info!( + "Core self-signed HTTPS certificate expires in {} days, refreshing", + expire_in.num_days() + ); + match refresh_core_self_signed_cert(pool).await { + Ok((_, _, new_expiry)) => { + info!("Core self-signed HTTPS certificate refreshed, new expiry: {new_expiry}"); + if let Err(err) = web_reload_tx.send(()) { + error!("Failed to trigger core web server reload: {err}"); } } + Err(err) => { + error!("Failed to refresh Core self-signed HTTPS certificate: {err}"); + } } } } - if let ProxyCertSource::SelfSigned = cert.proxy_http_cert_source { - if let Some(expiry) = cert.proxy_http_cert_expiry { - let expire_in = expiry - now; - if expire_in <= SELF_SIGNED_REFRESH_THRESHOLD { - info!( - "Proxy self-signed HTTPS certificate expires in {} days, refreshing", - expire_in.num_days() - ); - match refresh_proxy_self_signed_cert(pool).await { - Ok((cert_pem, key_pem, new_expiry)) => { - info!( - "Proxy self-signed HTTPS certificate refreshed, new expiry: {new_expiry}" - ); - if let Err(err) = proxy_control_tx - .send(ProxyControlMessage::BroadcastHttpsCerts { cert_pem, key_pem }) - .await - { - error!( - "Failed to broadcast refreshed proxy HTTPS cert to proxies: {err}" - ); - } - } - Err(err) => { - error!("Failed to refresh Proxy self-signed HTTPS certificate: {err}"); + if let ProxyCertSource::SelfSigned = cert.proxy_http_cert_source + && let Some(expiry) = cert.proxy_http_cert_expiry + { + let expire_in = expiry - now; + if expire_in <= SELF_SIGNED_REFRESH_THRESHOLD { + info!( + "Proxy self-signed HTTPS certificate expires in {} days, refreshing", + expire_in.num_days() + ); + match refresh_proxy_self_signed_cert(pool).await { + Ok((cert_pem, key_pem, new_expiry)) => { + info!( + "Proxy self-signed HTTPS certificate refreshed, new expiry: {new_expiry}" + ); + if let Err(err) = proxy_control_tx + .send(ProxyControlMessage::BroadcastHttpsCerts { cert_pem, key_pem }) + .await + { + error!("Failed to broadcast refreshed proxy HTTPS cert to proxies: {err}"); } } + Err(err) => { + error!("Failed to refresh Proxy self-signed HTTPS certificate: {err}"); + } } } } diff --git a/crates/defguard_core/tests/integration/api/auth.rs b/crates/defguard_core/tests/integration/api/auth.rs index dd7a5dad31..5dace4c6b4 100644 --- a/crates/defguard_core/tests/integration/api/auth.rs +++ b/crates/defguard_core/tests/integration/api/auth.rs @@ -432,9 +432,9 @@ async fn test_email_mfa(_: PgPoolOptions, options: PgConnectOptions) { // add dummy SMTP settings let mut settings = Settings::get_current_settings(); - settings.smtp_server = Some("smtp_server".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("smtp@sender.pl".into()); + settings.smtp.server = Some("smtp_server".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("smtp@sender.pl".into()); update_current_settings(&pool, settings).await.unwrap(); // initialize email MFA setup @@ -579,9 +579,9 @@ async fn dg25_15_test_email_mfa_brute_force(_: PgPoolOptions, options: PgConnect // add dummy SMTP settings let mut settings = Settings::get_current_settings(); - settings.smtp_server = Some("smtp_server".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("smtp@sender.pl".into()); + settings.smtp.server = Some("smtp_server".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("smtp@sender.pl".into()); update_current_settings(&pool, settings).await.unwrap(); // initialize email MFA setup diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index ba9260eb5c..e455e03b74 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -311,9 +311,9 @@ pub(crate) fn set_enterprise_license() { /// Set minimal SMTP fields on a [`Settings`] so that `smtp_configured()` returns `true`. pub(crate) fn configure_smtp(settings: &mut Settings) { - settings.smtp_server = Some("smtp.example.com".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("noreply@example.com".into()); + settings.smtp.server = Some("smtp.example.com".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("noreply@example.com".into()); } /// Set minimal LDAP fields on a [`Settings`] so that `ldap_configured()` returns `true`. diff --git a/crates/defguard_core/tests/integration/api/settings.rs b/crates/defguard_core/tests/integration/api/settings.rs index 86e446fc9b..8ba10996e2 100644 --- a/crates/defguard_core/tests/integration/api/settings.rs +++ b/crates/defguard_core/tests/integration/api/settings.rs @@ -78,14 +78,14 @@ async fn test_patch_settings_clears_optional_fields(_: PgPoolOptions, options: P assert_eq!(response.status(), StatusCode::OK); let settings: Settings = response.json().await; assert_eq!( - settings.smtp_user, - Some("testuser".to_string()), + settings.smtp.user, + Some("testuser".to_owned()), "smtp_user should be set after initial PATCH" ); // smtp_password is redacted in the API response; verify via DB let from_db = Settings::get(&pool).await.unwrap().unwrap(); assert!( - from_db.smtp_password.is_some(), + from_db.smtp.password.is_some(), "smtp_password should be set in DB after initial PATCH" ); @@ -98,11 +98,11 @@ async fn test_patch_settings_clears_optional_fields(_: PgPoolOptions, options: P // assert both fields are cleared in the DB let from_db = Settings::get(&pool).await.unwrap().unwrap(); assert!( - from_db.smtp_user.is_none(), + from_db.smtp.user.is_none(), "smtp_user should be cleared to None after PATCH with null" ); assert!( - from_db.smtp_password.is_none(), + from_db.smtp.password.is_none(), "smtp_password should be cleared to None after PATCH with null" ); @@ -232,9 +232,9 @@ async fn test_ldap_remote_enrollment_validation(_: PgPoolOptions, options: PgCon // configure SMTP via direct DB mutation (same pattern used for test setup in auth tests) let mut settings = Settings::get_current_settings(); - settings.smtp_server = Some("smtp.example.com".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("noreply@example.com".into()); + settings.smtp.server = Some("smtp.example.com".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("noreply@example.com".into()); update_current_settings(&client_state.pool, settings) .await .unwrap(); diff --git a/crates/defguard_core/tests/integration/ldap/mod.rs b/crates/defguard_core/tests/integration/ldap/mod.rs index 96b926904e..1fe9a6b7f3 100644 --- a/crates/defguard_core/tests/integration/ldap/mod.rs +++ b/crates/defguard_core/tests/integration/ldap/mod.rs @@ -84,9 +84,9 @@ fn enable_secure_enrollment() { let mut settings = Settings::get_current_settings(); settings.ldap_remote_enrollment_enabled = true; settings.ldap_remote_enrollment_send_invite = true; - settings.smtp_server = Some("smtp.example.com".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("noreply@test.defguard".into()); + settings.smtp.server = Some("smtp.example.com".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("noreply@test.defguard".into()); settings.public_proxy_url = "http://proxy.example.com".into(); set_settings(Some(settings)); } diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 79de806a87..e9a3f15165 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -266,11 +266,10 @@ impl GatewayHandler { // Send email only if disconnection time is before the connection time. if let (Some(connected_at), Some(disconnected_at)) = (self.gateway.connected_at, self.gateway.disconnected_at) + && disconnected_at > connected_at { - if disconnected_at > connected_at { - info!("{} disconnected; email notification not sent", self.gateway); - return; - } + info!("{} disconnected; email notification not sent", self.gateway); + return; } debug!("Sending Gateway disconnect email notification"); diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index ead2a84619..68e73cd32f 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -455,13 +455,13 @@ impl GatewayManager { } // Only mark disconnected if the gateway was actually connected - if gateway.is_connected() { - if let Err(err) = gateway.touch_disconnected(&self.pool).await { - error!( - "Failed to update disconnection time for Gateway \ + if gateway.is_connected() + && let Err(err) = gateway.touch_disconnected(&self.pool).await + { + error!( + "Failed to update disconnection time for Gateway \ id={gateway_id} after database change: {err}" - ); - } + ); } if gateway.enabled { diff --git a/crates/defguard_mail/Cargo.toml b/crates/defguard_mail/Cargo.toml index 2ae28f1d03..4cf337077e 100644 --- a/crates/defguard_mail/Cargo.toml +++ b/crates/defguard_mail/Cargo.toml @@ -12,6 +12,7 @@ defguard_common.workspace = true chrono.workspace = true lettre.workspace = true +openidconnect.workspace = true pulldown-cmark.workspace = true reqwest.workspace = true serde.workspace = true @@ -23,10 +24,10 @@ tokio.workspace = true tracing.workspace = true humantime.workspace = true +css-inline = "0.20" image = "0.25" # match with qrforge mrml = "6.0" qrforge = {version = "0.1", default-features = false, features = ["image"]} -css-inline = "0.20.2" [dev-dependencies] claims.workspace = true diff --git a/crates/defguard_mail/src/lib.rs b/crates/defguard_mail/src/lib.rs index f14e3888de..2dc9e329e0 100644 --- a/crates/defguard_mail/src/lib.rs +++ b/crates/defguard_mail/src/lib.rs @@ -4,49 +4,46 @@ //! - [RFC 2557](https://datatracker.ietf.org/doc/html/rfc2557) //! - [Meaning of mulitpart](https://www.codestudy.net/blog/mail-multipart-alternative-vs-multipart-mixed/) -use defguard_common::db::models::{Settings, settings::SmtpEncryption}; - -use crate::mail::MailError; -pub use crate::mail::{Attachment, Mail}; - pub mod mail; pub(crate) mod mail_context; mod qr; pub mod templates; #[cfg(test)] mod tests; +mod xoauth2; -/// Subset of Settings representing SMTP configuration. -pub(crate) struct SmtpSettings { - server: String, - port: u16, - encryption: SmtpEncryption, - user: Option, - password: Option, - sender: String, -} +#[derive(Debug, thiserror::Error)] +pub enum MailError { + #[error(transparent)] + LettreError(#[from] lettre::error::Error), + + #[error(transparent)] + AddressError(#[from] lettre::address::AddressError), + + #[error(transparent)] + SmtpError(#[from] lettre::transport::smtp::Error), + + #[error(transparent)] + SqlxError(#[from] sqlx::Error), + + #[error("SMTP not configured")] + SmtpNotConfigured, + + #[error("Invalid port: {0}")] + InvalidPort(i32), + + #[error(transparent)] + ReqwestError(#[from] openidconnect::reqwest::Error), + + #[error(transparent)] + UrlError(#[from] openidconnect::url::ParseError), + + #[error(transparent)] + OAuth2Error(#[from] openidconnect::ConfigurationError), + + #[error("Open ID discovery")] + OpenIDDiscovery, -impl SmtpSettings { - /// Constructs `SmtpSettings` from `Settings`. Returns error if `SmtpSettings` are incomplete. - pub(crate) fn from_settings(settings: Settings) -> Result { - if let (Some(server), Some(port), Some(sender)) = ( - settings.smtp_server, - settings.smtp_port, - settings.smtp_sender, - ) { - let port = port.try_into().map_err(|_| MailError::InvalidPort(port))?; - Ok(Self { - server, - port, - encryption: settings.smtp_encryption, - user: settings.smtp_user, - password: settings - .smtp_password - .map(|p| p.expose_secret().to_string()), - sender, - }) - } else { - Err(MailError::SmtpNotConfigured) - } - } + #[error("Refresh token exchange")] + RefreshTokenExchange, } diff --git a/crates/defguard_mail/src/mail.rs b/crates/defguard_mail/src/mail.rs index 40e8fd73ae..b94645d465 100644 --- a/crates/defguard_mail/src/mail.rs +++ b/crates/defguard_mail/src/mail.rs @@ -2,24 +2,27 @@ use std::{str::FromStr, time::Duration}; use defguard_common::db::models::{ MFAMethod, Settings, - settings::{SmtpEncryption, defaults::WELCOME_EMAIL_SUBJECT}, + settings::{ + defaults::WELCOME_EMAIL_SUBJECT, + smtp::{SmtpAuthentication, SmtpEncryption, SmtpSettings}, + }, }; use lettre::{ AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, message::{Body, Mailbox, MultiPart, SinglePart, header::ContentType}, - transport::smtp::authentication::Credentials, + transport::smtp::authentication::{Credentials, Mechanism}, }; use serde::Serialize; use sqlx::PgConnection; use tera::{Context, Tera, Value}; -use thiserror::Error; use tracing::{debug, error, info, warn}; -use super::SmtpSettings; -use crate::{ +use super::{ + MailError, mail_context::MailContext, qr::qr_png, templates::{DEFAULT_LANG, TemplateError}, + xoauth2::obtain_access_token, }; #[derive(Debug)] @@ -58,27 +61,6 @@ static NEW_ACCOUNT_2: &[u8] = include_bytes!("../assets/new_account_2.png"); static GOOGLE_PLAY: &[u8] = include_bytes!("../assets/google_play.png"); static APPLE: &[u8] = include_bytes!("../assets/apple.png"); -#[derive(Debug, Error)] -pub enum MailError { - #[error(transparent)] - LettreError(#[from] lettre::error::Error), - - #[error(transparent)] - AddressError(#[from] lettre::address::AddressError), - - #[error(transparent)] - SmtpError(#[from] lettre::transport::smtp::Error), - - #[error(transparent)] - SqlxError(#[from] sqlx::Error), - - #[error("SMTP not configured")] - SmtpNotConfigured, - - #[error("Invalid port: {0}")] - InvalidPort(i32), -} - /// Mail message #[derive(Debug)] pub struct Mail { @@ -197,22 +179,15 @@ impl Mail { let (to, subject) = (self.to.clone(), self.subject.clone()); debug!("Sending mail to: {to}, subject: {subject}"); - // fetch SMTP settings - let settings = Settings::get_current_settings(); - let settings = match SmtpSettings::from_settings(settings) { - Ok(settings) => settings, - Err(err @ MailError::SmtpNotConfigured) => { - warn!("SMTP not configured, email sending skipped"); - return Err(err); - } - Err(err) => { - error!("Error retrieving SMTP settings: {err}"); - return Err(err); - } + // SMTP settings + let smtp_settings = Settings::get_current_settings().smtp; + let Some(sender) = &smtp_settings.sender else { + warn!("SMTP not configured, email sending skipped"); + return Err(MailError::SmtpNotConfigured); }; // Construct lettre Message - let message = match self.into_message(&settings.sender) { + let message = match self.into_message(sender) { Ok(message) => message, Err(err) => { error!("Failed to build message to: {to}, subject: {subject}, error: {err}"); @@ -220,7 +195,7 @@ impl Mail { } }; // Build mailer and send the message - match Self::mailer(settings) { + match Self::mailer(smtp_settings).await { Ok(mailer) => match mailer.send(message).await { Ok(response) => { info!("Mail sent to: {to}, subject: {subject}, response: {response:?}"); @@ -248,24 +223,50 @@ impl Mail { } /// Builds mailer object with specified configuration. - fn mailer(settings: SmtpSettings) -> Result, MailError> { + async fn mailer( + mut smtp_settings: SmtpSettings, + ) -> Result, MailError> { type Builder = AsyncSmtpTransport; - let builder = match settings.encryption { - SmtpEncryption::None => Builder::builder_dangerous(&settings.server), - SmtpEncryption::StartTls => Builder::starttls_relay(&settings.server)?, - SmtpEncryption::ImplicitTls => Builder::relay(&settings.server)?, + let (Some(server), Some(port)) = (&smtp_settings.server, smtp_settings.port) else { + return Err(MailError::SmtpNotConfigured); + }; + + let mut builder = match smtp_settings.encryption { + SmtpEncryption::None => Builder::builder_dangerous(server), + SmtpEncryption::StartTls => Builder::starttls_relay(server)?, + SmtpEncryption::ImplicitTls => Builder::relay(server)?, } - .port(settings.port) + .port(port.try_into().map_err(|_| MailError::InvalidPort(port))?) .timeout(Some(SMTP_TIMEOUT)); - // Skip credentials if any of them is empty - let builder = if let (Some(user), Some(password)) = (settings.user, settings.password) { - builder.credentials(Credentials::new(user, password)) - } else { - debug!("SMTP credentials were not provided, skipping username/password authentication"); - builder - }; + // Skip credentials if any of them is empty. + match smtp_settings.authentication { + SmtpAuthentication::None => { + debug!( + "SMTP credentials were not provided, skipping username/password authentication" + ); + } + SmtpAuthentication::Login => { + let (Some(user), Some(password)) = (smtp_settings.user, smtp_settings.password) + else { + error!("LOGIN requires username and password"); + return Err(MailError::SmtpNotConfigured); + }; + builder = + builder.credentials(Credentials::new(user, password.expose_secret().into())); + } + SmtpAuthentication::XOAuth2 => { + let code = obtain_access_token(&mut smtp_settings).await?; + let Some(sender) = smtp_settings.sender else { + error!("XOAUTH2 requires sender email address"); + return Err(MailError::SmtpNotConfigured); + }; + builder = builder + .authentication(vec![Mechanism::Xoauth2]) + .credentials(Credentials::new(sender, code)); + } + } Ok(builder.build()) } @@ -470,10 +471,10 @@ impl MailMessage { mail.add_png_image("new_account_2", NEW_ACCOUNT_2); mail.add_png_image("google_play", GOOGLE_PLAY); mail.add_png_image("apple", APPLE); - if let Some(Value::String(url)) = context.get("url") { - if let Ok(qr) = qr_png(url.as_bytes()) { - mail.add_png_image("qr", &qr); - } + if let Some(Value::String(url)) = context.get("url") + && let Ok(qr) = qr_png(url.as_bytes()) + { + mail.add_png_image("qr", &qr); } } Self::MFACode | Self::MFAActivation => { diff --git a/crates/defguard_mail/src/templates.rs b/crates/defguard_mail/src/templates.rs index 6bf3417cc5..68c324fb1e 100644 --- a/crates/defguard_mail/src/templates.rs +++ b/crates/defguard_mail/src/templates.rs @@ -15,7 +15,7 @@ use tera::{Context, Function, Tera}; use thiserror::Error; use tracing::{debug, warn}; -use crate::{Attachment, mail::MailMessage}; +use crate::mail::{Attachment, MailMessage}; pub(crate) const DEFAULT_LANG: &str = "en_US"; diff --git a/crates/defguard_mail/src/tests.rs b/crates/defguard_mail/src/tests.rs index 41740d4cb4..7eadb4dc9f 100644 --- a/crates/defguard_mail/src/tests.rs +++ b/crates/defguard_mail/src/tests.rs @@ -6,7 +6,10 @@ use defguard_common::{ db::{ models::{ MFAMethod, Settings, - settings::{SmtpEncryption, initialize_current_settings, set_settings}, + settings::{ + initialize_current_settings, set_settings, + smtp::{SmtpAuthentication, SmtpEncryption}, + }, }, setup_pool, }, @@ -20,7 +23,10 @@ use sqlx::{ use tera::Context; use tokio::time::sleep; -use super::{Attachment, mail::MailMessage, templates}; +use super::{ + mail::{Attachment, MailMessage}, + templates, +}; #[test] fn dg25_8_server_side_template_injection() { @@ -42,13 +48,25 @@ async fn set_smtp_settings(pool: &PgPool) { initialize_current_settings(pool).await.unwrap(); let mut settings = Settings::get_current_settings(); - settings.smtp_server = env::var("SMTP_SERVER").ok(); - settings.smtp_port = Some(env::var("SMTP_PORT").map_or(587, |s| s.parse().unwrap())); - settings.smtp_encryption = SmtpEncryption::StartTls; - settings.smtp_user = env::var("SMTP_USER").ok(); - settings.smtp_password = - Some(SecretStringWrapper::from_str(&env::var("SMTP_PASSWORD").unwrap()).unwrap()); - settings.smtp_sender = env::var("SMTP_FROM").ok(); + settings.smtp.server = env::var("SMTP_SERVER").ok(); + settings.smtp.port = Some(env::var("SMTP_PORT").map_or(587, |s| s.parse().unwrap())); + + if let Ok(refresh_token) = env::var("SMTP_OAUTH_REFRESH_TOKEN") { + settings.smtp.oauth_issuer_url = env::var("SMTP_OAUTH_ISSUER_URL").ok(); + settings.smtp.oauth_client_id = env::var("SMTP_OAUTH_CLIENT_ID").ok(); + settings.smtp.oauth_client_secret = Some( + SecretStringWrapper::from_str(&env::var("SMTP_OAUTH_CLIENT_SECRET").unwrap()).unwrap(), + ); + settings.smtp.oauth_refresh_token = Some(refresh_token); + settings.smtp.authentication = SmtpAuthentication::XOAuth2; + } else { + settings.smtp.user = env::var("SMTP_USER").ok(); + settings.smtp.password = + Some(SecretStringWrapper::from_str(&env::var("SMTP_PASSWORD").unwrap()).unwrap()); + settings.smtp.encryption = SmtpEncryption::StartTls; + settings.smtp.authentication = SmtpAuthentication::Login; + } + settings.smtp.sender = env::var("SMTP_FROM").ok(); set_settings(Some(settings)); } diff --git a/crates/defguard_mail/src/xoauth2.rs b/crates/defguard_mail/src/xoauth2.rs new file mode 100644 index 0000000000..b9b7a943a6 --- /dev/null +++ b/crates/defguard_mail/src/xoauth2.rs @@ -0,0 +1,64 @@ +use defguard_common::db::models::settings::smtp::SmtpSettings; +use openidconnect::{ + ClientId, ClientSecret, IssuerUrl, OAuth2TokenResponse, RefreshToken, + core::{CoreClient, CoreProviderMetadata}, + reqwest::{ClientBuilder, redirect::Policy}, +}; +use tracing::{debug, error}; + +use super::MailError; + +/// Obtain access token for XOAUTH2 authentication. +pub(super) async fn obtain_access_token( + smtp_settings: &mut SmtpSettings, +) -> Result { + let (Some(issuer_url), Some(client_id), Some(client_secret), Some(refresh_token)) = ( + &smtp_settings.oauth_issuer_url, + &smtp_settings.oauth_client_id, + &smtp_settings.oauth_client_secret, + &smtp_settings.oauth_refresh_token, + ) else { + error!("SMTP XOAUTH requires: issuer URL, client ID, client secret, and refresh token"); + return Err(MailError::SmtpNotConfigured); + }; + let issuer_url = IssuerUrl::new(issuer_url.into())?; + let client_id = ClientId::new(client_id.into()); + let client_secret = ClientSecret::new(client_secret.expose_secret().into()); + let refresh_token = RefreshToken::new(refresh_token.into()); + + let http_client = ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(Policy::none()) + .build()?; + + let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, &http_client) + .await + .map_err(|err| { + error!("Failed OpenID Connect Discovery: {err}"); + MailError::OpenIDDiscovery + })?; + + let client = + CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)); + + let token_response = client + .exchange_refresh_token(&refresh_token)? + .request_async(&http_client) + .await + .map_err(|err| { + error!("Failed to fetch token: {err}"); + MailError::RefreshTokenExchange + })?; + + let access_token = token_response.access_token().secret(); + debug!("Got access token"); + if let Some(expires_in) = token_response.expires_in() { + debug!("Access token expires in:\n{expires_in:?}\n"); + } + if let Some(refresh_token) = token_response.refresh_token() { + debug!("Got refresh token"); + // TODO: use `smtp_settings.set_oauth_refresh_token` + smtp_settings.oauth_refresh_token = Some(refresh_token.secret().into()); + } + Ok(access_token.clone()) +} diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index 63c809b7f1..cab8d652c0 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -437,8 +437,8 @@ impl ProxyHandler { } Ok(purge) => { info!("Shutdown signal received, purge: {purge}, stopping proxy connection to {}", self.url); - if purge { - if let Some(client) = self.client.as_mut() { + if purge + && let Some(client) = self.client.as_mut() { debug!("Sending purge request to proxy {}", self.url); if let Err(err) = client.purge(Request::new(())).await { error!("Error sending purge request to proxy {}: {err}", self.url); @@ -446,7 +446,6 @@ impl ProxyHandler { info!("Sent purge request to proxy {}", self.url); } } - } } } if let Ok(mut map) = self.handler_tx_map.write() { diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index ce3fbf2d5b..0071ee70f0 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -339,13 +339,13 @@ impl EnrollmentServer { } 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", - )); - } + if let Some(ref phone_number) = request.phone_number + && !is_valid_phone_number(phone_number) + { + return Err(Status::new( + tonic::Code::InvalidArgument, + "invalid phone number", + )); } Ok(()) @@ -771,25 +771,23 @@ impl EnrollmentServer { error!("Failed to fetch WireguardNetwork with ID {location_id}: {err}"); Status::internal("unexpected error") })? - { - if let Some(firewall_config) = + && let Some(firewall_config) = try_get_location_firewall_config(&location, &mut transaction) .await .map_err(|err| { error!("Failed to get firewall config for location {location}: {err}"); Status::internal("unexpected error") })? - { - debug!( - "Sending firewall config update for location {location} affected by \ + { + debug!( + "Sending firewall config update for location {location} affected by \ adding new device {}, user {}({})", - device.wireguard_pubkey, user.username, user.id - ); - self.send_wireguard_event(GatewayEvent::FirewallConfigChanged( - location_id, - firewall_config, - )); - } + device.wireguard_pubkey, user.username, user.id + ); + self.send_wireguard_event(GatewayEvent::FirewallConfigChanged( + location_id, + firewall_config, + )); } } diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs index a1bea483e2..8f755a57c6 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs @@ -1,5 +1,7 @@ #![allow(deprecated)] -use defguard_core::db::models::enrollment::Token; +use defguard_core::{ + db::models::enrollment::Token, enterprise::handlers::openid_login::build_state, +}; use defguard_proto::{ client_types::{AuthFlowType, AuthInfoRequest, MfaMethod}, proxy::{ @@ -295,10 +297,7 @@ async fn test_mfa_oidc_full_flow(_: PgPoolOptions, options: PgConnectOptions) { // ---- Step 2: ClientMfaOidcAuthenticate ---- // Build the `state` field by encoding the mfa_token inside it. - let state = - defguard_core::enterprise::handlers::openid_login::build_state(Some(mfa_token.clone())) - .secret() - .clone(); + let state = build_state(Some(mfa_token.clone())).secret().clone(); let raw_nonce = "mfa-oidc-nonce"; let code = make_oidc_code(&user.email, &user.email, raw_nonce); diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs index 82aae8c3f5..3e14778503 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs @@ -838,9 +838,9 @@ pub(crate) async fn send_code_mfa_setup_finish( /// Set minimal SMTP fields on a [`Settings`] so that `smtp_configured()` returns `true`. pub(crate) fn configure_smtp(settings: &mut Settings) { - settings.smtp_server = Some("smtp.example.com".into()); - settings.smtp_port = Some(587); - settings.smtp_sender = Some("noreply@example.com".into()); + settings.smtp.server = Some("smtp.example.com".into()); + settings.smtp.port = Some(587); + settings.smtp.sender = Some("noreply@example.com".into()); } /// Set minimal LDAP fields on a [`Settings`] so that `ldap_configured()` returns `true`. diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 88ad9862a0..3e07f8ae31 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -31,7 +31,7 @@ pub mod events; pub mod session_state; const MESSAGE_LIMIT: usize = 100; -pub const SESSION_UPDATE_INTERVAL: Duration = Duration::from_secs(60); +pub const SESSION_UPDATE_INTERVAL: Duration = Duration::from_mins(1); pub enum IterationOutcome { ProcessedBatch(usize), diff --git a/crates/defguard_setup/src/auto_adoption.rs b/crates/defguard_setup/src/auto_adoption.rs index eea96fbb0d..22408ed030 100644 --- a/crates/defguard_setup/src/auto_adoption.rs +++ b/crates/defguard_setup/src/auto_adoption.rs @@ -885,8 +885,8 @@ async fn process_startup_auto_adoption( if status { match component { SetupAutoAdoptionComponent::Gateway => { - if let Some(result) = cert_info { - if let Err(err) = create_network_and_gateway( + if let Some(result) = cert_info + && let Err(err) = create_network_and_gateway( pool, &host, port, @@ -895,17 +895,16 @@ async fn process_startup_auto_adoption( result.core_client, ) .await - { - warn!( - "Gateway adoption TLS handshake succeeded but failed to persist \ + { + warn!( + "Gateway adoption TLS handshake succeeded but failed to persist \ network/gateway records: {err}" - ); - } + ); } } SetupAutoAdoptionComponent::Edge => { - if let Some(result) = cert_info { - if let Err(err) = create_proxy( + if let Some(result) = cert_info + && let Err(err) = create_proxy( pool, &host, port, @@ -914,12 +913,11 @@ async fn process_startup_auto_adoption( result.core_client, ) .await - { - warn!( - "Edge adoption TLS handshake succeeded but failed to persist \ + { + warn!( + "Edge adoption TLS handshake succeeded but failed to persist \ proxy record: {err}" - ); - } + ); } } } diff --git a/crates/defguard_static_ip/src/lib.rs b/crates/defguard_static_ip/src/lib.rs index e59f7b4905..ab3c06835a 100644 --- a/crates/defguard_static_ip/src/lib.rs +++ b/crates/defguard_static_ip/src/lib.rs @@ -8,7 +8,7 @@ use defguard_common::{ utils::{SplitIp, split_ip}, }; use serde::Serialize; -use sqlx::{PgConnection, PgPool, prelude::FromRow}; +use sqlx::{PgConnection, PgPool, prelude::FromRow, query_as}; use tracing::debug; use crate::error::StaticIpError; @@ -53,7 +53,7 @@ pub async fn get_ips_for_user( pool: &PgPool, ) -> Result, StaticIpError> { debug!("Fetching static IPs for user {username}"); - let rows = sqlx::query_as!( + let rows = query_as!( DeviceIpRow, "SELECT \ wn.id AS location_id, \ @@ -126,7 +126,7 @@ pub async fn get_ips_for_device( pool: &PgPool, ) -> Result, StaticIpError> { debug!("Fetching static IPs for device {device_id} of user {username}"); - let rows = sqlx::query_as!( + let rows = query_as!( DeviceIpRow, "SELECT \ wn.id AS location_id, \ diff --git a/crates/defguard_version/src/lib.rs b/crates/defguard_version/src/lib.rs index 615f9e2995..eb8af26c53 100644 --- a/crates/defguard_version/src/lib.rs +++ b/crates/defguard_version/src/lib.rs @@ -170,7 +170,7 @@ impl SystemInfo { /// A `SystemInfo` struct populated with the current system's characteristics. #[must_use] pub fn get() -> Self { - os_info::get().into() + Self::from(os_info::get()) } fn as_header_value(&self) -> String { @@ -178,7 +178,7 @@ impl SystemInfo { } fn try_from_header_value(header_value: &str) -> Result { - let parts: Vec<&str> = header_value.split(';').collect(); + let parts = header_value.split(';').collect::>(); if parts.len() != 3 { return Err(DefguardVersionError::SystemInfoParseError( header_value.to_string(), diff --git a/crates/defguard_version/src/tracing.rs b/crates/defguard_version/src/tracing.rs index cb08919a2a..8d064237d7 100644 --- a/crates/defguard_version/src/tracing.rs +++ b/crates/defguard_version/src/tracing.rs @@ -183,11 +183,9 @@ pub fn build_version_suffix( DefguardComponent::Gateway => version_suffix.push_str("[GW:"), } version_suffix.push_str(version); - if is_error { - if let Some(ref info) = extracted.info { - version_suffix.push(' '); - version_suffix.push_str(info); - } + if is_error && let Some(ref info) = extracted.info { + version_suffix.push(' '); + version_suffix.push_str(info); } version_suffix.push(']'); } diff --git a/crates/defguard_vpn_stats_purge/src/lib.rs b/crates/defguard_vpn_stats_purge/src/lib.rs index cd4af73485..8a105d7b2f 100644 --- a/crates/defguard_vpn_stats_purge/src/lib.rs +++ b/crates/defguard_vpn_stats_purge/src/lib.rs @@ -7,7 +7,7 @@ use tokio::time::sleep; use tracing::{debug, error, info, instrument}; // How long to sleep between loop iterations -const PURGE_LOOP_SLEEP: Duration = Duration::from_secs(300); // 5 minutes +const PURGE_LOOP_SLEEP: Duration = Duration::from_mins(5); // 5 minutes #[instrument(skip_all)] pub async fn run_periodic_stats_purge( diff --git a/crates/model_derive/src/lib.rs b/crates/model_derive/src/lib.rs index 5d1584110c..ca10d10164 100644 --- a/crates/model_derive/src/lib.rs +++ b/crates/model_derive/src/lib.rs @@ -75,10 +75,9 @@ fn field_type(ty: &Type) -> Option<&Ident> { path: Path { segments, .. }, .. }) = ty + && let Some(segment) = segments.last() { - if let Some(segment) = segments.last() { - return Some(&segment.ident); - } + return Some(&segment.ident); } None } @@ -88,16 +87,14 @@ fn option_field_type(ty: &Type) -> Option<&Ident> { path: Path { segments, .. }, .. }) = ty + && let Some(segment) = segments.last() + && segment.ident == "Option" { - if let Some(segment) = segments.last() { - if segment.ident == "Option" { - // Extract the generic arguments - if let PathArguments::AngleBracketed(args) = &segment.arguments { - // Get the first generic argument (the T in Option) - if let Some(GenericArgument::Type(inner_ty)) = args.args.first() { - return field_type(inner_ty); - } - } + // Extract the generic arguments + if let PathArguments::AngleBracketed(args) = &segment.arguments { + // Get the first generic argument (the T in Option) + if let Some(GenericArgument::Type(inner_ty)) = args.args.first() { + return field_type(inner_ty); } } } diff --git a/migrations/20260601090254_[2.0.2]_smtp_xoauth2.down.sql b/migrations/20260601090254_[2.0.2]_smtp_xoauth2.down.sql new file mode 100644 index 0000000000..31452abf33 --- /dev/null +++ b/migrations/20260601090254_[2.0.2]_smtp_xoauth2.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE settings + DROP smtp_authentication, + DROP smtp_oauth_issuer_url, + DROP smtp_oauth_client_id, + DROP smtp_oauth_client_secret, + DROP smtp_oauth_refresh_token; +DROP TYPE smtp_authentication; diff --git a/migrations/20260601090254_[2.0.2]_smtp_xoauth2.up.sql b/migrations/20260601090254_[2.0.2]_smtp_xoauth2.up.sql new file mode 100644 index 0000000000..a8da4e83a4 --- /dev/null +++ b/migrations/20260601090254_[2.0.2]_smtp_xoauth2.up.sql @@ -0,0 +1,12 @@ +CREATE TYPE smtp_authentication AS ENUM ( + 'none', + 'login', + 'xoauth2' +); +ALTER TABLE settings + ADD smtp_authentication smtp_authentication NOT NULL DEFAULT 'none', + ADD smtp_oauth_issuer_url text NULL, + ADD smtp_oauth_client_id text NULL, + ADD smtp_oauth_client_secret text NULL, + ADD smtp_oauth_refresh_token text NULL; +UPDATE settings SET smtp_authentication = 'login' WHERE smtp_user IS NOT NULL AND smtp_password IS NOT NULL;