|
|
|
@ -9,7 +9,10 @@ use crate::{ |
|
|
|
api::admin::FAKE_ADMIN_UUID, |
|
|
|
api::{ |
|
|
|
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, |
|
|
|
core::{CipherSyncData, CipherSyncType, accept_org_invite, log_event, two_factor}, |
|
|
|
core::{ |
|
|
|
CipherSyncData, CipherSyncType, accept_org_invite, emergency_access::delete_all_emergency_access_of_user, |
|
|
|
log_event, notify_pending_auto_confirm, two_factor, |
|
|
|
}, |
|
|
|
}, |
|
|
|
auth::{AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders, decode_invite}, |
|
|
|
db::{ |
|
|
|
@ -55,6 +58,9 @@ pub fn routes() -> Vec<Route> { |
|
|
|
bulk_reinvite_members, |
|
|
|
confirm_invite, |
|
|
|
bulk_confirm_invite, |
|
|
|
get_pending_auto_confirm_members, |
|
|
|
auto_confirm_member, |
|
|
|
bulk_auto_confirm_members, |
|
|
|
accept_invite, |
|
|
|
get_org_user_mini_details, |
|
|
|
get_user, |
|
|
|
@ -1045,6 +1051,7 @@ async fn send_invite( |
|
|
|
data: Json<InviteData>, |
|
|
|
headers: AdminHeaders, |
|
|
|
conn: DbConn, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> EmptyResult { |
|
|
|
if org_id != headers.org_id { |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
@ -1185,6 +1192,11 @@ async fn send_invite( |
|
|
|
let mut group_entry = GroupUser::new(group_id.clone(), new_member.uuid.clone()); |
|
|
|
group_entry.save(&conn).await?; |
|
|
|
} |
|
|
|
|
|
|
|
// With mail disabled an existing user is accepted right away, so there is no accept request later on.
|
|
|
|
// This is the last step on purpose: an admin client may confirm the member the moment it is told
|
|
|
|
// about it, and by then the collections and groups of the invite have to be in place.
|
|
|
|
notify_pending_auto_confirm(&new_member, &conn, &nt).await; |
|
|
|
} |
|
|
|
|
|
|
|
Ok(()) |
|
|
|
@ -1196,6 +1208,7 @@ async fn bulk_reinvite_members( |
|
|
|
data: Json<BulkMembershipIds>, |
|
|
|
headers: AdminHeaders, |
|
|
|
conn: DbConn, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> JsonResult { |
|
|
|
if org_id != headers.org_id { |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
@ -1204,7 +1217,7 @@ async fn bulk_reinvite_members( |
|
|
|
|
|
|
|
let mut bulk_response = Vec::new(); |
|
|
|
for member_id in data.ids { |
|
|
|
let err_msg = match reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn).await { |
|
|
|
let err_msg = match reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn, &nt).await { |
|
|
|
Ok(()) => String::new(), |
|
|
|
Err(e) => format!("{e:?}"), |
|
|
|
}; |
|
|
|
@ -1231,11 +1244,12 @@ async fn reinvite_member( |
|
|
|
member_id: MembershipId, |
|
|
|
headers: AdminHeaders, |
|
|
|
conn: DbConn, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> EmptyResult { |
|
|
|
if org_id != headers.org_id { |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
} |
|
|
|
reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn).await |
|
|
|
reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn, &nt).await |
|
|
|
} |
|
|
|
|
|
|
|
async fn reinvite_member_impl( |
|
|
|
@ -1243,6 +1257,7 @@ async fn reinvite_member_impl( |
|
|
|
member_id: &MembershipId, |
|
|
|
invited_by_email: &str, |
|
|
|
conn: &DbConn, |
|
|
|
nt: &Notify<'_>, |
|
|
|
) -> EmptyResult { |
|
|
|
let Some(member) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { |
|
|
|
err!("The user hasn't been invited to the organization.") |
|
|
|
@ -1276,6 +1291,7 @@ async fn reinvite_member_impl( |
|
|
|
let mut member = member; |
|
|
|
member.status = MembershipStatus::Accepted as i32; |
|
|
|
member.save(conn).await?; |
|
|
|
notify_pending_auto_confirm(&member, conn, nt).await; |
|
|
|
} |
|
|
|
|
|
|
|
Ok(()) |
|
|
|
@ -1295,6 +1311,7 @@ async fn accept_invite( |
|
|
|
data: Json<AcceptData>, |
|
|
|
headers: Headers, |
|
|
|
conn: DbConn, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> EmptyResult { |
|
|
|
// The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead
|
|
|
|
let data: AcceptData = data.into_inner(); |
|
|
|
@ -1333,7 +1350,7 @@ async fn accept_invite( |
|
|
|
// In case the user was invited before the mail was saved in db.
|
|
|
|
membership.invited_by_email = membership.invited_by_email.or(claims.invited_by_email); |
|
|
|
|
|
|
|
accept_org_invite(&headers.user, membership, reset_password_key, &conn).await?; |
|
|
|
accept_org_invite(&headers.user, membership, reset_password_key, &conn, &nt).await?; |
|
|
|
} else if CONFIG.mail_enabled() { |
|
|
|
// User was invited from /admin, so they are automatically confirmed
|
|
|
|
let org_name = CONFIG.invitation_org_name(); |
|
|
|
@ -1373,7 +1390,11 @@ async fn bulk_confirm_invite( |
|
|
|
match data.keys { |
|
|
|
Some(keys) => { |
|
|
|
for invite in keys { |
|
|
|
let member_id = invite.id.unwrap(); |
|
|
|
// Never unwrap the id, this is client supplied and a missing one must not take the request down
|
|
|
|
let Some(member_id) = invite.id else { |
|
|
|
error!("Ignoring a bulk confirm entry without a member id"); |
|
|
|
continue; |
|
|
|
}; |
|
|
|
let user_key = invite.key.unwrap_or_default(); |
|
|
|
let err_msg = match confirm_invite_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await { |
|
|
|
Ok(()) => String::new(), |
|
|
|
@ -1428,7 +1449,7 @@ async fn confirm_invite_impl( |
|
|
|
err!("Key or UserId is not set, unable to process request"); |
|
|
|
} |
|
|
|
|
|
|
|
let Some(mut member_to_confirm) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { |
|
|
|
let Some(member_to_confirm) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { |
|
|
|
err!("The specified user isn't a member of the organization") |
|
|
|
}; |
|
|
|
|
|
|
|
@ -1436,6 +1457,20 @@ async fn confirm_invite_impl( |
|
|
|
err!("Only Owners can confirm Managers, Admins or Owners") |
|
|
|
} |
|
|
|
|
|
|
|
confirm_member(member_to_confirm, key, headers, conn, nt).await |
|
|
|
} |
|
|
|
|
|
|
|
/// Shared by the manual and the automatic confirmation, both hand us the organization key encrypted
|
|
|
|
/// with the public key of the member to confirm.
|
|
|
|
async fn confirm_member( |
|
|
|
mut member_to_confirm: Membership, |
|
|
|
key: &str, |
|
|
|
headers: &AdminHeaders, |
|
|
|
conn: &DbConn, |
|
|
|
nt: &Notify<'_>, |
|
|
|
) -> EmptyResult { |
|
|
|
let org_id = member_to_confirm.org_uuid.clone(); |
|
|
|
|
|
|
|
if member_to_confirm.status != MembershipStatus::Accepted as i32 { |
|
|
|
err!("User in invalid state") |
|
|
|
} |
|
|
|
@ -1446,10 +1481,20 @@ async fn confirm_invite_impl( |
|
|
|
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
|
|
|
|
OrgPolicy::check_user_allowed(&member_to_confirm, "confirm", conn).await?; |
|
|
|
|
|
|
|
// An organization which confirms its members automatically does not tolerate emergency access: the
|
|
|
|
// grantee could take over the account of a member that nobody ever vetted and reach the organization
|
|
|
|
// vault through it. Enabling the policy drops the grants of the members present at that time, this
|
|
|
|
// covers the member which brings one along when it joins afterwards. Bitwarden does the same, and
|
|
|
|
// like there it applies to the manual confirmation as well.
|
|
|
|
// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
|
|
|
|
if OrgPolicy::is_auto_confirm_enabled(&org_id, conn).await { |
|
|
|
delete_all_emergency_access_of_user(&member_to_confirm.user_uuid, conn).await?; |
|
|
|
} |
|
|
|
|
|
|
|
log_event( |
|
|
|
EventType::OrganizationUserConfirmed as i32, |
|
|
|
&member_to_confirm.uuid, |
|
|
|
org_id, |
|
|
|
&org_id, |
|
|
|
&headers.user.uuid, |
|
|
|
headers.device.atype, |
|
|
|
&headers.ip.ip, |
|
|
|
@ -1458,7 +1503,7 @@ async fn confirm_invite_impl( |
|
|
|
.await; |
|
|
|
|
|
|
|
if CONFIG.mail_enabled() { |
|
|
|
let org_name = if let Some(org) = Organization::find_by_uuid(org_id, conn).await { |
|
|
|
let org_name = if let Some(org) = Organization::find_by_uuid(&org_id, conn).await { |
|
|
|
org.name |
|
|
|
} else { |
|
|
|
err!("Error looking up organization.") |
|
|
|
@ -1480,6 +1525,142 @@ async fn confirm_invite_impl( |
|
|
|
save_result |
|
|
|
} |
|
|
|
|
|
|
|
// Automatic user confirmation. The server can never confirm a member by itself, confirming means
|
|
|
|
// encrypting the organization key with the public key of the member and the server does not have the
|
|
|
|
// organization key. So all we do here is telling an admin client which members are waiting, the client
|
|
|
|
// does the actual work in the background.
|
|
|
|
// https://bitwarden.com/help/automatic-confirmation/
|
|
|
|
|
|
|
|
/// Only a member which accepted its invitation and holds the plain User role is ever confirmed without
|
|
|
|
/// a human looking at it. Every elevated role keeps needing a manual confirmation by an Owner, and an
|
|
|
|
/// Owner can not lift that restriction here like it can for the manual confirmation.
|
|
|
|
fn may_be_confirmed_automatically(member: &Membership) -> bool { |
|
|
|
member.status == MembershipStatus::Accepted as i32 && member.atype == MembershipType::User |
|
|
|
} |
|
|
|
|
|
|
|
#[get("/organizations/<org_id>/users/pending-auto-confirm")] |
|
|
|
async fn get_pending_auto_confirm_members(org_id: OrganizationId, headers: AdminHeaders, conn: DbConn) -> JsonResult { |
|
|
|
if org_id != headers.org_id { |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
} |
|
|
|
|
|
|
|
// Bitwarden responds with an empty list instead of an error when the feature or the policy is off.
|
|
|
|
let members = if OrgPolicy::is_auto_confirm_enabled(&org_id, &conn).await { |
|
|
|
Membership::find_by_org(&org_id, &conn) |
|
|
|
.await |
|
|
|
.into_iter() |
|
|
|
.filter(may_be_confirmed_automatically) |
|
|
|
.map(|m| { |
|
|
|
json!({ |
|
|
|
"object": "organizationUserPendingAutoConfirm", |
|
|
|
"id": m.uuid, |
|
|
|
"userId": m.user_uuid, |
|
|
|
}) |
|
|
|
}) |
|
|
|
.collect() |
|
|
|
} else { |
|
|
|
Vec::new() |
|
|
|
}; |
|
|
|
|
|
|
|
Ok(Json(json!({ |
|
|
|
"data": members, |
|
|
|
"object": "list", |
|
|
|
"continuationToken": null |
|
|
|
}))) |
|
|
|
} |
|
|
|
|
|
|
|
#[post("/organizations/<org_id>/users/<member_id>/auto-confirm", data = "<data>")] |
|
|
|
async fn auto_confirm_member( |
|
|
|
org_id: OrganizationId, |
|
|
|
member_id: MembershipId, |
|
|
|
data: Json<ConfirmData>, |
|
|
|
headers: AdminHeaders, |
|
|
|
conn: DbConn, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> EmptyResult { |
|
|
|
let data = data.into_inner(); |
|
|
|
let user_key = data.key.unwrap_or_default(); |
|
|
|
auto_confirm_member_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await |
|
|
|
} |
|
|
|
|
|
|
|
#[post("/organizations/<org_id>/users/bulk-auto-confirm", data = "<data>")] |
|
|
|
async fn bulk_auto_confirm_members( |
|
|
|
org_id: OrganizationId, |
|
|
|
data: Json<BulkConfirmData>, |
|
|
|
headers: AdminHeaders, |
|
|
|
conn: DbConn, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> JsonResult { |
|
|
|
if org_id != headers.org_id { |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
} |
|
|
|
let data = data.into_inner(); |
|
|
|
|
|
|
|
let mut bulk_response = Vec::new(); |
|
|
|
match data.keys { |
|
|
|
Some(keys) => { |
|
|
|
for member in keys { |
|
|
|
// Never unwrap the id, this is client supplied and a missing one must not take the request down
|
|
|
|
let Some(member_id) = member.id else { |
|
|
|
error!("Ignoring a bulk auto confirm entry without a member id"); |
|
|
|
continue; |
|
|
|
}; |
|
|
|
let user_key = member.key.unwrap_or_default(); |
|
|
|
let err_msg = match auto_confirm_member_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await |
|
|
|
{ |
|
|
|
Ok(()) => String::new(), |
|
|
|
Err(e) => format!("{e:?}"), |
|
|
|
}; |
|
|
|
|
|
|
|
bulk_response.push(json!( |
|
|
|
{ |
|
|
|
"object": "OrganizationBulkConfirmResponseModel", |
|
|
|
"id": member_id, |
|
|
|
"error": err_msg |
|
|
|
} |
|
|
|
)); |
|
|
|
} |
|
|
|
} |
|
|
|
None => error!("No keys to confirm"), |
|
|
|
} |
|
|
|
|
|
|
|
Ok(Json(json!({ |
|
|
|
"data": bulk_response, |
|
|
|
"object": "list", |
|
|
|
"continuationToken": null |
|
|
|
}))) |
|
|
|
} |
|
|
|
|
|
|
|
async fn auto_confirm_member_impl( |
|
|
|
org_id: &OrganizationId, |
|
|
|
member_id: &MembershipId, |
|
|
|
key: &str, |
|
|
|
headers: &AdminHeaders, |
|
|
|
conn: &DbConn, |
|
|
|
nt: &Notify<'_>, |
|
|
|
) -> EmptyResult { |
|
|
|
if org_id != &headers.org_id { |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
} |
|
|
|
if key.is_empty() || member_id.is_empty() { |
|
|
|
err!("Key or UserId is not set, unable to process request"); |
|
|
|
} |
|
|
|
|
|
|
|
if !OrgPolicy::is_auto_confirm_enabled(org_id, conn).await { |
|
|
|
err!("Automatic user confirmation is not enabled for this organization") |
|
|
|
} |
|
|
|
|
|
|
|
let Some(member_to_confirm) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { |
|
|
|
err!("The specified user isn't a member of the organization") |
|
|
|
}; |
|
|
|
|
|
|
|
if !may_be_confirmed_automatically(&member_to_confirm) { |
|
|
|
err!("This member can not be confirmed automatically") |
|
|
|
} |
|
|
|
|
|
|
|
confirm_member(member_to_confirm, key, headers, conn, nt).await |
|
|
|
} |
|
|
|
|
|
|
|
#[get("/organizations/<org_id>/users/mini-details", rank = 1)] |
|
|
|
async fn get_org_user_mini_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { |
|
|
|
if org_id != headers.membership.org_uuid { |
|
|
|
@ -2113,6 +2294,54 @@ async fn put_policy( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// The automatic user confirmation policy hands out organization access without anybody looking at it,
|
|
|
|
// so it needs to be allowed by the server first and it requires the Single Org policy on top.
|
|
|
|
// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/AutomaticUserConfirmationPolicyEventHandler.cs
|
|
|
|
let auto_confirm_turned_on = if pol_type_enum == OrgPolicyType::AutomaticUserConfirmation |
|
|
|
&& data.enabled |
|
|
|
// Only the step from disabled to enabled validates and has side effects. The web vault saves a
|
|
|
|
// policy on every edit, and re-running the below on an already enabled policy would keep wiping
|
|
|
|
// emergency access that members created in the meantime. Bitwarden guards this the same way.
|
|
|
|
&& !OrgPolicy::is_auto_confirm_enabled(&org_id, &conn).await |
|
|
|
{ |
|
|
|
if !CONFIG.org_auto_confirm_enabled() { |
|
|
|
err!("Automatic user confirmation is not enabled on this server.") |
|
|
|
} |
|
|
|
|
|
|
|
let single_org_policy_enabled = |
|
|
|
match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::SingleOrg, &conn).await { |
|
|
|
Some(p) => p.enabled, |
|
|
|
None => false, |
|
|
|
}; |
|
|
|
|
|
|
|
if !single_org_policy_enabled { |
|
|
|
err!("Single Organization policy is not enabled. It is mandatory for this policy to be enabled.") |
|
|
|
} |
|
|
|
|
|
|
|
// Every member has to be compliant already. Contrary to the Single Org policy below we do not revoke
|
|
|
|
// the members that are not, because this policy also applies to owners and admins and revoking those
|
|
|
|
// could lock the organization out of itself.
|
|
|
|
for member in Membership::find_by_org(&org_id, &conn).await { |
|
|
|
if member.status != MembershipStatus::Invited as i32 |
|
|
|
&& Membership::count_accepted_and_confirmed_by_user(&member.user_uuid, &org_id, &conn).await > 0 |
|
|
|
{ |
|
|
|
err!("This policy forbids members to be part of other organizations, but at least one member still is.") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
true |
|
|
|
} else { |
|
|
|
false |
|
|
|
}; |
|
|
|
|
|
|
|
// Also prevent the Single Org policy to be disabled while automatic user confirmation depends on it
|
|
|
|
if pol_type_enum == OrgPolicyType::SingleOrg |
|
|
|
&& !data.enabled |
|
|
|
&& OrgPolicy::is_auto_confirm_enabled(&org_id, &conn).await |
|
|
|
{ |
|
|
|
err!("Automatic user confirmation is enabled. It is not allowed to disable this policy.") |
|
|
|
} |
|
|
|
|
|
|
|
// When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA
|
|
|
|
if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { |
|
|
|
two_factor::enforce_2fa_policy_for_org( |
|
|
|
@ -2169,6 +2398,25 @@ async fn put_policy( |
|
|
|
policy.data = serde_json::to_string(&data.data)?; |
|
|
|
policy.save(&conn).await?; |
|
|
|
|
|
|
|
// Emergency access would hand the account of a member to somebody outside of the control of this
|
|
|
|
// organization, which defeats the point of vetting members. Bitwarden drops these grants when the
|
|
|
|
// policy is turned on, and blocks new ones while it is on (see `emergency_access.rs`).
|
|
|
|
// This runs after the policy is stored so that a failed save can not destroy data for nothing, and it
|
|
|
|
// skips invited members on purpose: an invitation is created by an admin without any consent of the
|
|
|
|
// invited user, so it must never be able to delete data of an account that never joined.
|
|
|
|
if auto_confirm_turned_on { |
|
|
|
for member in Membership::find_by_org(&org_id, &conn).await { |
|
|
|
if member.status == MembershipStatus::Invited as i32 { |
|
|
|
continue; |
|
|
|
} |
|
|
|
info!( |
|
|
|
"Removing emergency access of {} because automatic user confirmation was enabled for {org_id}", |
|
|
|
member.user_uuid |
|
|
|
); |
|
|
|
delete_all_emergency_access_of_user(&member.user_uuid, &conn).await?; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
log_event( |
|
|
|
EventType::PolicyUpdated as i32, |
|
|
|
policy.uuid.as_ref(), |
|
|
|
@ -3251,3 +3499,32 @@ async fn rotate_api_key( |
|
|
|
) -> JsonResult { |
|
|
|
api_key(&org_id, data, true, headers, conn).await |
|
|
|
} |
|
|
|
|
|
|
|
#[cfg(test)] |
|
|
|
mod tests { |
|
|
|
use super::*; |
|
|
|
|
|
|
|
/// Automatic confirmation hands out access to the organization vault without anybody looking at it,
|
|
|
|
/// so it must stay limited to plain members which actually accepted their invitation.
|
|
|
|
#[test] |
|
|
|
fn only_accepted_plain_members_are_confirmed_automatically() { |
|
|
|
let mut member = |
|
|
|
Membership::new(UserId::from(String::from("user")), OrganizationId::from(String::from("org")), None); |
|
|
|
|
|
|
|
for status in |
|
|
|
[MembershipStatus::Revoked as i32, MembershipStatus::Invited as i32, MembershipStatus::Confirmed as i32] |
|
|
|
{ |
|
|
|
member.status = status; |
|
|
|
assert!(!may_be_confirmed_automatically(&member), "status {status} must not qualify"); |
|
|
|
} |
|
|
|
|
|
|
|
member.status = MembershipStatus::Accepted as i32; |
|
|
|
for atype in [MembershipType::Owner as i32, MembershipType::Admin as i32, MembershipType::Manager as i32] { |
|
|
|
member.atype = atype; |
|
|
|
assert!(!may_be_confirmed_automatically(&member), "type {atype} must not qualify"); |
|
|
|
} |
|
|
|
|
|
|
|
member.atype = MembershipType::User as i32; |
|
|
|
assert!(may_be_confirmed_automatically(&member)); |
|
|
|
} |
|
|
|
} |
|
|
|
|