From cb36ee5415a021da7b8c3bc2019a833bbb172c85 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 26 Aug 2023 23:21:26 +0200 Subject: [PATCH 1/3] enforce 2fa policy on removal of second factor users should be revoked when their second factors are removed. we want to revoke users so they don't have to be invited again and organization admins and owners are aware that they no longer have access. we make an exception for non-confirmed users to speed up the invitation process as they would have to be restored before they can accept their invitation or be confirmed. if email is enabled, invited users have to add a second factor before they can accept the invitation to an organization with 2fa policy. and if it is not enabled that check is done when confirming the user. --- src/api/admin.rs | 8 ++- src/api/core/organizations.rs | 42 ++++----------- src/api/core/two_factor/mod.rs | 96 ++++++++++++++++++++++++++++------ src/db/models/organization.rs | 10 ++++ 4 files changed, 105 insertions(+), 51 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index ae30425388..cdbad6386b 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -13,7 +13,10 @@ use rocket::{ }; use crate::{ - api::{core::log_event, unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify, NumberOrString}, + api::{ + core::{log_event, two_factor}, + unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify, NumberOrString, + }, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, config::ConfigBuilder, db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, @@ -445,9 +448,10 @@ async fn enable_user(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyR } #[post("/users//remove-2fa")] -async fn remove_2fa(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult { +async fn remove_2fa(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult { let mut user = get_user_or_404(uuid, &mut conn).await?; TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; + two_factor::enforce_2fa_policy(&user, String::from(ACTING_ADMIN_USER), 14, &token.ip.ip, &mut conn).await?; user.totp_recover = None; user.save(&mut conn).await } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 59079e0121..935cd07f88 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -5,7 +5,7 @@ use serde_json::Value; use crate::{ api::{ - core::{log_event, CipherSyncData, CipherSyncType}, + core::{log_event, two_factor, CipherSyncData, CipherSyncType}, EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordOrOtpData, UpdateType, }, @@ -1697,38 +1697,16 @@ async fn put_policy( None => err!("Invalid or unsupported policy type"), }; - // When enabling the TwoFactorAuthentication policy, remove this org's members that do have 2FA + // When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { - for member in UserOrganization::find_by_org(org_id, &mut conn).await.into_iter() { - let user_twofactor_disabled = TwoFactor::find_by_user(&member.user_uuid, &mut conn).await.is_empty(); - - // Policy only applies to non-Owner/non-Admin members who have accepted joining the org - // Invited users still need to accept the invite and will get an error when they try to accept the invite. - if user_twofactor_disabled - && member.atype < UserOrgType::Admin - && member.status != UserOrgStatus::Invited as i32 - { - if CONFIG.mail_enabled() { - let org = Organization::find_by_uuid(&member.org_uuid, &mut conn).await.unwrap(); - let user = User::find_by_uuid(&member.user_uuid, &mut conn).await.unwrap(); - - mail::send_2fa_removed_from_org(&user.email, &org.name).await?; - } - - log_event( - EventType::OrganizationUserRemoved as i32, - &member.uuid, - org_id, - headers.user.uuid.clone(), - headers.device.atype, - &headers.ip.ip, - &mut conn, - ) - .await; - - member.delete(&mut conn).await?; - } - } + two_factor::enforce_2fa_policy_for_org( + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &headers.ip.ip, + &mut conn, + ) + .await?; } // When enabling the SingleOrg policy, remove this org's members that are members of other orgs diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 41368666de..18a4a20f1b 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -5,7 +5,10 @@ use rocket::Route; use serde_json::Value; use crate::{ - api::{core::log_user_event, JsonResult, JsonUpcase, NumberOrString, PasswordOrOtpData}, + api::{ + core::{log_event, log_user_event}, + EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordOrOtpData, + }, auth::{ClientHeaders, Headers}, crypto, db::{models::*, DbConn, DbPool}, @@ -96,6 +99,7 @@ async fn recover(data: JsonUpcase, client_headers: ClientHeade // Remove all twofactors from the user TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; + enforce_2fa_policy(&user, user.uuid.clone(), client_headers.device_type, &client_headers.ip.ip, &mut conn).await?; log_user_event( EventType::UserRecovered2fa as i32, @@ -149,22 +153,8 @@ async fn disable_twofactor(data: JsonUpcase, headers: Head .await; } - let twofactor_disabled = TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty(); - - if twofactor_disabled { - for user_org in - UserOrganization::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, &mut conn) - .await - .into_iter() - { - if user_org.atype < UserOrgType::Admin { - if CONFIG.mail_enabled() { - let org = Organization::find_by_uuid(&user_org.org_uuid, &mut conn).await.unwrap(); - mail::send_2fa_removed_from_org(&user.email, &org.name).await?; - } - user_org.delete(&mut conn).await?; - } - } + if TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty() { + enforce_2fa_policy(&user, user.uuid.clone(), headers.device.atype, &headers.ip.ip, &mut conn).await?; } Ok(Json(json!({ @@ -179,6 +169,78 @@ async fn disable_twofactor_put(data: JsonUpcase, headers: disable_twofactor(data, headers, conn).await } +pub async fn enforce_2fa_policy( + user: &User, + act_uuid: String, + device_type: i32, + ip: &std::net::IpAddr, + conn: &mut DbConn, +) -> EmptyResult { + for member in UserOrganization::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn) + .await + .into_iter() + { + // Policy only applies to non-Owner/non-Admin members who have accepted joining the org + if member.atype < UserOrgType::Admin { + if CONFIG.mail_enabled() { + let org = Organization::find_by_uuid(&member.org_uuid, conn).await.unwrap(); + mail::send_2fa_removed_from_org(&user.email, &org.name).await?; + } + let mut member = member; + member.revoke(); + member.save(conn).await?; + + log_event( + EventType::OrganizationUserRevoked as i32, + &member.uuid, + &member.org_uuid, + act_uuid.clone(), + device_type, + ip, + conn, + ) + .await; + } + } + + Ok(()) +} + +pub async fn enforce_2fa_policy_for_org( + org_uuid: &str, + act_uuid: String, + device_type: i32, + ip: &std::net::IpAddr, + conn: &mut DbConn, +) -> EmptyResult { + let org = Organization::find_by_uuid(org_uuid, conn).await.unwrap(); + for member in UserOrganization::find_confirmed_by_org(org_uuid, conn).await.into_iter() { + // Don't enforce the policy for Admins and Owners. + if member.atype < UserOrgType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() { + if CONFIG.mail_enabled() { + let user = User::find_by_uuid(&member.user_uuid, conn).await.unwrap(); + mail::send_2fa_removed_from_org(&user.email, &org.name).await?; + } + let mut member = member; + member.revoke(); + member.save(conn).await?; + + log_event( + EventType::OrganizationUserRevoked as i32, + &member.uuid, + org_uuid, + act_uuid.clone(), + device_type, + ip, + conn, + ) + .await; + } + } + + Ok(()) +} + pub async fn send_incomplete_2fa_notifications(pool: DbPool) { debug!("Sending notifications for incomplete 2FA logins"); diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 620d7428e1..144ea56741 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -665,6 +665,16 @@ impl UserOrganization { }} } + pub async fn find_confirmed_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec { + db_run! { conn: { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .load::(conn) + .unwrap_or_default().from_db() + }} + } + pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { db_run! { conn: { users_organizations::table From d5847e60a85ee8c59e938a39d6c3dd8a41c1a5ac Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Mon, 28 Aug 2023 20:48:08 +0200 Subject: [PATCH 2/3] use &str instead of String in log_event() --- src/api/admin.rs | 6 ++-- src/api/core/ciphers.rs | 24 +++++---------- src/api/core/events.rs | 4 +-- src/api/core/organizations.rs | 53 +++++++++++++++++----------------- src/api/core/two_factor/mod.rs | 12 ++++---- 5 files changed, 45 insertions(+), 54 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index cdbad6386b..a10df8918a 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -393,7 +393,7 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe EventType::OrganizationUserRemoved as i32, &user_org.uuid, &user_org.org_uuid, - String::from(ACTING_ADMIN_USER), + ACTING_ADMIN_USER, 14, // Use UnknownBrowser type &token.ip.ip, &mut conn, @@ -451,7 +451,7 @@ async fn enable_user(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyR async fn remove_2fa(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult { let mut user = get_user_or_404(uuid, &mut conn).await?; TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; - two_factor::enforce_2fa_policy(&user, String::from(ACTING_ADMIN_USER), 14, &token.ip.ip, &mut conn).await?; + two_factor::enforce_2fa_policy(&user, ACTING_ADMIN_USER, 14, &token.ip.ip, &mut conn).await?; user.totp_recover = None; user.save(&mut conn).await } @@ -521,7 +521,7 @@ async fn update_user_org_type(data: Json, token: AdminToken, mu EventType::OrganizationUserUpdated as i32, &user_to_edit.uuid, &data.org_uuid, - String::from(ACTING_ADMIN_USER), + ACTING_ADMIN_USER, 14, // Use UnknownBrowser type &token.ip.ip, &mut conn, diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 0410d68ed4..a4a2d8365b 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -510,7 +510,7 @@ pub async fn update_cipher_from_data( event_type as i32, &cipher.uuid, org_uuid, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -791,7 +791,7 @@ async fn post_collections_admin( EventType::CipherUpdatedCollections as i32, &cipher.uuid, &cipher.organization_uuid.unwrap(), - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1145,7 +1145,7 @@ async fn save_attachment( EventType::CipherAttachmentCreated as i32, &cipher.uuid, org_uuid, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1479,7 +1479,7 @@ async fn delete_all( EventType::OrganizationPurgedVault as i32, &org_data.org_id, &org_data.org_id, - user.uuid, + &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1560,16 +1560,8 @@ async fn _delete_cipher_by_uuid( false => EventType::CipherDeleted as i32, }; - log_event( - event_type, - &cipher.uuid, - &org_uuid, - headers.user.uuid.clone(), - headers.device.atype, - &headers.ip.ip, - conn, - ) - .await; + log_event(event_type, &cipher.uuid, &org_uuid, &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn) + .await; } Ok(()) @@ -1629,7 +1621,7 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon EventType::CipherRestored as i32, &cipher.uuid.clone(), org_uuid, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -1713,7 +1705,7 @@ async fn _delete_cipher_attachment_by_id( EventType::CipherAttachmentDeleted as i32, &cipher.uuid, &org_uuid, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 70704d792d..9a2027e0e9 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -263,7 +263,7 @@ pub async fn log_event( event_type: i32, source_uuid: &str, org_uuid: &str, - act_user_uuid: String, + act_user_uuid: &str, device_type: i32, ip: &IpAddr, conn: &mut DbConn, @@ -271,7 +271,7 @@ pub async fn log_event( if !CONFIG.org_events_enabled() { return; } - _log_event(event_type, source_uuid, org_uuid, &act_user_uuid, device_type, None, ip, conn).await; + _log_event(event_type, source_uuid, org_uuid, act_user_uuid, device_type, None, ip, conn).await; } #[allow(clippy::too_many_arguments)] diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 935cd07f88..f57d85061c 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -226,7 +226,7 @@ async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) -> EventType::OrganizationUserRemoved as i32, &user_org.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -279,7 +279,7 @@ async fn post_organization( EventType::OrganizationUpdated as i32, org_id, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -396,7 +396,7 @@ async fn post_organization_collections( EventType::CollectionCreated as i32, &collection.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -477,7 +477,7 @@ async fn post_organization_collection_update( EventType::CollectionUpdated as i32, &collection.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -565,7 +565,7 @@ async fn _delete_organization_collection( EventType::CollectionDeleted as i32, &collection.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -946,7 +946,7 @@ async fn send_invite( EventType::OrganizationUserInvited as i32, &new_user.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1240,7 +1240,7 @@ async fn _confirm_invite( EventType::OrganizationUserConfirmed as i32, &user_to_confirm.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -1402,7 +1402,7 @@ async fn edit_user( EventType::OrganizationUserUpdated as i32, &user_to_edit.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1494,7 +1494,7 @@ async fn _delete_user( EventType::OrganizationUserRemoved as i32, &user_to_delete.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -1701,7 +1701,7 @@ async fn put_policy( if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { two_factor::enforce_2fa_policy_for_org( org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1731,7 +1731,7 @@ async fn put_policy( EventType::OrganizationUserRemoved as i32, &member.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1756,7 +1756,7 @@ async fn put_policy( EventType::PolicyUpdated as i32, &policy.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1873,7 +1873,7 @@ async fn import(org_id: &str, data: JsonUpcase, headers: Headers, EventType::OrganizationUserRemoved as i32, &user_org.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1903,7 +1903,7 @@ async fn import(org_id: &str, data: JsonUpcase, headers: Headers, EventType::OrganizationUserInvited as i32, &new_org_user.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -1939,7 +1939,7 @@ async fn import(org_id: &str, data: JsonUpcase, headers: Headers, EventType::OrganizationUserRemoved as i32, &user_org.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -2052,7 +2052,7 @@ async fn _revoke_organization_user( EventType::OrganizationUserRevoked as i32, &user_org.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -2171,7 +2171,7 @@ async fn _restore_organization_user( EventType::OrganizationUserRestored as i32, &user_org.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -2300,7 +2300,7 @@ async fn post_groups( EventType::GroupCreated as i32, &group.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -2337,7 +2337,7 @@ async fn put_group( EventType::GroupUpdated as i32, &updated_group.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -2370,7 +2370,7 @@ async fn add_update_group( EventType::OrganizationUserUpdatedGroups as i32, &assigned_user_id, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -2425,7 +2425,7 @@ async fn _delete_group(org_id: &str, group_id: &str, headers: &AdminHeaders, con EventType::GroupDeleted as i32, &group.uuid, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn, @@ -2516,7 +2516,7 @@ async fn put_group_users( EventType::OrganizationUserUpdatedGroups as i32, &assigned_user_id, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -2594,7 +2594,7 @@ async fn put_user_groups( EventType::OrganizationUserUpdatedGroups as i32, org_user_id, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -2649,7 +2649,7 @@ async fn delete_group_user( EventType::OrganizationUserUpdatedGroups as i32, org_user_id, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -2738,7 +2738,7 @@ async fn put_reset_password( EventType::OrganizationUserAdminResetPassword as i32, org_user_id, org_id, - headers.user.uuid.clone(), + &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn, @@ -2865,8 +2865,7 @@ async fn put_reset_password_enrollment( EventType::OrganizationUserResetPasswordWithdraw as i32 }; - log_event(log_id, org_user_id, org_id, headers.user.uuid.clone(), headers.device.atype, &headers.ip.ip, &mut conn) - .await; + log_event(log_id, org_user_id, org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; Ok(()) } diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 18a4a20f1b..9a922d26f6 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -99,7 +99,7 @@ async fn recover(data: JsonUpcase, client_headers: ClientHeade // Remove all twofactors from the user TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; - enforce_2fa_policy(&user, user.uuid.clone(), client_headers.device_type, &client_headers.ip.ip, &mut conn).await?; + enforce_2fa_policy(&user, &user.uuid, client_headers.device_type, &client_headers.ip.ip, &mut conn).await?; log_user_event( EventType::UserRecovered2fa as i32, @@ -154,7 +154,7 @@ async fn disable_twofactor(data: JsonUpcase, headers: Head } if TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty() { - enforce_2fa_policy(&user, user.uuid.clone(), headers.device.atype, &headers.ip.ip, &mut conn).await?; + enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await?; } Ok(Json(json!({ @@ -171,7 +171,7 @@ async fn disable_twofactor_put(data: JsonUpcase, headers: pub async fn enforce_2fa_policy( user: &User, - act_uuid: String, + act_uuid: &str, device_type: i32, ip: &std::net::IpAddr, conn: &mut DbConn, @@ -194,7 +194,7 @@ pub async fn enforce_2fa_policy( EventType::OrganizationUserRevoked as i32, &member.uuid, &member.org_uuid, - act_uuid.clone(), + act_uuid, device_type, ip, conn, @@ -208,7 +208,7 @@ pub async fn enforce_2fa_policy( pub async fn enforce_2fa_policy_for_org( org_uuid: &str, - act_uuid: String, + act_uuid: &str, device_type: i32, ip: &std::net::IpAddr, conn: &mut DbConn, @@ -229,7 +229,7 @@ pub async fn enforce_2fa_policy_for_org( EventType::OrganizationUserRevoked as i32, &member.uuid, org_uuid, - act_uuid.clone(), + act_uuid, device_type, ip, conn, From 8a8b0c727d8c6325100abf11f7cb49683424079f Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Mon, 28 Aug 2023 22:59:40 +0200 Subject: [PATCH 3/3] enforce the 2fa policy on login if a user doesn't have a second factor check if they are in an organization that has the 2fa policy enabled to revoke their access --- src/api/identity.rs | 46 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 13e7ab433f..12becf6b1c 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -9,9 +9,11 @@ use serde_json::Value; use crate::{ api::{ - core::accounts::{PreloginData, RegisterData, _prelogin, _register}, - core::log_user_event, - core::two_factor::{duo, email, email::EmailTokenData, yubikey}, + core::{ + accounts::{PreloginData, RegisterData, _prelogin, _register}, + log_user_event, + two_factor::{authenticator, duo, email, enforce_2fa_policy, webauthn, yubikey}, + }, ApiResult, EmptyResult, JsonResult, JsonUpcase, }, auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, @@ -247,7 +249,7 @@ async fn _password_login( let (mut device, new_device) = get_device(&data, conn, &user).await; - let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, conn).await?; + let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?; if CONFIG.mail_enabled() && new_device { if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await { @@ -468,32 +470,32 @@ async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> (Devi } async fn twofactor_auth( - user_uuid: &str, + user: &User, data: &ConnectData, device: &mut Device, ip: &ClientIp, conn: &mut DbConn, ) -> ApiResult> { - let twofactors = TwoFactor::find_by_user(user_uuid, conn).await; + let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; // No twofactor token if twofactor is disabled if twofactors.is_empty() { + enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?; return Ok(None); } - TwoFactorIncomplete::mark_incomplete(user_uuid, &device.uuid, &device.name, ip, conn).await?; + TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, ip, conn).await?; let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect(); let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one let twofactor_code = match data.two_factor_token { Some(ref code) => code, - None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn).await?, "2FA token not provided"), + None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?, "2FA token not provided"), }; let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled); - use crate::api::core::two_factor as _tf; use crate::crypto::ct_eq; let selected_data = _selected_data(selected_twofactor); @@ -501,17 +503,15 @@ async fn twofactor_auth( match TwoFactorType::from_i32(selected_id) { Some(TwoFactorType::Authenticator) => { - _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn).await? - } - Some(TwoFactorType::Webauthn) => { - _tf::webauthn::validate_webauthn_login(user_uuid, twofactor_code, conn).await? + authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await? } - Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, + Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?, + Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::Duo) => { - _tf::duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await? + duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await? } Some(TwoFactorType::Email) => { - _tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn).await? + email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, conn).await? } Some(TwoFactorType::Remember) => { @@ -521,7 +521,7 @@ async fn twofactor_auth( } _ => { err_json!( - _json_err_twofactor(&twofactor_ids, user_uuid, conn).await?, + _json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?, "2FA Remember token not provided" ) } @@ -535,7 +535,7 @@ async fn twofactor_auth( ), } - TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn).await?; + TwoFactorIncomplete::mark_complete(&user.uuid, &device.uuid, conn).await?; if !CONFIG.disable_2fa_remember() && remember == 1 { Ok(Some(device.refresh_twofactor_remember())) @@ -550,8 +550,6 @@ fn _selected_data(tf: Option) -> ApiResult { } async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbConn) -> ApiResult { - use crate::api::core::two_factor; - let mut result = json!({ "error" : "invalid_grant", "error_description" : "Two factor required.", @@ -566,7 +564,7 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { - let request = two_factor::webauthn::generate_webauthn_login(user_uuid, conn).await?; + let request = webauthn::generate_webauthn_login(user_uuid, conn).await?; result["TwoFactorProviders2"][provider.to_string()] = request.0; } @@ -598,8 +596,6 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo } Some(tf_type @ TwoFactorType::Email) => { - use crate::api::core::two_factor as _tf; - let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await { Some(tf) => tf, None => err!("No twofactor email registered"), @@ -607,10 +603,10 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo // Send email immediately if email is the only 2FA option if providers.len() == 1 { - _tf::email::send_token(user_uuid, conn).await? + email::send_token(user_uuid, conn).await? } - let email_data = EmailTokenData::from_json(&twofactor.data)?; + let email_data = email::EmailTokenData::from_json(&twofactor.data)?; result["TwoFactorProviders2"][provider.to_string()] = json!({ "Email": email::obscure_email(&email_data.email), })