1
0
Fork 1
Spiegel von https://github.com/dani-garcia/vaultwarden.git synchronisiert 2024-11-26 05:50:29 +01:00

Merge branch 'BlackDex-org-user-revoke-access' into main

Dieser Commit ist enthalten in:
Daniel García 2022-09-04 23:04:22 +02:00
Commit 518d74ce21
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: FC8A7D14C3CD543A
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,14 +176,11 @@ 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(); {
if num_owners <= 1 {
err!("The last owner can't leave") 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,44 +775,18 @@ 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");
};
if org_twofactor_policy_enabled && user_twofactor_disabled {
err!("You cannot join this organization until you enable two-step login on your user account.")
} }
Err(OrgPolicyErr::SingleOrgEnforced) => {
// Enforce Single Organization Policy of organization user is trying to join err!("You cannot join this organization because you are a member of an organization which forbids it");
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;
@ -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,20 +1306,17 @@ 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 is_member_of_another_org {
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
let org = Organization::find_by_uuid(&member.org_uuid, &conn).await.unwrap(); let org = Organization::find_by_uuid(&member.org_uuid, &conn).await.unwrap();
let user = User::find_by_uuid(&member.user_uuid, &conn).await.unwrap(); let user = User::find_by_uuid(&member.user_uuid, &conn).await.unwrap();
@ -1331,9 +1327,8 @@ async fn put_policy(
} }
} }
} }
}
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,31 +191,120 @@ 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 {
for policy in
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 { if user.atype < UserOrgType::Admin {
return true; 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) => {
@ -211,8 +312,7 @@ impl OrgPolicy {
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,13 +275,13 @@ 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")
} }
} }
}
Send::delete_all_by_user(&self.uuid, conn).await?; Send::delete_all_by_user(&self.uuid, conn).await?;
EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?; EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?;