use num_traits::FromPrimitive; use rocket::serde::json::Json; use rocket::Route; use serde_json::Value; use crate::{ api::{ core::{log_event, CipherSyncData, CipherSyncType}, ApiResult, EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordData, UpdateType, }, auth::{decode_invite, AdminHeaders, ClientIp, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, db::{models::*, DbConn}, error::Error, mail, util::convert_json_key_lcase_first, CONFIG, }; pub fn routes() -> Vec { routes![ get_organization, create_organization, delete_organization, post_delete_organization, leave_organization, get_user_collections, get_org_collections, get_org_collection_detail, get_collection_users, put_collection_users, put_organization, post_organization, post_organization_collections, delete_organization_collection_user, post_organization_collection_delete_user, post_organization_collection_update, put_organization_collection_update, delete_organization_collection, post_organization_collection_delete, get_org_details, get_org_users, send_invite, reinvite_user, bulk_reinvite_user, confirm_invite, bulk_confirm_invite, accept_invite, get_user, edit_user, put_organization_user, delete_user, bulk_delete_user, post_delete_user, post_org_import, list_policies, list_policies_token, get_policy, put_policy, get_organization_tax, get_plans, get_plans_tax_rates, import, post_org_keys, get_organization_keys, bulk_public_keys, deactivate_organization_user, bulk_deactivate_organization_user, revoke_organization_user, bulk_revoke_organization_user, activate_organization_user, bulk_activate_organization_user, restore_organization_user, bulk_restore_organization_user, get_groups, post_groups, get_group, put_group, post_group, get_group_details, delete_group, post_delete_group, get_group_users, put_group_users, get_user_groups, post_user_groups, put_user_groups, delete_group_user, post_delete_group_user, put_reset_password_enrollment, get_reset_password_details, put_reset_password, get_org_export ] } #[derive(Deserialize)] #[allow(non_snake_case)] struct OrgData { BillingEmail: String, CollectionName: String, Key: String, Name: String, Keys: Option, #[serde(rename = "PlanType")] _PlanType: NumberOrString, // Ignored, always use the same plan } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] struct OrganizationUpdateData { BillingEmail: String, Name: String, } #[derive(Deserialize)] #[allow(non_snake_case)] struct NewCollectionData { Name: String, Groups: Vec, } #[derive(Deserialize)] #[allow(non_snake_case)] struct NewCollectionGroupData { HidePasswords: bool, Id: String, ReadOnly: bool, } #[derive(Deserialize)] #[allow(non_snake_case)] struct OrgKeyData { EncryptedPrivateKey: String, PublicKey: String, } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] struct OrgBulkIds { Ids: Vec, } #[post("/organizations", data = "")] async fn create_organization(headers: Headers, data: JsonUpcase, mut conn: DbConn) -> JsonResult { if !CONFIG.is_org_creation_allowed(&headers.user.email) { err!("User not allowed to create organizations") } if OrgPolicy::is_applicable_to_user(&headers.user.uuid, OrgPolicyType::SingleOrg, None, &mut conn).await { 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." ) } let data: OrgData = data.into_inner().data; let (private_key, public_key) = if data.Keys.is_some() { let keys: OrgKeyData = data.Keys.unwrap(); (Some(keys.EncryptedPrivateKey), Some(keys.PublicKey)) } else { (None, None) }; let org = Organization::new(data.Name, data.BillingEmail, private_key, public_key); let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone()); let collection = Collection::new(org.uuid.clone(), data.CollectionName); user_org.akey = data.Key; user_org.access_all = true; user_org.atype = UserOrgType::Owner as i32; user_org.status = UserOrgStatus::Confirmed as i32; org.save(&mut conn).await?; user_org.save(&mut conn).await?; collection.save(&mut conn).await?; Ok(Json(org.to_json())) } #[delete("/organizations/", data = "")] async fn delete_organization( org_id: String, data: JsonUpcase, headers: OwnerHeaders, mut conn: DbConn, ) -> EmptyResult { let data: PasswordData = data.into_inner().data; let password_hash = data.MasterPasswordHash; if !headers.user.check_valid_password(&password_hash) { err!("Invalid password") } match Organization::find_by_uuid(&org_id, &mut conn).await { None => err!("Organization not found"), Some(org) => org.delete(&mut conn).await, } } #[post("/organizations//delete", data = "")] async fn post_delete_organization( org_id: String, data: JsonUpcase, headers: OwnerHeaders, conn: DbConn, ) -> EmptyResult { delete_organization(org_id, data, headers, conn).await } #[post("/organizations//leave")] async fn leave_organization(org_id: String, headers: Headers, mut conn: DbConn, ip: ClientIp) -> EmptyResult { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { None => err!("User not part of organization"), Some(user_org) => { if user_org.atype == UserOrgType::Owner && UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await <= 1 { err!("The last owner can't leave") } log_event( EventType::OrganizationUserRemoved as i32, &user_org.uuid, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; user_org.delete(&mut conn).await } } } #[get("/organizations/")] async fn get_organization(org_id: String, _headers: OwnerHeaders, mut conn: DbConn) -> JsonResult { match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => Ok(Json(organization.to_json())), None => err!("Can't find organization details"), } } #[put("/organizations/", data = "")] async fn put_organization( org_id: String, headers: OwnerHeaders, data: JsonUpcase, conn: DbConn, ip: ClientIp, ) -> JsonResult { post_organization(org_id, headers, data, conn, ip).await } #[post("/organizations/", data = "")] async fn post_organization( org_id: String, headers: OwnerHeaders, data: JsonUpcase, mut conn: DbConn, ip: ClientIp, ) -> JsonResult { let data: OrganizationUpdateData = data.into_inner().data; let mut org = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => organization, None => err!("Can't find organization details"), }; org.name = data.Name; org.billing_email = data.BillingEmail; org.save(&mut conn).await?; log_event( EventType::OrganizationUpdated as i32, &org_id, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; Ok(Json(org.to_json())) } // GET /api/collections?writeOnly=false #[get("/collections")] async fn get_user_collections(headers: Headers, mut conn: DbConn) -> Json { Json(json!({ "Data": Collection::find_by_user_uuid(headers.user.uuid.clone(), &mut conn).await .iter() .map(Collection::to_json) .collect::(), "Object": "list", "ContinuationToken": null, })) } #[get("/organizations//collections")] async fn get_org_collections(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { Json(json!({ "Data": _get_org_collections(&org_id, &mut conn).await, "Object": "list", "ContinuationToken": null, })) } async fn _get_org_collections(org_id: &str, conn: &mut DbConn) -> Value { Collection::find_by_organization(org_id, conn).await.iter().map(Collection::to_json).collect::() } #[post("/organizations//collections", data = "")] async fn post_organization_collections( org_id: String, headers: ManagerHeadersLoose, data: JsonUpcase, mut conn: DbConn, ip: ClientIp, ) -> JsonResult { let data: NewCollectionData = data.into_inner().data; let org = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => organization, None => err!("Can't find organization details"), }; // Get the user_organization record so that we can check if the user has access to all collections. let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { Some(u) => u, None => err!("User is not part of organization"), }; let collection = Collection::new(org.uuid, data.Name); collection.save(&mut conn).await?; log_event( EventType::CollectionCreated as i32, &collection.uuid, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; for group in data.Groups { CollectionGroup::new(collection.uuid.clone(), group.Id, group.ReadOnly, group.HidePasswords) .save(&mut conn) .await?; } // If the user doesn't have access to all collections, only in case of a Manger, // then we need to save the creating user uuid (Manager) to the users_collection table. // Else the user will not have access to his own created collection. if !user_org.access_all { CollectionUser::save(&headers.user.uuid, &collection.uuid, false, false, &mut conn).await?; } Ok(Json(collection.to_json())) } #[put("/organizations//collections/", data = "")] async fn put_organization_collection_update( org_id: String, col_id: String, headers: ManagerHeaders, data: JsonUpcase, conn: DbConn, ip: ClientIp, ) -> JsonResult { post_organization_collection_update(org_id, col_id, headers, data, conn, ip).await } #[post("/organizations//collections/", data = "")] async fn post_organization_collection_update( org_id: String, col_id: String, headers: ManagerHeaders, data: JsonUpcase, mut conn: DbConn, ip: ClientIp, ) -> JsonResult { let data: NewCollectionData = data.into_inner().data; let org = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => organization, None => err!("Can't find organization details"), }; let mut collection = match Collection::find_by_uuid(&col_id, &mut conn).await { Some(collection) => collection, None => err!("Collection not found"), }; if collection.org_uuid != org.uuid { err!("Collection is not owned by organization"); } collection.name = data.Name; collection.save(&mut conn).await?; log_event( EventType::CollectionUpdated as i32, &collection.uuid, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?; for group in data.Groups { CollectionGroup::new(col_id.clone(), group.Id, group.ReadOnly, group.HidePasswords).save(&mut conn).await?; } Ok(Json(collection.to_json())) } #[delete("/organizations//collections//user/")] async fn delete_organization_collection_user( org_id: String, col_id: String, org_user_id: String, _headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { let collection = match Collection::find_by_uuid(&col_id, &mut conn).await { None => err!("Collection not found"), Some(collection) => { if collection.org_uuid == org_id { collection } else { err!("Collection and Organization id do not match") } } }; match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &mut conn).await { None => err!("User not found in organization"), Some(user_org) => { match CollectionUser::find_by_collection_and_user(&collection.uuid, &user_org.user_uuid, &mut conn).await { None => err!("User not assigned to collection"), Some(col_user) => col_user.delete(&mut conn).await, } } } } #[post("/organizations//collections//delete-user/")] async fn post_organization_collection_delete_user( org_id: String, col_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { delete_organization_collection_user(org_id, col_id, org_user_id, headers, conn).await } #[delete("/organizations//collections/")] async fn delete_organization_collection( org_id: String, col_id: String, headers: ManagerHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { match Collection::find_by_uuid(&col_id, &mut conn).await { None => err!("Collection not found"), Some(collection) => { if collection.org_uuid == org_id { log_event( EventType::CollectionDeleted as i32, &collection.uuid, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; collection.delete(&mut conn).await } else { err!("Collection and Organization id do not match") } } } } #[derive(Deserialize, Debug)] #[allow(non_snake_case, dead_code)] struct DeleteCollectionData { Id: String, OrgId: String, } #[post("/organizations//collections//delete", data = "<_data>")] async fn post_organization_collection_delete( org_id: String, col_id: String, headers: ManagerHeaders, _data: JsonUpcase, conn: DbConn, ip: ClientIp, ) -> EmptyResult { delete_organization_collection(org_id, col_id, headers, conn, ip).await } #[get("/organizations//collections//details")] async fn get_org_collection_detail( org_id: String, coll_id: String, headers: ManagerHeaders, mut conn: DbConn, ) -> JsonResult { match Collection::find_by_uuid_and_user(&coll_id, headers.user.uuid.clone(), &mut conn).await { None => err!("Collection not found"), Some(collection) => { if collection.org_uuid != org_id { err!("Collection is not owned by organization") } let groups: Vec = CollectionGroup::find_by_collection(&collection.uuid, &mut conn) .await .iter() .map(|collection_group| { SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json() }) .collect(); let mut json_object = collection.to_json(); json_object["Groups"] = json!(groups); json_object["Object"] = json!("collectionGroupDetails"); Ok(Json(json_object)) } } } #[get("/organizations//collections//users")] async fn get_collection_users( org_id: String, coll_id: String, _headers: ManagerHeaders, mut conn: DbConn, ) -> JsonResult { // Get org and collection, check that collection is from org let collection = match Collection::find_by_uuid_and_org(&coll_id, &org_id, &mut conn).await { None => err!("Collection not found in Organization"), Some(collection) => collection, }; let mut user_list = Vec::new(); for col_user in CollectionUser::find_by_collection(&collection.uuid, &mut conn).await { user_list.push( UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &mut conn) .await .unwrap() .to_json_user_access_restrictions(&col_user), ); } Ok(Json(json!(user_list))) } #[put("/organizations//collections//users", data = "")] async fn put_collection_users( org_id: String, coll_id: String, data: JsonUpcaseVec, _headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { // Get org and collection, check that collection is from org if Collection::find_by_uuid_and_org(&coll_id, &org_id, &mut conn).await.is_none() { err!("Collection not found in Organization") } // Delete all the user-collections CollectionUser::delete_all_by_collection(&coll_id, &mut conn).await?; // And then add all the received ones (except if the user has access_all) for d in data.iter().map(|d| &d.data) { let user = match UserOrganization::find_by_uuid(&d.Id, &mut conn).await { Some(u) => u, None => err!("User is not part of organization"), }; if user.access_all { continue; } CollectionUser::save(&user.user_uuid, &coll_id, d.ReadOnly, d.HidePasswords, &mut conn).await?; } Ok(()) } #[derive(FromForm)] struct OrgIdData { #[field(name = "organizationId")] organization_id: String, } #[get("/ciphers/organization-details?")] async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> Json { Json(json!({ "Data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await, "Object": "list", "ContinuationToken": null, })) } async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut DbConn) -> Value { let ciphers = Cipher::find_by_org(org_id, conn).await; let cipher_sync_data = CipherSyncData::new(user_uuid, CipherSyncType::Organization, conn).await; let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { ciphers_json.push(c.to_json(host, user_uuid, Some(&cipher_sync_data), conn).await); } json!(ciphers_json) } #[get("/organizations//users")] async fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { let mut users_json = Vec::new(); for u in UserOrganization::find_by_org(&org_id, &mut conn).await { users_json.push(u.to_json_user_details(&mut conn).await); } Json(json!({ "Data": users_json, "Object": "list", "ContinuationToken": null, })) } #[post("/organizations//keys", data = "")] async fn post_org_keys( org_id: String, data: JsonUpcase, _headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { let data: OrgKeyData = data.into_inner().data; let mut org = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => { if organization.private_key.is_some() && organization.public_key.is_some() { err!("Organization Keys already exist") } organization } None => err!("Can't find organization details"), }; org.private_key = Some(data.EncryptedPrivateKey); org.public_key = Some(data.PublicKey); org.save(&mut conn).await?; Ok(Json(json!({ "Object": "organizationKeys", "PublicKey": org.public_key, "PrivateKey": org.private_key, }))) } #[derive(Deserialize)] #[allow(non_snake_case)] struct CollectionData { Id: String, ReadOnly: bool, HidePasswords: bool, } #[derive(Deserialize)] #[allow(non_snake_case)] struct InviteData { Emails: Vec, Type: NumberOrString, Collections: Option>, AccessAll: Option, } #[post("/organizations//users/invite", data = "")] async fn send_invite( org_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { let data: InviteData = data.into_inner().data; let new_type = match UserOrgType::from_str(&data.Type.into_string()) { Some(new_type) => new_type as i32, None => err!("Invalid type"), }; if new_type != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { err!("Only Owners can invite Managers, Admins or Owners") } for email in data.Emails.iter() { let email = email.to_lowercase(); let mut user_org_status = UserOrgStatus::Invited as i32; let user = match User::find_by_mail(&email, &mut conn).await { None => { if !CONFIG.invitations_allowed() { err!(format!("User does not exist: {email}")) } if !CONFIG.is_email_domain_allowed(&email) { err!("Email domain not eligible for invitations") } if !CONFIG.mail_enabled() { let invitation = Invitation::new(&email); invitation.save(&mut conn).await?; } let mut user = User::new(email.clone()); user.save(&mut conn).await?; user } Some(user) => { if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await.is_some() { err!(format!("User already in organization: {email}")) } else { // automatically accept existing users if mail is disabled if !CONFIG.mail_enabled() && !user.password_hash.is_empty() { user_org_status = UserOrgStatus::Accepted as i32; } user } } }; let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); let access_all = data.AccessAll.unwrap_or(false); new_user.access_all = access_all; new_user.atype = new_type; new_user.status = user_org_status; // If no accessAll, add the collections received if !access_all { for col in data.Collections.iter().flatten() { match Collection::find_by_uuid_and_org(&col.Id, &org_id, &mut conn).await { None => err!("Collection not found in Organization"), Some(collection) => { CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, col.HidePasswords, &mut conn) .await?; } } } } new_user.save(&mut conn).await?; log_event( EventType::OrganizationUserInvited as i32, &new_user.uuid, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; if CONFIG.mail_enabled() { let org_name = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(org) => org.name, None => err!("Error looking up organization"), }; mail::send_invite( &email, &user.uuid, Some(org_id.clone()), Some(new_user.uuid), &org_name, Some(headers.user.email.clone()), ) .await?; } } Ok(()) } #[post("/organizations//users/reinvite", data = "")] async fn bulk_reinvite_user( org_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ) -> Json { let data: OrgBulkIds = data.into_inner().data; let mut bulk_response = Vec::new(); for org_user_id in data.Ids { let err_msg = match _reinvite_user(&org_id, &org_user_id, &headers.user.email, &mut conn).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; bulk_response.push(json!( { "Object": "OrganizationBulkConfirmResponseModel", "Id": org_user_id, "Error": err_msg } )) } Json(json!({ "Data": bulk_response, "Object": "list", "ContinuationToken": null })) } #[post("/organizations//users//reinvite")] async fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { _reinvite_user(&org_id, &user_org, &headers.user.email, &mut conn).await } async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, conn: &mut DbConn) -> EmptyResult { if !CONFIG.invitations_allowed() { err!("Invitations are not allowed.") } if !CONFIG.mail_enabled() { err!("SMTP is not configured.") } let user_org = match UserOrganization::find_by_uuid(user_org, conn).await { Some(user_org) => user_org, None => err!("The user hasn't been invited to the organization."), }; if user_org.status != UserOrgStatus::Invited as i32 { err!("The user is already accepted or confirmed to the organization") } let user = match User::find_by_uuid(&user_org.user_uuid, conn).await { Some(user) => user, None => err!("User not found."), }; let org_name = match Organization::find_by_uuid(org_id, conn).await { Some(org) => org.name, None => err!("Error looking up organization."), }; if CONFIG.mail_enabled() { mail::send_invite( &user.email, &user.uuid, Some(org_id.to_string()), Some(user_org.uuid), &org_name, Some(invited_by_email.to_string()), ) .await?; } else { let invitation = Invitation::new(&user.email); invitation.save(conn).await?; } Ok(()) } #[derive(Deserialize)] #[allow(non_snake_case)] struct AcceptData { Token: String, ResetPasswordKey: Option, } #[post("/organizations//users/<_org_user_id>/accept", data = "")] async fn accept_invite( org_id: String, _org_user_id: String, data: JsonUpcase, mut conn: DbConn, ) -> EmptyResult { // 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 claims = decode_invite(&data.Token)?; match User::find_by_mail(&claims.email, &mut conn).await { Some(_) => { Invitation::take(&claims.email, &mut conn).await; if let (Some(user_org), Some(org)) = (&claims.user_org_id, &claims.org_id) { let mut user_org = match UserOrganization::find_by_uuid_and_org(user_org, org, &mut conn).await { Some(user_org) => user_org, None => err!("Error accepting the invitation"), }; if user_org.status != UserOrgStatus::Invited as i32 { err!("User already accepted the invitation") } let master_password_required = OrgPolicy::org_is_reset_password_auto_enroll(org, &mut conn).await; if data.ResetPasswordKey.is_none() && master_password_required { err!("Reset password key is required, but not provided."); } // 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, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { err!("You cannot join this organization until you enable two-step login on your user account"); } Err(OrgPolicyErr::SingleOrgEnforced) => { err!("You cannot join this organization because you are a member of an organization which forbids it"); } } } user_org.status = UserOrgStatus::Accepted as i32; if master_password_required { user_org.reset_password_key = data.ResetPasswordKey; } user_org.save(&mut conn).await?; } } None => err!("Invited user not found"), } if CONFIG.mail_enabled() { let mut org_name = CONFIG.invitation_org_name(); if let Some(org_id) = &claims.org_id { org_name = match Organization::find_by_uuid(org_id, &mut conn).await { Some(org) => org.name, None => err!("Organization not found."), }; }; if let Some(invited_by_email) = &claims.invited_by_email { // User was invited to an organization, so they must be confirmed manually after acceptance mail::send_invite_accepted(&claims.email, invited_by_email, &org_name).await?; } else { // User was invited from /admin, so they are automatically confirmed mail::send_invite_confirmed(&claims.email, &org_name).await?; } } Ok(()) } #[post("/organizations//users/confirm", data = "")] async fn bulk_confirm_invite( org_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>, ) -> Json { let data = data.into_inner().data; let mut bulk_response = Vec::new(); match data["Keys"].as_array() { Some(keys) => { for invite in keys { let org_user_id = invite["Id"].as_str().unwrap_or_default(); let user_key = invite["Key"].as_str().unwrap_or_default(); let err_msg = match _confirm_invite(&org_id, org_user_id, user_key, &headers, &mut conn, &ip, &nt).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; bulk_response.push(json!( { "Object": "OrganizationBulkConfirmResponseModel", "Id": org_user_id, "Error": err_msg } )); } } None => error!("No keys to confirm"), } Json(json!({ "Data": bulk_response, "Object": "list", "ContinuationToken": null })) } #[post("/organizations//users//confirm", data = "")] async fn confirm_invite( org_id: String, org_user_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { let data = data.into_inner().data; let user_key = data["Key"].as_str().unwrap_or_default(); _confirm_invite(&org_id, &org_user_id, user_key, &headers, &mut conn, &ip, &nt).await } async fn _confirm_invite( org_id: &str, org_user_id: &str, key: &str, headers: &AdminHeaders, conn: &mut DbConn, ip: &ClientIp, nt: &Notify<'_>, ) -> EmptyResult { if key.is_empty() || org_user_id.is_empty() { err!("Key or UserId is not set, unable to process request"); } let mut user_to_confirm = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { Some(user) => user, None => err!("The specified user isn't a member of the organization"), }; if user_to_confirm.atype != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { err!("Only Owners can confirm Managers, Admins or Owners") } if user_to_confirm.status != UserOrgStatus::Accepted as i32 { 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.akey = key.to_string(); log_event( EventType::OrganizationUserConfirmed as i32, &user_to_confirm.uuid, String::from(org_id), headers.user.uuid.clone(), headers.device.atype, &ip.ip, conn, ) .await; if CONFIG.mail_enabled() { let org_name = match Organization::find_by_uuid(org_id, conn).await { Some(org) => org.name, None => err!("Error looking up organization."), }; let address = match User::find_by_uuid(&user_to_confirm.user_uuid, conn).await { Some(user) => user.email, None => err!("Error looking up user."), }; mail::send_invite_confirmed(&address, &org_name).await?; } let save_result = user_to_confirm.save(conn).await; if let Some(user) = User::find_by_uuid(&user_to_confirm.user_uuid, conn).await { nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; } save_result } #[get("/organizations//users/")] async fn get_user(org_id: String, org_user_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { let user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &mut conn).await { Some(user) => user, None => err!("The specified user isn't a member of the organization"), }; Ok(Json(user.to_json_details(&mut conn).await)) } #[derive(Deserialize)] #[allow(non_snake_case)] struct EditUserData { Type: NumberOrString, Collections: Option>, AccessAll: bool, } #[put("/organizations//users/", data = "", rank = 1)] async fn put_organization_user( org_id: String, org_user_id: String, data: JsonUpcase, headers: AdminHeaders, conn: DbConn, ip: ClientIp, ) -> EmptyResult { edit_user(org_id, org_user_id, data, headers, conn, ip).await } #[post("/organizations//users/", data = "", rank = 1)] async fn edit_user( org_id: String, org_user_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { let data: EditUserData = data.into_inner().data; let new_type = match UserOrgType::from_str(&data.Type.into_string()) { Some(new_type) => new_type, None => err!("Invalid type"), }; let mut user_to_edit = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &mut conn).await { Some(user) => user, None => err!("The specified user isn't member of the organization"), }; if new_type != user_to_edit.atype && (user_to_edit.atype >= UserOrgType::Admin || new_type >= UserOrgType::Admin) && headers.org_user_type != UserOrgType::Owner { err!("Only Owners can grant and remove Admin or Owner privileges") } if user_to_edit.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner { err!("Only Owners can edit Owner users") } if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner && user_to_edit.status == UserOrgStatus::Confirmed as i32 { // Removing owner permission, check that there is at least one other confirmed owner if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await <= 1 { 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, &mut 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.atype = new_type as i32; // Delete all the odd collections for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &mut conn).await { c.delete(&mut conn).await?; } // If no accessAll, add the collections received if !data.AccessAll { for col in data.Collections.iter().flatten() { match Collection::find_by_uuid_and_org(&col.Id, &org_id, &mut conn).await { None => err!("Collection not found in Organization"), Some(collection) => { CollectionUser::save( &user_to_edit.user_uuid, &collection.uuid, col.ReadOnly, col.HidePasswords, &mut conn, ) .await?; } } } } log_event( EventType::OrganizationUserUpdated as i32, &user_to_edit.uuid, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; user_to_edit.save(&mut conn).await } #[delete("/organizations//users", data = "")] async fn bulk_delete_user( org_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>, ) -> Json { let data: OrgBulkIds = data.into_inner().data; let mut bulk_response = Vec::new(); for org_user_id in data.Ids { let err_msg = match _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip, &nt).await { Ok(_) => String::new(), Err(e) => format!("{e:?}"), }; bulk_response.push(json!( { "Object": "OrganizationBulkConfirmResponseModel", "Id": org_user_id, "Error": err_msg } )) } Json(json!({ "Data": bulk_response, "Object": "list", "ContinuationToken": null })) } #[delete("/organizations//users/")] async fn delete_user( org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip, &nt).await } #[post("/organizations//users//delete")] async fn post_delete_user( org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip, &nt).await } async fn _delete_user( org_id: &str, org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn, ip: &ClientIp, nt: &Notify<'_>, ) -> EmptyResult { let user_to_delete = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { Some(user) => user, None => err!("User to delete isn't member of the organization"), }; if user_to_delete.atype != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { err!("Only Owners can delete Admins or Owners") } if user_to_delete.atype == UserOrgType::Owner && user_to_delete.status == UserOrgStatus::Confirmed as i32 { // Removing owner, check that there is at least one other confirmed owner if UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await <= 1 { err!("Can't delete the last owner") } } log_event( EventType::OrganizationUserRemoved as i32, &user_to_delete.uuid, String::from(org_id), headers.user.uuid.clone(), headers.device.atype, &ip.ip, conn, ) .await; if let Some(user) = User::find_by_uuid(&user_to_delete.user_uuid, conn).await { nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; } user_to_delete.delete(conn).await } #[post("/organizations//users/public-keys", data = "")] async fn bulk_public_keys( org_id: String, data: JsonUpcase, _headers: AdminHeaders, mut conn: DbConn, ) -> Json { let data: OrgBulkIds = data.into_inner().data; let mut bulk_response = Vec::new(); // Check all received UserOrg UUID's and find the matching User to retreive the public-key. // If the user does not exists, just ignore it, and do not return any information regarding that UserOrg UUID. // The web-vault will then ignore that user for the folowing steps. for user_org_id in data.Ids { match UserOrganization::find_by_uuid_and_org(&user_org_id, &org_id, &mut conn).await { Some(user_org) => match User::find_by_uuid(&user_org.user_uuid, &mut conn).await { Some(user) => bulk_response.push(json!( { "Object": "organizationUserPublicKeyResponseModel", "Id": user_org_id, "UserId": user.uuid, "Key": user.public_key } )), None => debug!("User doesn't exist"), }, None => debug!("UserOrg doesn't exist"), } } Json(json!({ "Data": bulk_response, "Object": "list", "ContinuationToken": null })) } use super::ciphers::update_cipher_from_data; use super::ciphers::CipherData; #[derive(Deserialize)] #[allow(non_snake_case)] struct ImportData { Ciphers: Vec, Collections: Vec, CollectionRelationships: Vec, } #[derive(Deserialize)] #[allow(non_snake_case)] struct RelationsData { // Cipher index Key: usize, // Collection index Value: usize, } #[post("/ciphers/import-organization?", data = "")] async fn post_org_import( query: OrgIdData, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { let data: ImportData = data.into_inner().data; let org_id = query.organization_id; // Validate the import before continuing // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. Cipher::validate_notes(&data.Ciphers)?; let mut collections = Vec::new(); for coll in data.Collections { let collection = Collection::new(org_id.clone(), coll.Name); if collection.save(&mut conn).await.is_err() { collections.push(Err(Error::new("Failed to create Collection", "Failed to create Collection"))); } else { collections.push(Ok(collection)); } } // Read the relations between collections and ciphers let mut relations = Vec::new(); for relation in data.CollectionRelationships { relations.push((relation.Key, relation.Value)); } let headers: Headers = headers.into(); let mut ciphers = Vec::new(); for cipher_data in data.Ciphers { let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None) .await .ok(); ciphers.push(cipher); } // Assign the collections for (cipher_index, coll_index) in relations { let cipher_id = &ciphers[cipher_index].uuid; let coll = &collections[coll_index]; let coll_id = match coll { Ok(coll) => coll.uuid.as_str(), Err(_) => err!("Failed to assign to collection"), }; CollectionCipher::save(cipher_id, coll_id, &mut conn).await?; } let mut user = headers.user; user.update_revision(&mut conn).await } #[get("/organizations//policies")] async fn list_policies(org_id: String, _headers: AdminHeaders, mut conn: DbConn) -> Json { let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await; let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); Json(json!({ "Data": policies_json, "Object": "list", "ContinuationToken": null })) } #[get("/organizations//policies/token?")] async fn list_policies_token(org_id: String, token: String, mut conn: DbConn) -> JsonResult { let invite = crate::auth::decode_invite(&token)?; let invite_org_id = match invite.org_id { Some(invite_org_id) => invite_org_id, None => err!("Invalid token"), }; if invite_org_id != org_id { err!("Token doesn't match request organization"); } // TODO: We receive the invite token as ?token=<>, validate it contains the org id let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await; let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); Ok(Json(json!({ "Data": policies_json, "Object": "list", "ContinuationToken": null }))) } #[get("/organizations//policies/")] async fn get_policy(org_id: String, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { Some(pt) => pt, None => err!("Invalid or unsupported policy type"), }; let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await { Some(p) => p, None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), }; Ok(Json(policy.to_json())) } #[derive(Deserialize)] struct PolicyData { enabled: bool, #[serde(rename = "type")] _type: i32, data: Option, } #[put("/organizations//policies/", data = "")] async fn put_policy( org_id: String, pol_type: i32, data: Json, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> JsonResult { let data: PolicyData = data.into_inner(); let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { Some(pt) => pt, None => err!("Invalid or unsupported policy type"), }; // When enabling the TwoFactorAuthentication policy, remove this org's members that do have 2FA if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { for member in UserOrganization::find_by_org(&org_id, &mut conn).await.into_iter() { let user_twofactor_disabled = TwoFactor::find_by_user(&member.user_uuid, &mut conn).await.is_empty(); // Policy only applies to non-Owner/non-Admin members who have accepted joining the org // Invited users still need to accept the invite and will get an error when they try to accept the invite. if user_twofactor_disabled && member.atype < UserOrgType::Admin && member.status != UserOrgStatus::Invited as i32 { if CONFIG.mail_enabled() { let org = Organization::find_by_uuid(&member.org_uuid, &mut conn).await.unwrap(); let user = User::find_by_uuid(&member.user_uuid, &mut conn).await.unwrap(); mail::send_2fa_removed_from_org(&user.email, &org.name).await?; } log_event( EventType::OrganizationUserRemoved as i32, &member.uuid, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; member.delete(&mut conn).await?; } } } // When enabling the SingleOrg policy, remove this org's members that are members of other orgs if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled { for member in UserOrganization::find_by_org(&org_id, &mut conn).await.into_iter() { // Policy only applies to non-Owner/non-Admin members who have accepted joining the org // Exclude invited and revoked users when checking for this policy. // Those users will not be allowed to accept or be activated because of the policy checks done there. // We check if the count is larger then 1, because it includes this organization also. if member.atype < UserOrgType::Admin && member.status != UserOrgStatus::Invited as i32 && UserOrganization::count_accepted_and_confirmed_by_user(&member.user_uuid, &mut conn).await > 1 { if CONFIG.mail_enabled() { let org = Organization::find_by_uuid(&member.org_uuid, &mut conn).await.unwrap(); let user = User::find_by_uuid(&member.user_uuid, &mut conn).await.unwrap(); mail::send_single_org_removed_from_org(&user.email, &org.name).await?; } log_event( EventType::OrganizationUserRemoved as i32, &member.uuid, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; member.delete(&mut conn).await?; } } } let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await { Some(p) => p, None => OrgPolicy::new(org_id.clone(), pol_type_enum, "{}".to_string()), }; policy.enabled = data.enabled; policy.data = serde_json::to_string(&data.data)?; policy.save(&mut conn).await?; log_event( EventType::PolicyUpdated as i32, &policy.uuid, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; Ok(Json(policy.to_json())) } #[allow(unused_variables)] #[get("/organizations//tax")] fn get_organization_tax(org_id: String, _headers: Headers) -> Json { // Prevent a 404 error, which also causes Javascript errors. // Upstream sends "Only allowed when not self hosted." As an error message. // If we do the same it will also output this to the log, which is overkill. // An empty list/data also works fine. Json(_empty_data_json()) } #[get("/plans")] fn get_plans() -> Json { // Respond with a minimal json just enough to allow the creation of an new organization. Json(json!({ "Object": "list", "Data": [{ "Object": "plan", "Type": 0, "Product": 0, "Name": "Free", "NameLocalizationKey": "planNameFree", "DescriptionLocalizationKey": "planDescFree" }], "ContinuationToken": null })) } #[get("/plans/sales-tax-rates")] fn get_plans_tax_rates(_headers: Headers) -> Json { // Prevent a 404 error, which also causes Javascript errors. Json(_empty_data_json()) } fn _empty_data_json() -> Value { json!({ "Object": "list", "Data": [], "ContinuationToken": null }) } #[derive(Deserialize, Debug)] #[allow(non_snake_case, dead_code)] struct OrgImportGroupData { Name: String, // "GroupName" ExternalId: String, // "cn=GroupName,ou=Groups,dc=example,dc=com" Users: Vec, // ["uid=user,ou=People,dc=example,dc=com"] } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] struct OrgImportUserData { Email: String, // "user@maildomain.net" #[allow(dead_code)] ExternalId: String, // "uid=user,ou=People,dc=example,dc=com" Deleted: bool, } #[derive(Deserialize, Debug)] #[allow(non_snake_case)] struct OrgImportData { #[allow(dead_code)] Groups: Vec, OverwriteExisting: bool, Users: Vec, } #[post("/organizations//import", data = "")] async fn import( org_id: String, data: JsonUpcase, headers: Headers, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { let data = data.into_inner().data; // TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way // to differentiate between auto-imported users and manually added ones. // This means that this endpoint can end up removing users that were added manually by an admin, // as opposed to upstream which only removes auto-imported users. // User needs to be admin or owner to use the Directry Connector match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { Some(user_org) if user_org.atype >= UserOrgType::Admin => { /* Okay, nothing to do */ } Some(_) => err!("User has insufficient permissions to use Directory Connector"), None => err!("User not part of organization"), }; for user_data in &data.Users { if user_data.Deleted { // If user is marked for deletion and it exists, delete it if let Some(user_org) = UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await { log_event( EventType::OrganizationUserRemoved as i32, &user_org.uuid, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; user_org.delete(&mut conn).await?; } // If user is not part of the organization, but it exists } else if UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await.is_none() { if let Some(user) = User::find_by_mail(&user_data.Email, &mut conn).await { let user_org_status = if CONFIG.mail_enabled() { UserOrgStatus::Invited as i32 } else { UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites }; let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); new_org_user.access_all = false; new_org_user.atype = UserOrgType::User as i32; new_org_user.status = user_org_status; new_org_user.save(&mut conn).await?; log_event( EventType::OrganizationUserInvited as i32, &new_org_user.uuid, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; if CONFIG.mail_enabled() { let org_name = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(org) => org.name, None => err!("Error looking up organization"), }; mail::send_invite( &user_data.Email, &user.uuid, Some(org_id.clone()), Some(new_org_user.uuid), &org_name, Some(headers.user.email.clone()), ) .await?; } } } } // 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 { for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User, &mut conn).await { if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.email) { if !data.Users.iter().any(|u| u.Email == user_email) { log_event( EventType::OrganizationUserRemoved as i32, &user_org.uuid, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; user_org.delete(&mut conn).await?; } } } } Ok(()) } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users//deactivate")] async fn deactivate_organization_user( org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/deactivate", data = "")] async fn bulk_deactivate_organization_user( org_id: String, data: JsonUpcase, headers: AdminHeaders, conn: DbConn, ip: ClientIp, ) -> Json { bulk_revoke_organization_user(org_id, data, headers, conn, ip).await } #[put("/organizations//users//revoke")] async fn revoke_organization_user( org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } #[put("/organizations//users/revoke", data = "")] async fn bulk_revoke_organization_user( org_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> Json { 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 _revoke_organization_user(&org_id, org_user_id, &headers, &mut conn, &ip).await { Ok(_) => String::new(), 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 _revoke_organization_user( org_id: &str, org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn, ip: &ClientIp, ) -> 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?; log_event( EventType::OrganizationUserRevoked as i32, &user_org.uuid, org_id.to_string(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, conn, ) .await; } Some(_) => err!("User is already revoked"), None => err!("User not found in organization"), } Ok(()) } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users//activate")] async fn activate_organization_user( org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } // Pre web-vault v2022.9.x endpoint #[put("/organizations//users/activate", data = "")] async fn bulk_activate_organization_user( org_id: String, data: JsonUpcase, headers: AdminHeaders, conn: DbConn, ip: ClientIp, ) -> Json { bulk_restore_organization_user(org_id, data, headers, conn, ip).await } #[put("/organizations//users//restore")] async fn restore_organization_user( org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } #[put("/organizations//users/restore", data = "")] async fn bulk_restore_organization_user( org_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> Json { 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 _restore_organization_user(&org_id, org_user_id, &headers, &mut conn, &ip).await { Ok(_) => String::new(), 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 _restore_organization_user( org_id: &str, org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn, ip: &ClientIp, ) -> 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.restore(); user_org.save(conn).await?; log_event( EventType::OrganizationUserRestored as i32, &user_org.uuid, org_id.to_string(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, conn, ) .await; } Some(_) => err!("User is already active"), None => err!("User not found in organization"), } Ok(()) } #[get("/organizations//groups")] async fn get_groups(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } let groups = Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::(); Ok(Json(json!({ "Data": groups, "Object": "list", "ContinuationToken": null, }))) } #[derive(Deserialize)] #[allow(non_snake_case)] struct GroupRequest { Name: String, AccessAll: Option, ExternalId: Option, Collections: Vec, } impl GroupRequest { pub fn to_group(&self, organizations_uuid: &str) -> ApiResult { match self.AccessAll { Some(access_all_value) => Ok(Group::new( organizations_uuid.to_owned(), self.Name.clone(), access_all_value, self.ExternalId.clone(), )), _ => err!("Could not convert GroupRequest to Group, because AccessAll has no value!"), } } pub fn update_group(&self, mut group: Group) -> ApiResult { match self.AccessAll { Some(access_all_value) => { group.name = self.Name.clone(); group.access_all = access_all_value; group.set_external_id(self.ExternalId.clone()); Ok(group) } _ => err!("Could not update group, because AccessAll has no value!"), } } } #[derive(Deserialize, Serialize)] #[allow(non_snake_case)] struct SelectionReadOnly { Id: String, ReadOnly: bool, HidePasswords: bool, } impl SelectionReadOnly { pub fn to_collection_group(&self, groups_uuid: String) -> CollectionGroup { CollectionGroup::new(self.Id.clone(), groups_uuid, self.ReadOnly, self.HidePasswords) } pub fn to_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly { SelectionReadOnly { Id: collection_group.collections_uuid.clone(), ReadOnly: collection_group.read_only, HidePasswords: collection_group.hide_passwords, } } pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly { SelectionReadOnly { Id: collection_group.groups_uuid.clone(), ReadOnly: collection_group.read_only, HidePasswords: collection_group.hide_passwords, } } pub fn to_json(&self) -> Value { json!(self) } } #[post("/organizations//groups/", data = "")] async fn post_group( org_id: String, group_id: String, data: JsonUpcase, headers: AdminHeaders, conn: DbConn, ip: ClientIp, ) -> JsonResult { put_group(org_id, group_id, data, headers, conn, ip).await } #[post("/organizations//groups", data = "")] async fn post_groups( org_id: String, headers: AdminHeaders, data: JsonUpcase, mut conn: DbConn, ip: ClientIp, ) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } let group_request = data.into_inner().data; let group = group_request.to_group(&org_id)?; log_event( EventType::GroupCreated as i32, &group.uuid, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; add_update_group(group, group_request.Collections, &mut conn).await } #[put("/organizations//groups/", data = "")] async fn put_group( org_id: String, group_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } let group = match Group::find_by_uuid(&group_id, &mut conn).await { Some(group) => group, None => err!("Group not found"), }; let group_request = data.into_inner().data; let updated_group = group_request.update_group(group)?; CollectionGroup::delete_all_by_group(&group_id, &mut conn).await?; log_event( EventType::GroupUpdated as i32, &updated_group.uuid, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; add_update_group(updated_group, group_request.Collections, &mut conn).await } async fn add_update_group(mut group: Group, collections: Vec, conn: &mut DbConn) -> JsonResult { group.save(conn).await?; for selection_read_only_request in collections { let mut collection_group = selection_read_only_request.to_collection_group(group.uuid.clone()); collection_group.save(conn).await?; } Ok(Json(json!({ "Id": group.uuid, "OrganizationId": group.organizations_uuid, "Name": group.name, "AccessAll": group.access_all, "ExternalId": group.get_external_id() }))) } #[get("/organizations/<_org_id>/groups//details")] async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } let group = match Group::find_by_uuid(&group_id, &mut conn).await { Some(group) => group, _ => err!("Group could not be found!"), }; let collections_groups = CollectionGroup::find_by_group(&group_id, &mut conn) .await .iter() .map(|entry| SelectionReadOnly::to_group_details_read_only(entry).to_json()) .collect::(); Ok(Json(json!({ "Id": group.uuid, "OrganizationId": group.organizations_uuid, "Name": group.name, "AccessAll": group.access_all, "ExternalId": group.get_external_id(), "Collections": collections_groups }))) } #[post("/organizations//groups//delete")] async fn post_delete_group( org_id: String, group_id: String, headers: AdminHeaders, conn: DbConn, ip: ClientIp, ) -> EmptyResult { delete_group(org_id, group_id, headers, conn, ip).await } #[delete("/organizations//groups/")] async fn delete_group( org_id: String, group_id: String, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } let group = match Group::find_by_uuid(&group_id, &mut conn).await { Some(group) => group, _ => err!("Group not found"), }; log_event( EventType::GroupDeleted as i32, &group.uuid, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; group.delete(&mut conn).await } #[get("/organizations/<_org_id>/groups/")] async fn get_group(_org_id: String, group_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } let group = match Group::find_by_uuid(&group_id, &mut conn).await { Some(group) => group, _ => err!("Group not found"), }; Ok(Json(group.to_json())) } #[get("/organizations/<_org_id>/groups//users")] async fn get_group_users(_org_id: String, group_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } match Group::find_by_uuid(&group_id, &mut conn).await { Some(_) => { /* Do nothing */ } _ => err!("Group could not be found!"), }; let group_users: Vec = GroupUser::find_by_group(&group_id, &mut conn) .await .iter() .map(|entry| entry.users_organizations_uuid.clone()) .collect(); Ok(Json(json!(group_users))) } #[put("/organizations//groups//users", data = "")] async fn put_group_users( org_id: String, group_id: String, headers: AdminHeaders, data: JsonVec, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } match Group::find_by_uuid(&group_id, &mut conn).await { Some(_) => { /* Do nothing */ } _ => err!("Group could not be found!"), }; GroupUser::delete_all_by_group(&group_id, &mut conn).await?; let assigned_user_ids = data.into_inner(); for assigned_user_id in assigned_user_ids { let mut user_entry = GroupUser::new(group_id.clone(), assigned_user_id.clone()); user_entry.save(&mut conn).await?; log_event( EventType::OrganizationUserUpdatedGroups as i32, &assigned_user_id, org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; } Ok(()) } #[get("/organizations/<_org_id>/users//groups")] async fn get_user_groups(_org_id: String, user_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } match UserOrganization::find_by_uuid(&user_id, &mut conn).await { Some(_) => { /* Do nothing */ } _ => err!("User could not be found!"), }; let user_groups: Vec = GroupUser::find_by_user(&user_id, &mut conn).await.iter().map(|entry| entry.groups_uuid.clone()).collect(); Ok(Json(json!(user_groups))) } #[derive(Deserialize)] #[allow(non_snake_case)] struct OrganizationUserUpdateGroupsRequest { GroupIds: Vec, } #[post("/organizations//users//groups", data = "")] async fn post_user_groups( org_id: String, org_user_id: String, data: JsonUpcase, headers: AdminHeaders, conn: DbConn, ip: ClientIp, ) -> EmptyResult { put_user_groups(org_id, org_user_id, data, headers, conn, ip).await } #[put("/organizations//users//groups", data = "")] async fn put_user_groups( org_id: String, org_user_id: String, data: JsonUpcase, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } match UserOrganization::find_by_uuid(&org_user_id, &mut conn).await { Some(_) => { /* Do nothing */ } _ => err!("User could not be found!"), }; GroupUser::delete_all_by_user(&org_user_id, &mut conn).await?; let assigned_group_ids = data.into_inner().data; for assigned_group_id in assigned_group_ids.GroupIds { let mut group_user = GroupUser::new(assigned_group_id.clone(), org_user_id.clone()); group_user.save(&mut conn).await?; } log_event( EventType::OrganizationUserUpdatedGroups as i32, &org_user_id, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; Ok(()) } #[post("/organizations//groups//delete-user/")] async fn post_delete_group_user( org_id: String, group_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn, ip: ClientIp, ) -> EmptyResult { delete_group_user(org_id, group_id, org_user_id, headers, conn, ip).await } #[delete("/organizations//groups//users/")] async fn delete_group_user( org_id: String, group_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } match UserOrganization::find_by_uuid(&org_user_id, &mut conn).await { Some(_) => { /* Do nothing */ } _ => err!("User could not be found!"), }; match Group::find_by_uuid(&group_id, &mut conn).await { Some(_) => { /* Do nothing */ } _ => err!("Group could not be found!"), }; log_event( EventType::OrganizationUserUpdatedGroups as i32, &org_user_id, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; GroupUser::delete_by_group_id_and_user_id(&group_id, &org_user_id, &mut conn).await } #[derive(Deserialize)] #[allow(non_snake_case)] struct OrganizationUserResetPasswordEnrollmentRequest { ResetPasswordKey: Option, } #[derive(Deserialize)] #[allow(non_snake_case)] struct OrganizationUserResetPasswordRequest { NewMasterPasswordHash: String, Key: String, } #[get("/organizations//keys")] async fn get_organization_keys(org_id: String, mut conn: DbConn) -> JsonResult { let org = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(organization) => organization, None => err!("Organization not found"), }; Ok(Json(json!({ "Object": "organizationKeys", "PublicKey": org.public_key, "PrivateKey": org.private_key, }))) } #[put("/organizations//users//reset-password", data = "")] async fn put_reset_password( org_id: String, org_user_id: String, headers: AdminHeaders, data: JsonUpcase, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { let org = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(org) => org, None => err!("Required organization not found"), }; let org_user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org.uuid, &mut conn).await { Some(user) => user, None => err!("User to reset isn't member of required organization"), }; let mut user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { Some(user) => user, None => err!("User not found"), }; check_reset_password_applicable_and_permissions(&org_id, &org_user_id, &headers, &mut conn).await?; if org_user.reset_password_key.is_none() { err!("Password reset not or not correctly enrolled"); } if org_user.status != (UserOrgStatus::Confirmed as i32) { err!("Organization user must be confirmed for password reset functionality"); } // Sending email before resetting password to ensure working email configuration and the resulting // user notification. Also this might add some protection against security flaws and misuse if let Err(e) = mail::send_admin_reset_password(&user.email, &user.name, &org.name).await { error!("Error sending user reset password email: {:#?}", e); } let reset_request = data.into_inner().data; user.set_password(reset_request.NewMasterPasswordHash.as_str(), Some(reset_request.Key), true, None); user.save(&mut conn).await?; nt.send_logout(&user, None).await; log_event( EventType::OrganizationUserAdminResetPassword as i32, &org_user_id, org.uuid.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn, ) .await; Ok(()) } #[get("/organizations//users//reset-password-details")] async fn get_reset_password_details( org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { let org = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(org) => org, None => err!("Required organization not found"), }; let org_user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &mut conn).await { Some(user) => user, None => err!("User to reset isn't member of required organization"), }; let user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { Some(user) => user, None => err!("User not found"), }; check_reset_password_applicable_and_permissions(&org_id, &org_user_id, &headers, &mut conn).await?; Ok(Json(json!({ "Object": "organizationUserResetPasswordDetails", "Kdf":user.client_kdf_type, "KdfIterations":user.client_kdf_iter, "ResetPasswordKey":org_user.reset_password_key, "EncryptedPrivateKey":org.private_key , }))) } async fn check_reset_password_applicable_and_permissions( org_id: &str, org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn, ) -> EmptyResult { check_reset_password_applicable(org_id, conn).await?; let target_user = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { Some(user) => user, None => err!("Reset target user not found"), }; // Resetting user must be higher/equal to user to reset match headers.org_user_type { UserOrgType::Owner => Ok(()), UserOrgType::Admin if target_user.atype <= UserOrgType::Admin => Ok(()), _ => err!("No permission to reset this user's password"), } } async fn check_reset_password_applicable(org_id: &str, conn: &mut DbConn) -> EmptyResult { if !CONFIG.mail_enabled() { err!("Password reset is not supported on an email-disabled instance."); } let policy = match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, conn).await { Some(p) => p, None => err!("Policy not found"), }; if !policy.enabled { err!("Reset password policy not enabled"); } Ok(()) } #[put("/organizations//users//reset-password-enrollment", data = "")] async fn put_reset_password_enrollment( org_id: String, org_user_id: String, headers: Headers, data: JsonUpcase, mut conn: DbConn, ip: ClientIp, ) -> EmptyResult { let mut org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { Some(u) => u, None => err!("User to enroll isn't member of required organization"), }; check_reset_password_applicable(&org_id, &mut conn).await?; let reset_request = data.into_inner().data; if reset_request.ResetPasswordKey.is_none() && OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await { err!("Reset password can't be withdrawed due to an enterprise policy"); } org_user.reset_password_key = reset_request.ResetPasswordKey; org_user.save(&mut conn).await?; let log_id = if org_user.reset_password_key.is_some() { EventType::OrganizationUserResetPasswordEnroll as i32 } else { EventType::OrganizationUserResetPasswordWithdraw as i32 }; log_event(log_id, &org_user_id, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn).await; Ok(()) } // This is a new function active since the v2022.9.x clients. // It combines the previous two calls done before. // We call those two functions here and combine them our selfs. // // NOTE: It seems clients can't handle uppercase-first keys!! // We need to convert all keys so they have the first character to be a lowercase. // Else the export will be just an empty JSON file. #[get("/organizations//export")] async fn get_org_export(org_id: String, headers: AdminHeaders, mut conn: DbConn) -> Json { use semver::{Version, VersionReq}; // Since version v2023.1.0 the format of the export is different. // Also, this endpoint was created since v2022.9.0. // Therefore, we will check for any version smaller then v2023.1.0 and return a different response. // If we can't determine the version, we will use the latest default v2023.1.0 and higher. // https://github.com/bitwarden/server/blob/9ca93381ce416454734418c3a9f99ab49747f1b6/src/Api/Controllers/OrganizationExportController.cs#L44 let use_list_response_model = if let Some(client_version) = headers.client_version { let ver_match = VersionReq::parse("<2023.1.0").unwrap(); let client_version = Version::parse(&client_version).unwrap(); ver_match.matches(&client_version) } else { false }; // Also both main keys here need to be lowercase, else the export will fail. if use_list_response_model { // Backwards compatible pre v2023.1.0 response Json(json!({ "collections": { "data": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), "object": "list", "continuationToken": null, }, "ciphers": { "data": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await), "object": "list", "continuationToken": null, } })) } else { // v2023.1.0 and newer response Json(json!({ "collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), "ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await), })) } }