mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-13 03:55:58 +03:00
enforce 2FA policy on removal of second factor and login (#3803)
* 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. * use &str instead of String in log_event() * 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
This commit is contained in:
@@ -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,
|
||||
|
@@ -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)]
|
||||
|
@@ -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,
|
||||
},
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
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
|
||||
@@ -1753,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,
|
||||
@@ -1778,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,
|
||||
@@ -1895,7 +1873,7 @@ async fn import(org_id: &str, data: JsonUpcase<OrgImportData>, 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,
|
||||
@@ -1925,7 +1903,7 @@ async fn import(org_id: &str, data: JsonUpcase<OrgImportData>, 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,
|
||||
@@ -1961,7 +1939,7 @@ async fn import(org_id: &str, data: JsonUpcase<OrgImportData>, 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,
|
||||
@@ -2074,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,
|
||||
@@ -2193,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,
|
||||
@@ -2322,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,
|
||||
@@ -2359,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,
|
||||
@@ -2392,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,
|
||||
@@ -2447,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,
|
||||
@@ -2538,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,
|
||||
@@ -2616,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,
|
||||
@@ -2671,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,
|
||||
@@ -2760,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,
|
||||
@@ -2887,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(())
|
||||
}
|
||||
|
@@ -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<RecoverTwoFactor>, 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, 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<DisableTwoFactorData>, 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, headers.device.atype, &headers.ip.ip, &mut conn).await?;
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
@@ -179,6 +169,78 @@ async fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers:
|
||||
disable_twofactor(data, headers, conn).await
|
||||
}
|
||||
|
||||
pub async fn enforce_2fa_policy(
|
||||
user: &User,
|
||||
act_uuid: &str,
|
||||
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,
|
||||
device_type,
|
||||
ip,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn enforce_2fa_policy_for_org(
|
||||
org_uuid: &str,
|
||||
act_uuid: &str,
|
||||
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,
|
||||
device_type,
|
||||
ip,
|
||||
conn,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||
debug!("Sending notifications for incomplete 2FA logins");
|
||||
|
||||
|
Reference in New Issue
Block a user