1
0
Fork 1
Spiegel von https://github.com/dani-garcia/vaultwarden.git synchronisiert 2024-06-30 19:24:42 +02:00

Add Org user revoke feature

This PR adds a the new v2022.8.x revoke feature which allows an
organization owner or admin to revoke access for one or more users.

This PR also fixes several permissions and policy checks which were faulty.

- Modified some functions to use DB Count features instead of iter/count aftwards.
- Rearanged some if statements (faster matching or just one if instead of nested if's)
- Added and fixed several policy checks where needed
- Some small updates on some response models
- Made some functions require an enum instead of an i32
Dieser Commit ist enthalten in:
BlackDex 2022-08-20 16:42:36 +02:00
Ursprung 60ed5ff99d
Commit 1722742ab3
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 58C80A2AA6C765E1
9 geänderte Dateien mit 486 neuen und 148 gelöschten Zeilen

Datei anzeigen

@ -418,15 +418,26 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, c
}; };
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner { if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
// Removing owner permmission, check that there are at least another owner // Removing owner permmission, check that there is at least one other confirmed owner
let num_owners = if UserOrganization::count_confirmed_by_org_and_type(&data.org_uuid, UserOrgType::Owner, &conn).await <= 1 {
UserOrganization::find_by_org_and_type(&data.org_uuid, UserOrgType::Owner as i32, &conn).await.len();
if num_owners <= 1 {
err!("Can't change the type of the last owner") err!("Can't change the type of the last owner")
} }
} }
// This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_user_org_type
// It returns different error messages per function.
if new_type < UserOrgType::Admin {
match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &conn).await {
Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => {
err!("You cannot modify this user to this type because it has no two-step login method activated");
}
Err(OrgPolicyErr::SingleOrgEnforced) => {
err!("You cannot modify this user to this type because it is a member of an organization which forbids it");
}
}
}
user_to_edit.atype = new_type; user_to_edit.atype = new_type;
user_to_edit.save(&conn).await user_to_edit.save(&conn).await
} }

Datei anzeigen

@ -328,7 +328,7 @@ async fn enforce_personal_ownership_policy(data: Option<&CipherData>, headers: &
if data.is_none() || data.unwrap().OrganizationId.is_none() { if data.is_none() || data.unwrap().OrganizationId.is_none() {
let user_uuid = &headers.user.uuid; let user_uuid = &headers.user.uuid;
let policy_type = OrgPolicyType::PersonalOwnership; let policy_type = OrgPolicyType::PersonalOwnership;
if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn).await { if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, None, conn).await {
err!("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.") err!("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.")
} }
} }

Datei anzeigen

@ -258,7 +258,7 @@ async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Heade
match User::find_by_mail(&email, &conn).await { match User::find_by_mail(&email, &conn).await {
Some(user) => { Some(user) => {
match accept_invite_process(user.uuid, new_emergency_access.uuid, Some(email), conn.borrow()).await { match accept_invite_process(user.uuid, new_emergency_access.uuid, Some(email), conn.borrow()).await {
Ok(v) => (v), Ok(v) => v,
Err(e) => err!(e.to_string()), Err(e) => err!(e.to_string()),
} }
} }
@ -317,7 +317,7 @@ async fn resend_invite(emer_id: String, headers: Headers, conn: DbConn) -> Empty
match accept_invite_process(grantee_user.uuid, emergency_access.uuid, emergency_access.email, conn.borrow()) match accept_invite_process(grantee_user.uuid, emergency_access.uuid, emergency_access.email, conn.borrow())
.await .await
{ {
Ok(v) => (v), Ok(v) => v,
Err(e) => err!(e.to_string()), Err(e) => err!(e.to_string()),
} }
} }
@ -363,7 +363,7 @@ async fn accept_invite(emer_id: String, data: JsonUpcase<AcceptData>, conn: DbCo
&& (claims.grantor_email.is_some() && grantor_user.email == claims.grantor_email.unwrap()) && (claims.grantor_email.is_some() && grantor_user.email == claims.grantor_email.unwrap())
{ {
match accept_invite_process(grantee_user.uuid.clone(), emer_id, Some(grantee_user.email.clone()), &conn).await { match accept_invite_process(grantee_user.uuid.clone(), emer_id, Some(grantee_user.email.clone()), &conn).await {
Ok(v) => (v), Ok(v) => v,
Err(e) => err!(e.to_string()), Err(e) => err!(e.to_string()),
} }

Datei anzeigen

@ -61,6 +61,10 @@ pub fn routes() -> Vec<Route> {
import, import,
post_org_keys, post_org_keys,
bulk_public_keys, bulk_public_keys,
deactivate_organization_user,
bulk_deactivate_organization_user,
activate_organization_user,
bulk_activate_organization_user
] ]
} }
@ -107,7 +111,7 @@ async fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn:
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")
} }
if OrgPolicy::is_applicable_to_user(&headers.user.uuid, OrgPolicyType::SingleOrg, &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."
) )
@ -172,13 +176,10 @@ async fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> E
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await {
None => err!("User not part of organization"), None => err!("User not part of organization"),
Some(user_org) => { Some(user_org) => {
if user_org.atype == UserOrgType::Owner { if user_org.atype == UserOrgType::Owner
let num_owners = && UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &conn).await <= 1
UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).await.len(); {
err!("The last owner can't leave")
if num_owners <= 1 {
err!("The last owner can't leave")
}
} }
user_org.delete(&conn).await user_org.delete(&conn).await
@ -749,17 +750,16 @@ struct AcceptData {
Token: String, Token: String,
} }
#[post("/organizations/<_org_id>/users/<_org_user_id>/accept", data = "<data>")] #[post("/organizations/<org_id>/users/<_org_user_id>/accept", data = "<data>")]
async fn accept_invite( async fn accept_invite(
_org_id: String, org_id: String,
_org_user_id: String, _org_user_id: String,
data: JsonUpcase<AcceptData>, data: JsonUpcase<AcceptData>,
conn: DbConn, conn: DbConn,
) -> EmptyResult { ) -> EmptyResult {
// The web-vault passes org_id and org_user_id in the URL, but we are just reading them from the JWT instead // The web-vault passes org_id and org_user_id in the URL, but we are just reading them from the JWT instead
let data: AcceptData = data.into_inner().data; let data: AcceptData = data.into_inner().data;
let token = &data.Token; let claims = decode_invite(&data.Token)?;
let claims = decode_invite(token)?;
match User::find_by_mail(&claims.email, &conn).await { match User::find_by_mail(&claims.email, &conn).await {
Some(_) => { Some(_) => {
@ -775,46 +775,20 @@ async fn accept_invite(
err!("User already accepted the invitation") err!("User already accepted the invitation")
} }
let user_twofactor_disabled = TwoFactor::find_by_user(&user_org.user_uuid, &conn).await.is_empty(); // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
// It returns different error messages per function.
let policy = OrgPolicyType::TwoFactorAuthentication as i32; if user_org.atype < UserOrgType::Admin {
let org_twofactor_policy_enabled = match OrgPolicy::is_user_allowed(&user_org.user_uuid, &org_id, false, &conn).await {
match OrgPolicy::find_by_org_and_type(&user_org.org_uuid, policy, &conn).await { Ok(_) => {}
Some(p) => p.enabled, Err(OrgPolicyErr::TwoFactorMissing) => {
None => false, err!("You cannot join this organization until you enable two-step login on your user account");
}; }
Err(OrgPolicyErr::SingleOrgEnforced) => {
if org_twofactor_policy_enabled && user_twofactor_disabled { err!("You cannot join this organization because you are a member of an organization which forbids it");
err!("You cannot join this organization until you enable two-step login on your user account.") }
}
// Enforce Single Organization Policy of organization user is trying to join
let single_org_policy_enabled =
match OrgPolicy::find_by_org_and_type(&user_org.org_uuid, OrgPolicyType::SingleOrg as i32, &conn)
.await
{
Some(p) => p.enabled,
None => false,
};
if single_org_policy_enabled && user_org.atype < UserOrgType::Admin {
let is_member_of_another_org = UserOrganization::find_any_state_by_user(&user_org.user_uuid, &conn)
.await
.into_iter()
.filter(|uo| uo.org_uuid != user_org.org_uuid)
.count()
> 1;
if is_member_of_another_org {
err!("You may not join this organization until you leave or remove all other organizations.")
} }
} }
// Enforce Single Organization Policy of other organizations user is a member of
if OrgPolicy::is_applicable_to_user(&user_org.user_uuid, OrgPolicyType::SingleOrg, &conn).await {
err!(
"You cannot join this organization because you are a member of an organization which forbids it"
)
}
user_org.status = UserOrgStatus::Accepted as i32; user_org.status = UserOrgStatus::Accepted as i32;
user_org.save(&conn).await?; user_org.save(&conn).await?;
} }
@ -918,6 +892,20 @@ async fn _confirm_invite(
err!("User in invalid state") err!("User in invalid state")
} }
// This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
// It returns different error messages per function.
if user_to_confirm.atype < UserOrgType::Admin {
match OrgPolicy::is_user_allowed(&user_to_confirm.user_uuid, org_id, true, conn).await {
Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => {
err!("You cannot confirm this user because it has no two-step login method activated");
}
Err(OrgPolicyErr::SingleOrgEnforced) => {
err!("You cannot confirm this user because it is a member of an organization which forbids it");
}
}
}
user_to_confirm.status = UserOrgStatus::Confirmed as i32; user_to_confirm.status = UserOrgStatus::Confirmed as i32;
user_to_confirm.akey = key.to_string(); user_to_confirm.akey = key.to_string();
@ -997,14 +985,26 @@ async fn edit_user(
} }
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner { if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
// Removing owner permmission, check that there are at least another owner // Removing owner permmission, check that there is at least one other confirmed owner
let num_owners = UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).await.len(); if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &conn).await <= 1 {
if num_owners <= 1 {
err!("Can't delete the last owner") err!("Can't delete the last owner")
} }
} }
// This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
// It returns different error messages per function.
if new_type < UserOrgType::Admin {
match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &org_id, true, &conn).await {
Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => {
err!("You cannot modify this user to this type because it has no two-step login method activated");
}
Err(OrgPolicyErr::SingleOrgEnforced) => {
err!("You cannot modify this user to this type because it is a member of an organization which forbids it");
}
}
}
user_to_edit.access_all = data.AccessAll; user_to_edit.access_all = data.AccessAll;
user_to_edit.atype = new_type as i32; user_to_edit.atype = new_type as i32;
@ -1083,10 +1083,8 @@ async fn _delete_user(org_id: &str, org_user_id: &str, headers: &AdminHeaders, c
} }
if user_to_delete.atype == UserOrgType::Owner { if user_to_delete.atype == UserOrgType::Owner {
// Removing owner, check that there are at least another owner // Removing owner, check that there is at least one other confirmed owner
let num_owners = UserOrganization::find_by_org_and_type(org_id, UserOrgType::Owner as i32, conn).await.len(); if UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await <= 1 {
if num_owners <= 1 {
err!("Can't delete the last owner") err!("Can't delete the last owner")
} }
} }
@ -1255,7 +1253,7 @@ async fn get_policy(org_id: String, pol_type: i32, _headers: AdminHeaders, conn:
None => err!("Invalid or unsupported policy type"), None => err!("Invalid or unsupported policy type"),
}; };
let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn).await { let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {
Some(p) => p, Some(p) => p,
None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()),
}; };
@ -1283,15 +1281,16 @@ async fn put_policy(
let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { let pol_type_enum = match OrgPolicyType::from_i32(pol_type) {
Some(pt) => pt, Some(pt) => pt,
None => err!("Invalid policy type"), None => err!("Invalid or unsupported policy type"),
}; };
// If enabling the TwoFactorAuthentication policy, remove this org's members that do have 2FA // When enabling the TwoFactorAuthentication policy, remove this org's members that do 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, &conn).await.into_iter() { for member in UserOrganization::find_by_org(&org_id, &conn).await.into_iter() {
let user_twofactor_disabled = TwoFactor::find_by_user(&member.user_uuid, &conn).await.is_empty(); let user_twofactor_disabled = TwoFactor::find_by_user(&member.user_uuid, &conn).await.is_empty();
// Policy only applies to non-Owner/non-Admin members who have accepted joining the org // 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 if user_twofactor_disabled
&& member.atype < UserOrgType::Admin && member.atype < UserOrgType::Admin
&& member.status != UserOrgStatus::Invited as i32 && member.status != UserOrgStatus::Invited as i32
@ -1307,33 +1306,29 @@ async fn put_policy(
} }
} }
// If 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
if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled { if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled {
for member in UserOrganization::find_by_org(&org_id, &conn).await.into_iter() { for member in UserOrganization::find_by_org(&org_id, &conn).await.into_iter() {
// Policy only applies to non-Owner/non-Admin members who have accepted joining the org // Policy only applies to non-Owner/non-Admin members who have accepted joining the org
if member.atype < UserOrgType::Admin && member.status != UserOrgStatus::Invited as i32 { // Exclude invited and revoked users when checking for this policy.
let is_member_of_another_org = UserOrganization::find_any_state_by_user(&member.user_uuid, &conn) // Those users will not be allowed to accept or be activated because of the policy checks done there.
.await // We check if the count is larger then 1, because it includes this organization also.
.into_iter() if member.atype < UserOrgType::Admin
// Other UserOrganization's where they have accepted being a member of && member.status != UserOrgStatus::Invited as i32
.filter(|uo| uo.uuid != member.uuid && uo.status != UserOrgStatus::Invited as i32) && UserOrganization::count_accepted_and_confirmed_by_user(&member.user_uuid, &conn).await > 1
.count() {
> 1; if CONFIG.mail_enabled() {
let org = Organization::find_by_uuid(&member.org_uuid, &conn).await.unwrap();
let user = User::find_by_uuid(&member.user_uuid, &conn).await.unwrap();
if is_member_of_another_org { mail::send_single_org_removed_from_org(&user.email, &org.name).await?;
if CONFIG.mail_enabled() {
let org = Organization::find_by_uuid(&member.org_uuid, &conn).await.unwrap();
let user = User::find_by_uuid(&member.user_uuid, &conn).await.unwrap();
mail::send_single_org_removed_from_org(&user.email, &org.name).await?;
}
member.delete(&conn).await?;
} }
member.delete(&conn).await?;
} }
} }
} }
let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn).await { let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {
Some(p) => p, Some(p) => p,
None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()),
}; };
@ -1473,7 +1468,7 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header
// If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
if data.OverwriteExisting { if data.OverwriteExisting {
for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User as i32, &conn).await { for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User, &conn).await {
if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &conn).await.map(|u| u.email) { if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &conn).await.map(|u| u.email) {
if !data.Users.iter().any(|u| u.Email == user_email) { if !data.Users.iter().any(|u| u.Email == user_email) {
user_org.delete(&conn).await?; user_org.delete(&conn).await?;
@ -1484,3 +1479,166 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header
Ok(()) Ok(())
} }
#[put("/organizations/<org_id>/users/<org_user_id>/deactivate")]
async fn deactivate_organization_user(
org_id: String,
org_user_id: String,
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
_deactivate_organization_user(&org_id, &org_user_id, &headers, &conn).await
}
#[put("/organizations/<org_id>/users/deactivate", data = "<data>")]
async fn bulk_deactivate_organization_user(
org_id: String,
data: JsonUpcase<Value>,
headers: AdminHeaders,
conn: DbConn,
) -> Json<Value> {
let data = data.into_inner().data;
let mut bulk_response = Vec::new();
match data["Ids"].as_array() {
Some(org_users) => {
for org_user_id in org_users {
let org_user_id = org_user_id.as_str().unwrap_or_default();
let err_msg = match _deactivate_organization_user(&org_id, org_user_id, &headers, &conn).await {
Ok(_) => String::from(""),
Err(e) => format!("{:?}", e),
};
bulk_response.push(json!(
{
"Object": "OrganizationUserBulkResponseModel",
"Id": org_user_id,
"Error": err_msg
}
));
}
}
None => error!("No users to revoke"),
}
Json(json!({
"Data": bulk_response,
"Object": "list",
"ContinuationToken": null
}))
}
async fn _deactivate_organization_user(
org_id: &str,
org_user_id: &str,
headers: &AdminHeaders,
conn: &DbConn,
) -> EmptyResult {
match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await {
Some(mut user_org) if user_org.status > UserOrgStatus::Revoked as i32 => {
if user_org.user_uuid == headers.user.uuid {
err!("You cannot revoke yourself")
}
if user_org.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner {
err!("Only owners can revoke other owners")
}
if user_org.atype == UserOrgType::Owner
&& UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await <= 1
{
err!("Organization must have at least one confirmed owner")
}
user_org.revoke();
user_org.save(conn).await?;
}
Some(_) => err!("User is already revoked"),
None => err!("User not found in organization"),
}
Ok(())
}
#[put("/organizations/<org_id>/users/<org_user_id>/activate")]
async fn activate_organization_user(
org_id: String,
org_user_id: String,
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
_activate_organization_user(&org_id, &org_user_id, &headers, &conn).await
}
#[put("/organizations/<org_id>/users/activate", data = "<data>")]
async fn bulk_activate_organization_user(
org_id: String,
data: JsonUpcase<Value>,
headers: AdminHeaders,
conn: DbConn,
) -> Json<Value> {
let data = data.into_inner().data;
let mut bulk_response = Vec::new();
match data["Ids"].as_array() {
Some(org_users) => {
for org_user_id in org_users {
let org_user_id = org_user_id.as_str().unwrap_or_default();
let err_msg = match _activate_organization_user(&org_id, org_user_id, &headers, &conn).await {
Ok(_) => String::from(""),
Err(e) => format!("{:?}", e),
};
bulk_response.push(json!(
{
"Object": "OrganizationUserBulkResponseModel",
"Id": org_user_id,
"Error": err_msg
}
));
}
}
None => error!("No users to restore"),
}
Json(json!({
"Data": bulk_response,
"Object": "list",
"ContinuationToken": null
}))
}
async fn _activate_organization_user(
org_id: &str,
org_user_id: &str,
headers: &AdminHeaders,
conn: &DbConn,
) -> EmptyResult {
match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await {
Some(mut user_org) if user_org.status < UserOrgStatus::Accepted as i32 => {
if user_org.user_uuid == headers.user.uuid {
err!("You cannot restore yourself")
}
if user_org.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner {
err!("Only owners can restore other owners")
}
// This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
// It returns different error messages per function.
if user_org.atype < UserOrgType::Admin {
match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, conn).await {
Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => {
err!("You cannot restore this user because it has no two-step login method activated");
}
Err(OrgPolicyErr::SingleOrgEnforced) => {
err!("You cannot restore this user because it is a member of an organization which forbids it");
}
}
}
user_org.activate();
user_org.save(conn).await?;
}
Some(_) => err!("User is already active"),
None => err!("User not found in organization"),
}
Ok(())
}

Datei anzeigen

@ -70,8 +70,9 @@ struct SendData {
/// controls this policy globally. /// controls this policy globally.
async fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult { async fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult {
let user_uuid = &headers.user.uuid; let user_uuid = &headers.user.uuid;
let policy_type = OrgPolicyType::DisableSend; if !CONFIG.sends_allowed()
if !CONFIG.sends_allowed() || OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn).await { || OrgPolicy::is_applicable_to_user(user_uuid, OrgPolicyType::DisableSend, None, conn).await
{
err!("Due to an Enterprise Policy, you are only able to delete an existing Send.") err!("Due to an Enterprise Policy, you are only able to delete an existing Send.")
} }
Ok(()) Ok(())

Datei anzeigen

@ -19,7 +19,7 @@ pub use self::device::Device;
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
pub use self::favorite::Favorite; pub use self::favorite::Favorite;
pub use self::folder::{Folder, FolderCipher}; pub use self::folder::{Folder, FolderCipher};
pub use self::org_policy::{OrgPolicy, OrgPolicyType}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
pub use self::send::{Send, SendType}; pub use self::send::{Send, SendType};
pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor::{TwoFactor, TwoFactorType};

Datei anzeigen

@ -6,7 +6,7 @@ use crate::db::DbConn;
use crate::error::MapResult; use crate::error::MapResult;
use crate::util::UpCase; use crate::util::UpCase;
use super::{UserOrgStatus, UserOrgType, UserOrganization}; use super::{TwoFactor, UserOrgStatus, UserOrgType, UserOrganization};
db_object! { db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -21,25 +21,37 @@ db_object! {
} }
} }
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/PolicyType.cs
#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)] #[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]
pub enum OrgPolicyType { pub enum OrgPolicyType {
TwoFactorAuthentication = 0, TwoFactorAuthentication = 0,
MasterPassword = 1, MasterPassword = 1,
PasswordGenerator = 2, PasswordGenerator = 2,
SingleOrg = 3, SingleOrg = 3,
// RequireSso = 4, // Not currently supported. // RequireSso = 4, // Not supported
PersonalOwnership = 5, PersonalOwnership = 5,
DisableSend = 6, DisableSend = 6,
SendOptions = 7, SendOptions = 7,
// ResetPassword = 8, // Not supported
// MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)
// DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed)
} }
// https://github.com/bitwarden/server/blob/master/src/Core/Models/Data/SendOptionsPolicyData.cs // https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub struct SendOptionsPolicyData { pub struct SendOptionsPolicyData {
pub DisableHideEmail: bool, pub DisableHideEmail: bool,
} }
pub type OrgPolicyResult = Result<(), OrgPolicyErr>;
#[derive(Debug)]
pub enum OrgPolicyErr {
TwoFactorMissing,
SingleOrgEnforced,
}
/// Local methods /// Local methods
impl OrgPolicy { impl OrgPolicy {
pub fn new(org_uuid: String, atype: OrgPolicyType, data: String) -> Self { pub fn new(org_uuid: String, atype: OrgPolicyType, data: String) -> Self {
@ -160,11 +172,11 @@ impl OrgPolicy {
}} }}
} }
pub async fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> { pub async fn find_by_org_and_type(org_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> Option<Self> {
db_run! { conn: { db_run! { conn: {
org_policies::table org_policies::table
.filter(org_policies::org_uuid.eq(org_uuid)) .filter(org_policies::org_uuid.eq(org_uuid))
.filter(org_policies::atype.eq(atype)) .filter(org_policies::atype.eq(policy_type as i32))
.first::<OrgPolicyDb>(conn) .first::<OrgPolicyDb>(conn)
.ok() .ok()
.from_db() .from_db()
@ -179,40 +191,128 @@ impl OrgPolicy {
}} }}
} }
pub async fn find_accepted_and_confirmed_by_user_and_active_policy(
user_uuid: &str,
policy_type: OrgPolicyType,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
org_policies::table
.inner_join(
users_organizations::table.on(
users_organizations::org_uuid.eq(org_policies::org_uuid)
.and(users_organizations::user_uuid.eq(user_uuid)))
)
.filter(
users_organizations::status.eq(UserOrgStatus::Accepted as i32)
)
.or_filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
)
.filter(org_policies::atype.eq(policy_type as i32))
.filter(org_policies::enabled.eq(true))
.select(org_policies::all_columns)
.load::<OrgPolicyDb>(conn)
.expect("Error loading org_policy")
.from_db()
}}
}
pub async fn find_confirmed_by_user_and_active_policy(
user_uuid: &str,
policy_type: OrgPolicyType,
conn: &DbConn,
) -> Vec<Self> {
db_run! { conn: {
org_policies::table
.inner_join(
users_organizations::table.on(
users_organizations::org_uuid.eq(org_policies::org_uuid)
.and(users_organizations::user_uuid.eq(user_uuid)))
)
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
)
.filter(org_policies::atype.eq(policy_type as i32))
.filter(org_policies::enabled.eq(true))
.select(org_policies::all_columns)
.load::<OrgPolicyDb>(conn)
.expect("Error loading org_policy")
.from_db()
}}
}
/// Returns true if the user belongs to an org that has enabled the specified policy type, /// Returns true if the user belongs to an org that has enabled the specified policy type,
/// and the user is not an owner or admin of that org. This is only useful for checking /// and the user is not an owner or admin of that org. This is only useful for checking
/// applicability of policy types that have these particular semantics. /// applicability of policy types that have these particular semantics.
pub async fn is_applicable_to_user(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> bool { pub async fn is_applicable_to_user(
// TODO: Should check confirmed and accepted users user_uuid: &str,
for policy in OrgPolicy::find_confirmed_by_user(user_uuid, conn).await { policy_type: OrgPolicyType,
if policy.enabled && policy.has_type(policy_type) { exclude_org_uuid: Option<&str>,
let org_uuid = &policy.org_uuid; conn: &DbConn,
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await { ) -> bool {
if user.atype < UserOrgType::Admin { for policy in
return true; OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await
} {
// Check if we need to skip this organization.
if exclude_org_uuid.is_some() && exclude_org_uuid.unwrap() == policy.org_uuid {
continue;
}
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
if user.atype < UserOrgType::Admin {
return true;
} }
} }
} }
false false
} }
pub async fn is_user_allowed(
user_uuid: &str,
org_uuid: &str,
exclude_current_org: bool,
conn: &DbConn,
) -> OrgPolicyResult {
// Enforce TwoFactor/TwoStep login
if TwoFactor::find_by_user(user_uuid, conn).await.is_empty() {
match Self::find_by_org_and_type(org_uuid, OrgPolicyType::TwoFactorAuthentication, conn).await {
Some(p) if p.enabled => {
return Err(OrgPolicyErr::TwoFactorMissing);
}
_ => {}
};
}
// Enforce Single Organization Policy of other organizations user is a member of
// This check here needs to exclude this current org-id, else an accepted user can not be confirmed.
let exclude_org = if exclude_current_org {
Some(org_uuid)
} else {
None
};
if Self::is_applicable_to_user(user_uuid, OrgPolicyType::SingleOrg, exclude_org, conn).await {
return Err(OrgPolicyErr::SingleOrgEnforced);
}
Ok(())
}
/// Returns true if the user belongs to an org that has enabled the `DisableHideEmail` /// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`
/// option of the `Send Options` policy, and the user is not an owner or admin of that org. /// option of the `Send Options` policy, and the user is not an owner or admin of that org.
pub async fn is_hide_email_disabled(user_uuid: &str, conn: &DbConn) -> bool { pub async fn is_hide_email_disabled(user_uuid: &str, conn: &DbConn) -> bool {
for policy in OrgPolicy::find_confirmed_by_user(user_uuid, conn).await { for policy in
if policy.enabled && policy.has_type(OrgPolicyType::SendOptions) { OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
let org_uuid = &policy.org_uuid; {
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await { if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
if user.atype < UserOrgType::Admin { if user.atype < UserOrgType::Admin {
match serde_json::from_str::<UpCase<SendOptionsPolicyData>>(&policy.data) { match serde_json::from_str::<UpCase<SendOptionsPolicyData>>(&policy.data) {
Ok(opts) => { Ok(opts) => {
if opts.data.DisableHideEmail { if opts.data.DisableHideEmail {
return true; return true;
}
} }
_ => error!("Failed to deserialize policy data: {}", policy.data),
} }
_ => error!("Failed to deserialize SendOptionsPolicyData: {}", policy.data),
} }
} }
} }

Datei anzeigen

@ -31,7 +31,9 @@ db_object! {
} }
} }
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
pub enum UserOrgStatus { pub enum UserOrgStatus {
Revoked = -1,
Invited = 0, Invited = 0,
Accepted = 1, Accepted = 1,
Confirmed = 2, Confirmed = 2,
@ -133,26 +135,29 @@ impl Organization {
public_key, public_key,
} }
} }
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs
pub fn to_json(&self) -> Value { pub fn to_json(&self) -> Value {
json!({ json!({
"Id": self.uuid, "Id": self.uuid,
"Identifier": null, // not supported by us "Identifier": null, // not supported by us
"Name": self.name, "Name": self.name,
"Seats": 10, // The value doesn't matter, we don't check server-side "Seats": 10, // The value doesn't matter, we don't check server-side
// "MaxAutoscaleSeats": null, // The value doesn't matter, we don't check server-side
"MaxCollections": 10, // The value doesn't matter, we don't check server-side "MaxCollections": 10, // The value doesn't matter, we don't check server-side
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
"Use2fa": true, "Use2fa": true,
"UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet) "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
"UseEvents": false, // not supported by us "UseEvents": false, // Not supported
"UseGroups": false, // not supported by us "UseGroups": false, // Not supported
"UseTotp": true, "UseTotp": true,
"UsePolicies": true, "UsePolicies": true,
"UseSso": false, // We do not support SSO // "UseScim": false, // Not supported (Not AGPLv3 Licensed)
"UseSso": false, // Not supported
// "UseKeyConnector": false, // Not supported
"SelfHost": true, "SelfHost": true,
"UseApi": false, // not supported by us "UseApi": false, // Not supported
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
"ResetPasswordEnrolled": false, // not supported by us "UseResetPassword": false, // Not supported
"BusinessName": null, "BusinessName": null,
"BusinessAddress1": null, "BusinessAddress1": null,
@ -170,6 +175,12 @@ impl Organization {
} }
} }
// Used to either subtract or add to the current status
// The number 128 should be fine, it is well within the range of an i32
// The same goes for the database where we only use INTEGER (the same as an i32)
// It should also provide enough room for 100+ types, which i doubt will ever happen.
static ACTIVATE_REVOKE_DIFF: i32 = 128;
impl UserOrganization { impl UserOrganization {
pub fn new(user_uuid: String, org_uuid: String) -> Self { pub fn new(user_uuid: String, org_uuid: String) -> Self {
Self { Self {
@ -184,6 +195,18 @@ impl UserOrganization {
atype: UserOrgType::User as i32, atype: UserOrgType::User as i32,
} }
} }
pub fn activate(&mut self) {
if self.status < UserOrgStatus::Accepted as i32 {
self.status += ACTIVATE_REVOKE_DIFF;
}
}
pub fn revoke(&mut self) {
if self.status > UserOrgStatus::Revoked as i32 {
self.status -= ACTIVATE_REVOKE_DIFF;
}
}
} }
use crate::db::DbConn; use crate::db::DbConn;
@ -265,9 +288,10 @@ impl UserOrganization {
pub async fn to_json(&self, conn: &DbConn) -> Value { pub async fn to_json(&self, conn: &DbConn) -> Value {
let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap(); let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap();
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
json!({ json!({
"Id": self.org_uuid, "Id": self.org_uuid,
"Identifier": null, // not supported by us "Identifier": null, // Not supported
"Name": org.name, "Name": org.name,
"Seats": 10, // The value doesn't matter, we don't check server-side "Seats": 10, // The value doesn't matter, we don't check server-side
"MaxCollections": 10, // The value doesn't matter, we don't check server-side "MaxCollections": 10, // The value doesn't matter, we don't check server-side
@ -275,44 +299,48 @@ impl UserOrganization {
"Use2fa": true, "Use2fa": true,
"UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet) "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
"UseEvents": false, // not supported by us "UseEvents": false, // Not supported
"UseGroups": false, // not supported by us "UseGroups": false, // Not supported
"UseTotp": true, "UseTotp": true,
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
"UsePolicies": true, "UsePolicies": true,
"UseApi": false, // not supported by us "UseApi": false, // Not supported
"SelfHost": true, "SelfHost": true,
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(), "HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
"ResetPasswordEnrolled": false, // not supported by us "ResetPasswordEnrolled": false, // Not supported
"SsoBound": false, // We do not support SSO "SsoBound": false, // Not supported
"UseSso": false, // We do not support SSO "UseSso": false, // Not supported
// TODO: Add support for Business Portal
// Upstream is moving Policies and SSO management outside of the web-vault to /portal
// For now they still have that code also in the web-vault, but they will remove it at some point.
// https://github.com/bitwarden/server/tree/master/bitwarden_license/src/
"UseBusinessPortal": false, // Disable BusinessPortal Button
"ProviderId": null, "ProviderId": null,
"ProviderName": null, "ProviderName": null,
// "KeyConnectorEnabled": false,
// "KeyConnectorUrl": null,
// TODO: Add support for Custom User Roles // TODO: Add support for Custom User Roles
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
// "Permissions": { // "Permissions": {
// "AccessBusinessPortal": false, // "AccessEventLogs": false, // Not supported
// "AccessEventLogs": false,
// "AccessImportExport": false, // "AccessImportExport": false,
// "AccessReports": false, // "AccessReports": false,
// "ManageAllCollections": false, // "ManageAllCollections": false,
// "CreateNewCollections": false,
// "EditAnyCollection": false,
// "DeleteAnyCollection": false,
// "ManageAssignedCollections": false, // "ManageAssignedCollections": false,
// "editAssignedCollections": false,
// "deleteAssignedCollections": false,
// "ManageCiphers": false, // "ManageCiphers": false,
// "ManageGroups": false, // "ManageGroups": false, // Not supported
// "ManagePolicies": false, // "ManagePolicies": false,
// "ManageResetPassword": false, // "ManageResetPassword": false, // Not supported
// "ManageSso": false, // "ManageSso": false, // Not supported
// "ManageUsers": false, // "ManageUsers": false,
// "ManageScim": false, // Not supported (Not AGPLv3 Licensed)
// }, // },
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
// These are per user // These are per user
"UserId": self.user_uuid,
"Key": self.akey, "Key": self.akey,
"Status": self.status, "Status": self.status,
"Type": self.atype, "Type": self.atype,
@ -325,13 +353,21 @@ impl UserOrganization {
pub async fn to_json_user_details(&self, conn: &DbConn) -> Value { pub async fn to_json_user_details(&self, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap(); let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
// We subtract/add a number so we can restore/activate the user to it's previouse state again.
let status = if self.status < UserOrgStatus::Revoked as i32 {
UserOrgStatus::Revoked as i32
} else {
self.status
};
json!({ json!({
"Id": self.uuid, "Id": self.uuid,
"UserId": self.user_uuid, "UserId": self.user_uuid,
"Name": user.name, "Name": user.name,
"Email": user.email, "Email": user.email,
"Status": self.status, "Status": status,
"Type": self.atype, "Type": self.atype,
"AccessAll": self.access_all, "AccessAll": self.access_all,
@ -365,11 +401,19 @@ impl UserOrganization {
.collect() .collect()
}; };
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
// We subtract/add a number so we can restore/activate the user to it's previouse state again.
let status = if self.status < UserOrgStatus::Revoked as i32 {
UserOrgStatus::Revoked as i32
} else {
self.status
};
json!({ json!({
"Id": self.uuid, "Id": self.uuid,
"UserId": self.user_uuid, "UserId": self.user_uuid,
"Status": self.status, "Status": status,
"Type": self.atype, "Type": self.atype,
"AccessAll": self.access_all, "AccessAll": self.access_all,
"Collections": coll_uuids, "Collections": coll_uuids,
@ -507,6 +551,18 @@ impl UserOrganization {
}} }}
} }
pub async fn count_accepted_and_confirmed_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Accepted as i32))
.or_filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.count()
.first::<i64>(conn)
.unwrap_or(0)
}}
}
pub async fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { pub async fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
db_run! { conn: { db_run! { conn: {
users_organizations::table users_organizations::table
@ -527,16 +583,28 @@ impl UserOrganization {
}} }}
} }
pub async fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Vec<Self> { pub async fn find_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &DbConn) -> Vec<Self> {
db_run! { conn: { db_run! { conn: {
users_organizations::table users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::atype.eq(atype)) .filter(users_organizations::atype.eq(atype as i32))
.load::<UserOrganizationDb>(conn) .load::<UserOrganizationDb>(conn)
.expect("Error loading user organizations").from_db() .expect("Error loading user organizations").from_db()
}} }}
} }
pub async fn count_confirmed_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &DbConn) -> i64 {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::atype.eq(atype as i32))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.count()
.first::<i64>(conn)
.unwrap_or(0)
}}
}
pub async fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { pub async fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: { db_run! { conn: {
users_organizations::table users_organizations::table

Datei anzeigen

@ -275,11 +275,11 @@ impl User {
pub async fn delete(self, conn: &DbConn) -> EmptyResult { pub async fn delete(self, conn: &DbConn) -> EmptyResult {
for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await { for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
if user_org.atype == UserOrgType::Owner { if user_org.atype == UserOrgType::Owner
let owner_type = UserOrgType::Owner as i32; && UserOrganization::count_confirmed_by_org_and_type(&user_org.org_uuid, UserOrgType::Owner, conn).await
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, conn).await.len() <= 1 { <= 1
err!("Can't delete last owner") {
} err!("Can't delete last owner")
} }
} }