1
0
Fork 1
Spiegel von https://github.com/dani-garcia/vaultwarden.git synchronisiert 2024-11-22 05:10:29 +01: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
Dieser Commit ist enthalten in:
Stefan Melmuk 2024-01-01 19:41:40 +01:00 committet von GitHub
Ursprung d672ad3f76
Commit 2c36993792
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
7 geänderte Dateien mit 163 neuen und 122 gelöschten Zeilen

Datei anzeigen

@ -13,7 +13,10 @@ use rocket::{
}; };
use crate::{ 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}, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
config::ConfigBuilder, config::ConfigBuilder,
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
@ -390,7 +393,7 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe
EventType::OrganizationUserRemoved as i32, EventType::OrganizationUserRemoved as i32,
&user_org.uuid, &user_org.uuid,
&user_org.org_uuid, &user_org.org_uuid,
String::from(ACTING_ADMIN_USER), ACTING_ADMIN_USER,
14, // Use UnknownBrowser type 14, // Use UnknownBrowser type
&token.ip.ip, &token.ip.ip,
&mut conn, &mut conn,
@ -445,9 +448,10 @@ async fn enable_user(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyR
} }
#[post("/users/<uuid>/remove-2fa")] #[post("/users/<uuid>/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?; let mut user = get_user_or_404(uuid, &mut conn).await?;
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
two_factor::enforce_2fa_policy(&user, ACTING_ADMIN_USER, 14, &token.ip.ip, &mut conn).await?;
user.totp_recover = None; user.totp_recover = None;
user.save(&mut conn).await user.save(&mut conn).await
} }
@ -517,7 +521,7 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mu
EventType::OrganizationUserUpdated as i32, EventType::OrganizationUserUpdated as i32,
&user_to_edit.uuid, &user_to_edit.uuid,
&data.org_uuid, &data.org_uuid,
String::from(ACTING_ADMIN_USER), ACTING_ADMIN_USER,
14, // Use UnknownBrowser type 14, // Use UnknownBrowser type
&token.ip.ip, &token.ip.ip,
&mut conn, &mut conn,

Datei anzeigen

@ -510,7 +510,7 @@ pub async fn update_cipher_from_data(
event_type as i32, event_type as i32,
&cipher.uuid, &cipher.uuid,
org_uuid, org_uuid,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -791,7 +791,7 @@ async fn post_collections_admin(
EventType::CipherUpdatedCollections as i32, EventType::CipherUpdatedCollections as i32,
&cipher.uuid, &cipher.uuid,
&cipher.organization_uuid.unwrap(), &cipher.organization_uuid.unwrap(),
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1145,7 +1145,7 @@ async fn save_attachment(
EventType::CipherAttachmentCreated as i32, EventType::CipherAttachmentCreated as i32,
&cipher.uuid, &cipher.uuid,
org_uuid, org_uuid,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1479,7 +1479,7 @@ async fn delete_all(
EventType::OrganizationPurgedVault as i32, EventType::OrganizationPurgedVault as i32,
&org_data.org_id, &org_data.org_id,
&org_data.org_id, &org_data.org_id,
user.uuid, &user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1560,15 +1560,7 @@ async fn _delete_cipher_by_uuid(
false => EventType::CipherDeleted as i32, false => EventType::CipherDeleted as i32,
}; };
log_event( log_event(event_type, &cipher.uuid, &org_uuid, &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn)
event_type,
&cipher.uuid,
&org_uuid,
headers.user.uuid.clone(),
headers.device.atype,
&headers.ip.ip,
conn,
)
.await; .await;
} }
@ -1629,7 +1621,7 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon
EventType::CipherRestored as i32, EventType::CipherRestored as i32,
&cipher.uuid.clone(), &cipher.uuid.clone(),
org_uuid, org_uuid,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -1713,7 +1705,7 @@ async fn _delete_cipher_attachment_by_id(
EventType::CipherAttachmentDeleted as i32, EventType::CipherAttachmentDeleted as i32,
&cipher.uuid, &cipher.uuid,
&org_uuid, &org_uuid,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,

Datei anzeigen

@ -263,7 +263,7 @@ pub async fn log_event(
event_type: i32, event_type: i32,
source_uuid: &str, source_uuid: &str,
org_uuid: &str, org_uuid: &str,
act_user_uuid: String, act_user_uuid: &str,
device_type: i32, device_type: i32,
ip: &IpAddr, ip: &IpAddr,
conn: &mut DbConn, conn: &mut DbConn,
@ -271,7 +271,7 @@ pub async fn log_event(
if !CONFIG.org_events_enabled() { if !CONFIG.org_events_enabled() {
return; 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)] #[allow(clippy::too_many_arguments)]

Datei anzeigen

@ -5,7 +5,7 @@ use serde_json::Value;
use crate::{ use crate::{
api::{ api::{
core::{log_event, CipherSyncData, CipherSyncType}, core::{log_event, two_factor, CipherSyncData, CipherSyncType},
EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordOrOtpData, EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordOrOtpData,
UpdateType, UpdateType,
}, },
@ -226,7 +226,7 @@ async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) ->
EventType::OrganizationUserRemoved as i32, EventType::OrganizationUserRemoved as i32,
&user_org.uuid, &user_org.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -279,7 +279,7 @@ async fn post_organization(
EventType::OrganizationUpdated as i32, EventType::OrganizationUpdated as i32,
org_id, org_id,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -396,7 +396,7 @@ async fn post_organization_collections(
EventType::CollectionCreated as i32, EventType::CollectionCreated as i32,
&collection.uuid, &collection.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -477,7 +477,7 @@ async fn post_organization_collection_update(
EventType::CollectionUpdated as i32, EventType::CollectionUpdated as i32,
&collection.uuid, &collection.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -565,7 +565,7 @@ async fn _delete_organization_collection(
EventType::CollectionDeleted as i32, EventType::CollectionDeleted as i32,
&collection.uuid, &collection.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -946,7 +946,7 @@ async fn send_invite(
EventType::OrganizationUserInvited as i32, EventType::OrganizationUserInvited as i32,
&new_user.uuid, &new_user.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1240,7 +1240,7 @@ async fn _confirm_invite(
EventType::OrganizationUserConfirmed as i32, EventType::OrganizationUserConfirmed as i32,
&user_to_confirm.uuid, &user_to_confirm.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -1402,7 +1402,7 @@ async fn edit_user(
EventType::OrganizationUserUpdated as i32, EventType::OrganizationUserUpdated as i32,
&user_to_edit.uuid, &user_to_edit.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1494,7 +1494,7 @@ async fn _delete_user(
EventType::OrganizationUserRemoved as i32, EventType::OrganizationUserRemoved as i32,
&user_to_delete.uuid, &user_to_delete.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -1697,38 +1697,16 @@ async fn put_policy(
None => err!("Invalid or unsupported policy type"), 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 { if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled {
for member in UserOrganization::find_by_org(org_id, &mut conn).await.into_iter() { two_factor::enforce_2fa_policy_for_org(
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, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
) )
.await; .await?;
member.delete(&mut conn).await?;
}
}
} }
// When enabling the SingleOrg policy, remove this org's members that are members of other orgs // 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, EventType::OrganizationUserRemoved as i32,
&member.uuid, &member.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1778,7 +1756,7 @@ async fn put_policy(
EventType::PolicyUpdated as i32, EventType::PolicyUpdated as i32,
&policy.uuid, &policy.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1895,7 +1873,7 @@ async fn import(org_id: &str, data: JsonUpcase<OrgImportData>, headers: Headers,
EventType::OrganizationUserRemoved as i32, EventType::OrganizationUserRemoved as i32,
&user_org.uuid, &user_org.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1925,7 +1903,7 @@ async fn import(org_id: &str, data: JsonUpcase<OrgImportData>, headers: Headers,
EventType::OrganizationUserInvited as i32, EventType::OrganizationUserInvited as i32,
&new_org_user.uuid, &new_org_user.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -1961,7 +1939,7 @@ async fn import(org_id: &str, data: JsonUpcase<OrgImportData>, headers: Headers,
EventType::OrganizationUserRemoved as i32, EventType::OrganizationUserRemoved as i32,
&user_org.uuid, &user_org.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -2074,7 +2052,7 @@ async fn _revoke_organization_user(
EventType::OrganizationUserRevoked as i32, EventType::OrganizationUserRevoked as i32,
&user_org.uuid, &user_org.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -2193,7 +2171,7 @@ async fn _restore_organization_user(
EventType::OrganizationUserRestored as i32, EventType::OrganizationUserRestored as i32,
&user_org.uuid, &user_org.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -2322,7 +2300,7 @@ async fn post_groups(
EventType::GroupCreated as i32, EventType::GroupCreated as i32,
&group.uuid, &group.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -2359,7 +2337,7 @@ async fn put_group(
EventType::GroupUpdated as i32, EventType::GroupUpdated as i32,
&updated_group.uuid, &updated_group.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -2392,7 +2370,7 @@ async fn add_update_group(
EventType::OrganizationUserUpdatedGroups as i32, EventType::OrganizationUserUpdatedGroups as i32,
&assigned_user_id, &assigned_user_id,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -2447,7 +2425,7 @@ async fn _delete_group(org_id: &str, group_id: &str, headers: &AdminHeaders, con
EventType::GroupDeleted as i32, EventType::GroupDeleted as i32,
&group.uuid, &group.uuid,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
conn, conn,
@ -2538,7 +2516,7 @@ async fn put_group_users(
EventType::OrganizationUserUpdatedGroups as i32, EventType::OrganizationUserUpdatedGroups as i32,
&assigned_user_id, &assigned_user_id,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -2616,7 +2594,7 @@ async fn put_user_groups(
EventType::OrganizationUserUpdatedGroups as i32, EventType::OrganizationUserUpdatedGroups as i32,
org_user_id, org_user_id,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -2671,7 +2649,7 @@ async fn delete_group_user(
EventType::OrganizationUserUpdatedGroups as i32, EventType::OrganizationUserUpdatedGroups as i32,
org_user_id, org_user_id,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -2760,7 +2738,7 @@ async fn put_reset_password(
EventType::OrganizationUserAdminResetPassword as i32, EventType::OrganizationUserAdminResetPassword as i32,
org_user_id, org_user_id,
org_id, org_id,
headers.user.uuid.clone(), &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
&mut conn, &mut conn,
@ -2887,8 +2865,7 @@ async fn put_reset_password_enrollment(
EventType::OrganizationUserResetPasswordWithdraw as i32 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) log_event(log_id, org_user_id, org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await;
.await;
Ok(()) Ok(())
} }

Datei anzeigen

@ -5,7 +5,10 @@ use rocket::Route;
use serde_json::Value; use serde_json::Value;
use crate::{ 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}, auth::{ClientHeaders, Headers},
crypto, crypto,
db::{models::*, DbConn, DbPool}, db::{models::*, DbConn, DbPool},
@ -96,6 +99,7 @@ async fn recover(data: JsonUpcase<RecoverTwoFactor>, client_headers: ClientHeade
// Remove all twofactors from the user // Remove all twofactors from the user
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; 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( log_user_event(
EventType::UserRecovered2fa as i32, EventType::UserRecovered2fa as i32,
@ -149,22 +153,8 @@ async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Head
.await; .await;
} }
let twofactor_disabled = TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty(); 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?;
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?;
}
}
} }
Ok(Json(json!({ Ok(Json(json!({
@ -179,6 +169,78 @@ async fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers:
disable_twofactor(data, headers, conn).await 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) { pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
debug!("Sending notifications for incomplete 2FA logins"); debug!("Sending notifications for incomplete 2FA logins");

Datei anzeigen

@ -9,9 +9,11 @@ use serde_json::Value;
use crate::{ use crate::{
api::{ api::{
core::accounts::{PreloginData, RegisterData, _prelogin, _register}, core::{
core::log_user_event, accounts::{PreloginData, RegisterData, _prelogin, _register},
core::two_factor::{duo, email, email::EmailTokenData, yubikey}, log_user_event,
two_factor::{authenticator, duo, email, enforce_2fa_policy, webauthn, yubikey},
},
ApiResult, EmptyResult, JsonResult, JsonUpcase, ApiResult, EmptyResult, JsonResult, JsonUpcase,
}, },
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, 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 (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 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 { 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( async fn twofactor_auth(
user_uuid: &str, user: &User,
data: &ConnectData, data: &ConnectData,
device: &mut Device, device: &mut Device,
ip: &ClientIp, ip: &ClientIp,
conn: &mut DbConn, conn: &mut DbConn,
) -> ApiResult<Option<String>> { ) -> ApiResult<Option<String>> {
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 // No twofactor token if twofactor is disabled
if twofactors.is_empty() { if twofactors.is_empty() {
enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?;
return Ok(None); 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 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 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 { let twofactor_code = match data.two_factor_token {
Some(ref code) => code, 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); 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; use crate::crypto::ct_eq;
let selected_data = _selected_data(selected_twofactor); let selected_data = _selected_data(selected_twofactor);
@ -501,17 +503,15 @@ async fn twofactor_auth(
match TwoFactorType::from_i32(selected_id) { match TwoFactorType::from_i32(selected_id) {
Some(TwoFactorType::Authenticator) => { Some(TwoFactorType::Authenticator) => {
_tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn).await? authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
} }
Some(TwoFactorType::Webauthn) => { Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
_tf::webauthn::validate_webauthn_login(user_uuid, twofactor_code, conn).await? Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
}
Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
Some(TwoFactorType::Duo) => { 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) => { 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) => { Some(TwoFactorType::Remember) => {
@ -521,7 +521,7 @@ async fn twofactor_auth(
} }
_ => { _ => {
err_json!( 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" "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 { if !CONFIG.disable_2fa_remember() && remember == 1 {
Ok(Some(device.refresh_twofactor_remember())) Ok(Some(device.refresh_twofactor_remember()))
@ -550,8 +550,6 @@ fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
} }
async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbConn) -> ApiResult<Value> { async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbConn) -> ApiResult<Value> {
use crate::api::core::two_factor;
let mut result = json!({ let mut result = json!({
"error" : "invalid_grant", "error" : "invalid_grant",
"error_description" : "Two factor required.", "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::Authenticator) => { /* Nothing to do for TOTP */ }
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { 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; 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) => { 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 { let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await {
Some(tf) => tf, Some(tf) => tf,
None => err!("No twofactor email registered"), 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 // Send email immediately if email is the only 2FA option
if providers.len() == 1 { 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!({ result["TwoFactorProviders2"][provider.to_string()] = json!({
"Email": email::obscure_email(&email_data.email), "Email": email::obscure_email(&email_data.email),
}) })

Datei anzeigen

@ -664,6 +664,16 @@ impl UserOrganization {
}} }}
} }
pub async fn find_confirmed_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.load::<UserOrganizationDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
db_run! { conn: { db_run! { conn: {
users_organizations::table users_organizations::table