@ -9,15 +9,19 @@ use crate::{
api ::admin ::FAKE_ADMIN_UUID ,
api ::admin ::FAKE_ADMIN_UUID ,
api ::{
api ::{
EmptyResult , JsonResult , Notify , PasswordOrOtpData , UpdateType ,
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 } ,
auth ::{ AdminHeaders , Headers , ManagerHeaders , ManagerHeadersLoose , OrgMemberHeaders , OwnerHeaders , decode_invite } ,
db ::{
db ::{
DbConn ,
DbConn ,
models ::{
models ::{
Cipher , CipherId , Collection , CollectionCipher , CollectionGroup , CollectionId , CollectionUser , EventType ,
AutoConfirmRequirement , Cipher , CipherId , Collection , CollectionCipher , CollectionGroup , CollectionId ,
Group , GroupId , GroupUser , Invitation , Membership , MembershipId , MembershipStatus , MembershipType ,
CollectionUser , EventType , Group , GroupId , GroupUser , Invitation , Membership , MembershipId ,
OrgPolicy , OrgPolicyType , Organization , OrganizationApiKey , OrganizationId , User , UserId ,
MembershipStatus , MembershipType , OrgPolicy , OrgPolicyType , Organization , OrganizationApiKey ,
OrganizationId , User , UserId ,
} ,
} ,
} ,
} ,
mail ,
mail ,
@ -55,6 +59,9 @@ pub fn routes() -> Vec<Route> {
bulk_reinvite_members ,
bulk_reinvite_members ,
confirm_invite ,
confirm_invite ,
bulk_confirm_invite ,
bulk_confirm_invite ,
get_pending_auto_confirm_members ,
auto_confirm_member ,
bulk_auto_confirm_members ,
accept_invite ,
accept_invite ,
get_org_user_mini_details ,
get_org_user_mini_details ,
get_user ,
get_user ,
@ -196,6 +203,15 @@ async fn create_organization(headers: Headers, data: Json<OrgData>, conn: DbConn
if ! CONFIG . is_org_creation_allowed ( & headers . user . email ) {
if ! CONFIG . is_org_creation_allowed ( & headers . user . email ) {
err ! ( "User not allowed to create organizations" )
err ! ( "User not allowed to create organizations" )
}
}
// Stricter than the SingleOrg policy below, which exempts owners and admins: an organization which
// confirms its members automatically forbids every one of its members, in any role and in any status,
// to be part of another organization, so it may not create one either.
// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs
if AutoConfirmRequirement ::for_user ( & headers . user . uuid , & conn ) . await . forbids_creating_organization ( ) {
err ! (
"You may not create an organization. You belong to an organization which confirms its members automatically and prohibits you from being a member of any other organization."
)
}
if OrgPolicy ::is_applicable_to_user ( & headers . user . uuid , OrgPolicyType ::SingleOrg , None , & conn ) . await {
if OrgPolicy ::is_applicable_to_user ( & headers . user . uuid , OrgPolicyType ::SingleOrg , None , & conn ) . await {
err ! (
err ! (
"You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization."
"You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization."
@ -1045,6 +1061,7 @@ async fn send_invite(
data : Json < InviteData > ,
data : Json < InviteData > ,
headers : AdminHeaders ,
headers : AdminHeaders ,
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
if org_id ! = headers . org_id {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
err ! ( "Organization not found" , "Organization id's do not match" ) ;
@ -1185,6 +1202,11 @@ async fn send_invite(
let mut group_entry = GroupUser ::new ( group_id . clone ( ) , new_member . uuid . clone ( ) ) ;
let mut group_entry = GroupUser ::new ( group_id . clone ( ) , new_member . uuid . clone ( ) ) ;
group_entry . save ( & conn ) . await ? ;
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 ( ( ) )
Ok ( ( ) )
@ -1196,6 +1218,7 @@ async fn bulk_reinvite_members(
data : Json < BulkMembershipIds > ,
data : Json < BulkMembershipIds > ,
headers : AdminHeaders ,
headers : AdminHeaders ,
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
if org_id ! = headers . org_id {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
err ! ( "Organization not found" , "Organization id's do not match" ) ;
@ -1204,7 +1227,7 @@ async fn bulk_reinvite_members(
let mut bulk_response = Vec ::new ( ) ;
let mut bulk_response = Vec ::new ( ) ;
for member_id in data . ids {
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 ( ) ,
Ok ( ( ) ) = > String ::new ( ) ,
Err ( e ) = > format ! ( "{e:?}" ) ,
Err ( e ) = > format ! ( "{e:?}" ) ,
} ;
} ;
@ -1231,11 +1254,12 @@ async fn reinvite_member(
member_id : MembershipId ,
member_id : MembershipId ,
headers : AdminHeaders ,
headers : AdminHeaders ,
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
if org_id ! = headers . org_id {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
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 (
async fn reinvite_member_impl (
@ -1243,6 +1267,7 @@ async fn reinvite_member_impl(
member_id : & MembershipId ,
member_id : & MembershipId ,
invited_by_email : & str ,
invited_by_email : & str ,
conn : & DbConn ,
conn : & DbConn ,
nt : & Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
let Some ( member ) = Membership ::find_by_uuid_and_org ( member_id , org_id , conn ) . await else {
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." )
err ! ( "The user hasn't been invited to the organization." )
@ -1276,6 +1301,7 @@ async fn reinvite_member_impl(
let mut member = member ;
let mut member = member ;
member . status = MembershipStatus ::Accepted as i32 ;
member . status = MembershipStatus ::Accepted as i32 ;
member . save ( conn ) . await ? ;
member . save ( conn ) . await ? ;
notify_pending_auto_confirm ( & member , conn , nt ) . await ;
}
}
Ok ( ( ) )
Ok ( ( ) )
@ -1295,6 +1321,7 @@ async fn accept_invite(
data : Json < AcceptData > ,
data : Json < AcceptData > ,
headers : Headers ,
headers : Headers ,
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
// The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead
// 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 ( ) ;
let data : AcceptData = data . into_inner ( ) ;
@ -1333,7 +1360,7 @@ async fn accept_invite(
// In case the user was invited before the mail was saved in db.
// 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 ) ;
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 ( ) {
} else if CONFIG . mail_enabled ( ) {
// User was invited from /admin, so they are automatically confirmed
// User was invited from /admin, so they are automatically confirmed
let org_name = CONFIG . invitation_org_name ( ) ;
let org_name = CONFIG . invitation_org_name ( ) ;
@ -1373,7 +1400,11 @@ async fn bulk_confirm_invite(
match data . keys {
match data . keys {
Some ( keys ) = > {
Some ( keys ) = > {
for invite in 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 user_key = invite . key . unwrap_or_default ( ) ;
let err_msg = match confirm_invite_impl ( & org_id , & member_id , & user_key , & headers , & conn , & nt ) . await {
let err_msg = match confirm_invite_impl ( & org_id , & member_id , & user_key , & headers , & conn , & nt ) . await {
Ok ( ( ) ) = > String ::new ( ) ,
Ok ( ( ) ) = > String ::new ( ) ,
@ -1428,7 +1459,7 @@ async fn confirm_invite_impl(
err ! ( "Key or UserId is not set, unable to process request" ) ;
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" )
err ! ( "The specified user isn't a member of the organization" )
} ;
} ;
@ -1436,6 +1467,20 @@ async fn confirm_invite_impl(
err ! ( "Only Owners can confirm Managers, Admins or Owners" )
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 {
if member_to_confirm . status ! = MembershipStatus ::Accepted as i32 {
err ! ( "User in invalid state" )
err ! ( "User in invalid state" )
}
}
@ -1446,10 +1491,20 @@ async fn confirm_invite_impl(
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
// 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 ? ;
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 (
log_event (
EventType ::OrganizationUserConfirmed ,
EventType ::OrganizationUserConfirmed ,
& member_to_confirm . uuid ,
& member_to_confirm . uuid ,
org_id ,
& org_id ,
& headers . user . uuid ,
& headers . user . uuid ,
headers . device . atype ,
headers . device . atype ,
& headers . ip . ip ,
& headers . ip . ip ,
@ -1458,7 +1513,7 @@ async fn confirm_invite_impl(
. await ;
. await ;
if CONFIG . mail_enabled ( ) {
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
org . name
} else {
} else {
err ! ( "Error looking up organization." )
err ! ( "Error looking up organization." )
@ -1480,6 +1535,142 @@ async fn confirm_invite_impl(
save_result
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) ]
#[ get( " /organizations/<org_id>/users/mini-details " , rank = 1) ]
async fn get_org_user_mini_details ( org_id : OrganizationId , headers : ManagerHeadersLoose , conn : DbConn ) -> JsonResult {
async fn get_org_user_mini_details ( org_id : OrganizationId , headers : ManagerHeadersLoose , conn : DbConn ) -> JsonResult {
if org_id ! = headers . membership . org_uuid {
if org_id ! = headers . membership . org_uuid {
@ -2113,6 +2304,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 . counts_for_auto_confirm ( )
& & Membership ::count_accepted_confirmed_and_revoked_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
// 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 {
two_factor ::enforce_2fa_policy_for_org (
two_factor ::enforce_2fa_policy_for_org (
@ -2169,6 +2408,25 @@ async fn put_policy(
policy . data = serde_json ::to_string ( & data . data ) ? ;
policy . data = serde_json ::to_string ( & data . data ) ? ;
policy . save ( & conn ) . await ? ;
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 (
log_event (
EventType ::PolicyUpdated ,
EventType ::PolicyUpdated ,
policy . uuid . as_ref ( ) ,
policy . uuid . as_ref ( ) ,
@ -2434,6 +2692,20 @@ async fn restore_member_impl(
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
// This check need to be done after restoring to work with the correct status
// This check need to be done after restoring to work with the correct status
OrgPolicy ::check_user_allowed ( & member , "restore" , conn ) . await ? ;
OrgPolicy ::check_user_allowed ( & member , "restore" , conn ) . await ? ;
// A revoked membership is restored without another accept step, so the member is back inside the
// organization with whatever it set up while it was revoked. Emergency access created in that
// window would therefore outlive the revocation, which the policy must not allow, so it is
// dropped here before the membership becomes active again. Like enabling the policy this leaves
// a member which is only invited alone, an invitation must never delete data of an account that
// never joined.
// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs
if member . status ! = MembershipStatus ::Invited as i32
& & OrgPolicy ::is_auto_confirm_enabled ( org_id , conn ) . await
{
delete_all_emergency_access_of_user ( & member . user_uuid , conn ) . await ? ;
}
member . save ( conn ) . await ? ;
member . save ( conn ) . await ? ;
log_event (
log_event (
@ -3251,3 +3523,32 @@ async fn rotate_api_key(
) -> JsonResult {
) -> JsonResult {
api_key ( & org_id , data , true , headers , conn ) . await
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 ) ) ;
}
}