| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893 |
- 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, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
- db::{models::*, DbConn},
- error::Error,
- mail,
- util::convert_json_key_lcase_first,
- CONFIG,
- };
- pub fn routes() -> Vec<Route> {
- routes![
- get_organization,
- create_organization,
- delete_organization,
- post_delete_organization,
- leave_organization,
- get_user_collections,
- get_org_collections,
- get_org_collections_details,
- 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,
- bulk_delete_organization_collections,
- 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,
- bulk_delete_groups,
- 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<OrgKeyData>,
- #[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<NewCollectionObjectData>,
- Users: Vec<NewCollectionObjectData>,
- }
- #[derive(Deserialize)]
- #[allow(non_snake_case)]
- struct NewCollectionObjectData {
- 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<String>,
- }
- #[post("/organizations", data = "<data>")]
- async fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, 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/<org_id>", data = "<data>")]
- async fn delete_organization(
- org_id: &str,
- data: JsonUpcase<PasswordData>,
- 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/<org_id>/delete", data = "<data>")]
- async fn post_delete_organization(
- org_id: &str,
- data: JsonUpcase<PasswordData>,
- headers: OwnerHeaders,
- conn: DbConn,
- ) -> EmptyResult {
- delete_organization(org_id, data, headers, conn).await
- }
- #[post("/organizations/<org_id>/leave")]
- async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) -> 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,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- user_org.delete(&mut conn).await
- }
- }
- }
- #[get("/organizations/<org_id>")]
- async fn get_organization(org_id: &str, _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/<org_id>", data = "<data>")]
- async fn put_organization(
- org_id: &str,
- headers: OwnerHeaders,
- data: JsonUpcase<OrganizationUpdateData>,
- conn: DbConn,
- ) -> JsonResult {
- post_organization(org_id, headers, data, conn).await
- }
- #[post("/organizations/<org_id>", data = "<data>")]
- async fn post_organization(
- org_id: &str,
- headers: OwnerHeaders,
- data: JsonUpcase<OrganizationUpdateData>,
- mut conn: DbConn,
- ) -> 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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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<Value> {
- Json(json!({
- "Data":
- Collection::find_by_user_uuid(headers.user.uuid.clone(), &mut conn).await
- .iter()
- .map(Collection::to_json)
- .collect::<Value>(),
- "Object": "list",
- "ContinuationToken": null,
- }))
- }
- #[get("/organizations/<org_id>/collections")]
- async fn get_org_collections(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
- Json(json!({
- "Data": _get_org_collections(org_id, &mut conn).await,
- "Object": "list",
- "ContinuationToken": null,
- }))
- }
- #[get("/organizations/<org_id>/collections/details")]
- async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
- let mut data = Vec::new();
- 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 coll_users = CollectionUser::find_by_organization(org_id, &mut conn).await;
- for col in Collection::find_by_organization(org_id, &mut conn).await {
- let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
- CollectionGroup::find_by_collection(&col.uuid, &mut conn)
- .await
- .iter()
- .map(|collection_group| {
- SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
- })
- .collect()
- } else {
- // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
- // so just act as if there are no groups.
- Vec::with_capacity(0)
- };
- let mut assigned = false;
- let users: Vec<Value> = coll_users
- .iter()
- .filter(|collection_user| collection_user.collection_uuid == col.uuid)
- .map(|collection_user| {
- // Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `CollectionUser::find_by_organization` call.
- // We check here if the current user is assigned to this collection or not.
- if collection_user.user_uuid == user_org.uuid {
- assigned = true;
- }
- SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
- })
- .collect();
- if user_org.access_all {
- assigned = true;
- }
- let mut json_object = col.to_json();
- json_object["Assigned"] = json!(assigned);
- json_object["Users"] = json!(users);
- json_object["Groups"] = json!(groups);
- json_object["Object"] = json!("collectionAccessDetails");
- data.push(json_object)
- }
- Ok(Json(json!({
- "Data": data,
- "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::<Value>()
- }
- #[post("/organizations/<org_id>/collections", data = "<data>")]
- async fn post_organization_collections(
- org_id: &str,
- headers: ManagerHeadersLoose,
- data: JsonUpcase<NewCollectionData>,
- mut conn: DbConn,
- ) -> 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 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,
- &headers.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?;
- }
- for user in data.Users {
- let org_user = match UserOrganization::find_by_uuid(&user.Id, &mut conn).await {
- Some(u) => u,
- None => err!("User is not part of organization"),
- };
- if org_user.access_all {
- continue;
- }
- CollectionUser::save(&org_user.user_uuid, &collection.uuid, user.ReadOnly, user.HidePasswords, &mut conn)
- .await?;
- }
- Ok(Json(collection.to_json()))
- }
- #[put("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
- async fn put_organization_collection_update(
- org_id: &str,
- col_id: &str,
- headers: ManagerHeaders,
- data: JsonUpcase<NewCollectionData>,
- conn: DbConn,
- ) -> JsonResult {
- post_organization_collection_update(org_id, col_id, headers, data, conn).await
- }
- #[post("/organizations/<org_id>/collections/<col_id>", data = "<data>")]
- async fn post_organization_collection_update(
- org_id: &str,
- col_id: &str,
- headers: ManagerHeaders,
- data: JsonUpcase<NewCollectionData>,
- mut conn: DbConn,
- ) -> 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,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- CollectionGroup::delete_all_by_collection(col_id, &mut conn).await?;
- for group in data.Groups {
- CollectionGroup::new(String::from(col_id), group.Id, group.ReadOnly, group.HidePasswords)
- .save(&mut conn)
- .await?;
- }
- CollectionUser::delete_all_by_collection(col_id, &mut conn).await?;
- for user in data.Users {
- let org_user = match UserOrganization::find_by_uuid(&user.Id, &mut conn).await {
- Some(u) => u,
- None => err!("User is not part of organization"),
- };
- if org_user.access_all {
- continue;
- }
- CollectionUser::save(&org_user.user_uuid, col_id, user.ReadOnly, user.HidePasswords, &mut conn).await?;
- }
- Ok(Json(collection.to_json()))
- }
- #[delete("/organizations/<org_id>/collections/<col_id>/user/<org_user_id>")]
- async fn delete_organization_collection_user(
- org_id: &str,
- col_id: &str,
- org_user_id: &str,
- _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/<org_id>/collections/<col_id>/delete-user/<org_user_id>")]
- async fn post_organization_collection_delete_user(
- org_id: &str,
- col_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- conn: DbConn,
- ) -> EmptyResult {
- delete_organization_collection_user(org_id, col_id, org_user_id, headers, conn).await
- }
- async fn _delete_organization_collection(
- org_id: &str,
- col_id: &str,
- headers: &ManagerHeaders,
- conn: &mut DbConn,
- ) -> EmptyResult {
- match Collection::find_by_uuid(col_id, 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,
- &headers.ip.ip,
- conn,
- )
- .await;
- collection.delete(conn).await
- } else {
- err!("Collection and Organization id do not match")
- }
- }
- }
- }
- #[delete("/organizations/<org_id>/collections/<col_id>")]
- async fn delete_organization_collection(
- org_id: &str,
- col_id: &str,
- headers: ManagerHeaders,
- mut conn: DbConn,
- ) -> EmptyResult {
- _delete_organization_collection(org_id, col_id, &headers, &mut conn).await
- }
- #[derive(Deserialize, Debug)]
- #[allow(non_snake_case, dead_code)]
- struct DeleteCollectionData {
- Id: String,
- OrgId: String,
- }
- #[post("/organizations/<org_id>/collections/<col_id>/delete", data = "<_data>")]
- async fn post_organization_collection_delete(
- org_id: &str,
- col_id: &str,
- headers: ManagerHeaders,
- _data: JsonUpcase<DeleteCollectionData>,
- mut conn: DbConn,
- ) -> EmptyResult {
- _delete_organization_collection(org_id, col_id, &headers, &mut conn).await
- }
- #[derive(Deserialize, Debug)]
- #[allow(non_snake_case)]
- struct BulkCollectionIds {
- Ids: Vec<String>,
- OrganizationId: String,
- }
- #[delete("/organizations/<org_id>/collections", data = "<data>")]
- async fn bulk_delete_organization_collections(
- org_id: &str,
- headers: ManagerHeadersLoose,
- data: JsonUpcase<BulkCollectionIds>,
- mut conn: DbConn,
- ) -> EmptyResult {
- let data: BulkCollectionIds = data.into_inner().data;
- if org_id != data.OrganizationId {
- err!("OrganizationId mismatch");
- }
- let collections = data.Ids;
- let headers = ManagerHeaders::from_loose(headers, &collections, &mut conn).await?;
- for col_id in collections {
- _delete_organization_collection(org_id, &col_id, &headers, &mut conn).await?
- }
- Ok(())
- }
- #[get("/organizations/<org_id>/collections/<coll_id>/details")]
- async fn get_org_collection_detail(
- org_id: &str,
- coll_id: &str,
- 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 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 groups: Vec<Value> = if CONFIG.org_groups_enabled() {
- 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()
- } else {
- // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
- // so just act as if there are no groups.
- Vec::with_capacity(0)
- };
- let mut assigned = false;
- let users: Vec<Value> =
- CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid(&collection.uuid, &mut conn)
- .await
- .iter()
- .map(|collection_user| {
- // Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `find_by_collection_swap_user_uuid_with_org_user_uuid` call.
- // We check here if the current user is assigned to this collection or not.
- if collection_user.user_uuid == user_org.uuid {
- assigned = true;
- }
- SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
- })
- .collect();
- if user_org.access_all {
- assigned = true;
- }
- let mut json_object = collection.to_json();
- json_object["Assigned"] = json!(assigned);
- json_object["Users"] = json!(users);
- json_object["Groups"] = json!(groups);
- json_object["Object"] = json!("collectionAccessDetails");
- Ok(Json(json_object))
- }
- }
- }
- #[get("/organizations/<org_id>/collections/<coll_id>/users")]
- async fn get_collection_users(org_id: &str, coll_id: &str, _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/<org_id>/collections/<coll_id>/users", data = "<data>")]
- async fn put_collection_users(
- org_id: &str,
- coll_id: &str,
- data: JsonUpcaseVec<CollectionData>,
- _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?<data..>")]
- async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> Json<Value> {
- 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), CipherSyncType::Organization, conn).await);
- }
- json!(ciphers_json)
- }
- #[derive(FromForm)]
- struct GetOrgUserData {
- #[field(name = "includeCollections")]
- include_collections: Option<bool>,
- #[field(name = "includeGroups")]
- include_groups: Option<bool>,
- }
- #[get("/organizations/<org_id>/users?<data..>")]
- async fn get_org_users(
- data: GetOrgUserData,
- org_id: &str,
- _headers: ManagerHeadersLoose,
- mut conn: DbConn,
- ) -> Json<Value> {
- 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(
- data.include_collections.unwrap_or(false),
- data.include_groups.unwrap_or(false),
- &mut conn,
- )
- .await,
- );
- }
- Json(json!({
- "Data": users_json,
- "Object": "list",
- "ContinuationToken": null,
- }))
- }
- #[post("/organizations/<org_id>/keys", data = "<data>")]
- async fn post_org_keys(
- org_id: &str,
- data: JsonUpcase<OrgKeyData>,
- _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<String>,
- Type: NumberOrString,
- Collections: Option<Vec<CollectionData>>,
- AccessAll: Option<bool>,
- }
- #[post("/organizations/<org_id>/users/invite", data = "<data>")]
- async fn send_invite(
- org_id: &str,
- data: JsonUpcase<InviteData>,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> 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(), String::from(org_id));
- 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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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(String::from(org_id)),
- Some(new_user.uuid),
- &org_name,
- Some(headers.user.email.clone()),
- )
- .await?;
- }
- }
- Ok(())
- }
- #[post("/organizations/<org_id>/users/reinvite", data = "<data>")]
- async fn bulk_reinvite_user(
- org_id: &str,
- data: JsonUpcase<OrgBulkIds>,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> Json<Value> {
- 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/<org_id>/users/<user_org>/reinvite")]
- async fn reinvite_user(org_id: &str, user_org: &str, 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<String>,
- }
- #[post("/organizations/<org_id>/users/<_org_user_id>/accept", data = "<data>")]
- async fn accept_invite(
- org_id: &str,
- _org_user_id: &str,
- data: JsonUpcase<AcceptData>,
- 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/<org_id>/users/confirm", data = "<data>")]
- async fn bulk_confirm_invite(
- org_id: &str,
- data: JsonUpcase<Value>,
- headers: AdminHeaders,
- mut conn: DbConn,
- nt: Notify<'_>,
- ) -> Json<Value> {
- 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, &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/<org_id>/users/<org_user_id>/confirm", data = "<data>")]
- async fn confirm_invite(
- org_id: &str,
- org_user_id: &str,
- data: JsonUpcase<Value>,
- headers: AdminHeaders,
- mut conn: DbConn,
- 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, &nt).await
- }
- async fn _confirm_invite(
- org_id: &str,
- org_user_id: &str,
- key: &str,
- headers: &AdminHeaders,
- conn: &mut DbConn,
- 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,
- org_id,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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/<org_id>/users/<org_user_id>?<data..>")]
- async fn get_user(
- org_id: &str,
- org_user_id: &str,
- data: GetOrgUserData,
- _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"),
- };
- // In this case, when groups are requested we also need to include collections.
- // Else these will not be shown in the interface, and could lead to missing collections when saved.
- let include_groups = data.include_groups.unwrap_or(false);
- Ok(Json(
- user.to_json_user_details(data.include_collections.unwrap_or(include_groups), include_groups, &mut conn).await,
- ))
- }
- #[derive(Deserialize)]
- #[allow(non_snake_case)]
- struct EditUserData {
- Type: NumberOrString,
- Collections: Option<Vec<CollectionData>>,
- Groups: Option<Vec<String>>,
- AccessAll: bool,
- }
- #[put("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)]
- async fn put_organization_user(
- org_id: &str,
- org_user_id: &str,
- data: JsonUpcase<EditUserData>,
- headers: AdminHeaders,
- conn: DbConn,
- ) -> EmptyResult {
- edit_user(org_id, org_user_id, data, headers, conn).await
- }
- #[post("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)]
- async fn edit_user(
- org_id: &str,
- org_user_id: &str,
- data: JsonUpcase<EditUserData>,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> 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?;
- }
- }
- }
- }
- GroupUser::delete_all_by_user(&user_to_edit.uuid, &mut conn).await?;
- for group in data.Groups.iter().flatten() {
- let mut group_entry = GroupUser::new(String::from(group), user_to_edit.uuid.clone());
- group_entry.save(&mut conn).await?;
- }
- log_event(
- EventType::OrganizationUserUpdated as i32,
- &user_to_edit.uuid,
- org_id,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- user_to_edit.save(&mut conn).await
- }
- #[delete("/organizations/<org_id>/users", data = "<data>")]
- async fn bulk_delete_user(
- org_id: &str,
- data: JsonUpcase<OrgBulkIds>,
- headers: AdminHeaders,
- mut conn: DbConn,
- nt: Notify<'_>,
- ) -> Json<Value> {
- 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, &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/<org_id>/users/<org_user_id>")]
- async fn delete_user(
- org_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- mut conn: DbConn,
- nt: Notify<'_>,
- ) -> EmptyResult {
- _delete_user(org_id, org_user_id, &headers, &mut conn, &nt).await
- }
- #[post("/organizations/<org_id>/users/<org_user_id>/delete")]
- async fn post_delete_user(
- org_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- mut conn: DbConn,
- nt: Notify<'_>,
- ) -> EmptyResult {
- _delete_user(org_id, org_user_id, &headers, &mut conn, &nt).await
- }
- async fn _delete_user(
- org_id: &str,
- org_user_id: &str,
- headers: &AdminHeaders,
- conn: &mut DbConn,
- 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,
- org_id,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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/<org_id>/users/public-keys", data = "<data>")]
- async fn bulk_public_keys(
- org_id: &str,
- data: JsonUpcase<OrgBulkIds>,
- _headers: AdminHeaders,
- mut conn: DbConn,
- ) -> Json<Value> {
- 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<CipherData>,
- Collections: Vec<NewCollectionData>,
- CollectionRelationships: Vec<RelationsData>,
- }
- #[derive(Deserialize)]
- #[allow(non_snake_case)]
- struct RelationsData {
- // Cipher index
- Key: usize,
- // Collection index
- Value: usize,
- }
- #[post("/ciphers/import-organization?<query..>", data = "<data>")]
- async fn post_org_import(
- query: OrgIdData,
- data: JsonUpcase<ImportData>,
- headers: AdminHeaders,
- mut conn: DbConn,
- 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, &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/<org_id>/policies")]
- async fn list_policies(org_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
- let policies = OrgPolicy::find_by_org(org_id, &mut conn).await;
- let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
- Json(json!({
- "Data": policies_json,
- "Object": "list",
- "ContinuationToken": null
- }))
- }
- #[get("/organizations/<org_id>/policies/token?<token>")]
- async fn list_policies_token(org_id: &str, token: &str, 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<Value> = policies.iter().map(OrgPolicy::to_json).collect();
- Ok(Json(json!({
- "Data": policies_json,
- "Object": "list",
- "ContinuationToken": null
- })))
- }
- #[get("/organizations/<org_id>/policies/<pol_type>")]
- async fn get_policy(org_id: &str, 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(String::from(org_id), pol_type_enum, "null".to_string()),
- };
- Ok(Json(policy.to_json()))
- }
- #[derive(Deserialize)]
- struct PolicyData {
- enabled: bool,
- #[serde(rename = "type")]
- _type: i32,
- data: Option<Value>,
- }
- #[put("/organizations/<org_id>/policies/<pol_type>", data = "<data>")]
- async fn put_policy(
- org_id: &str,
- pol_type: i32,
- data: Json<PolicyData>,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> 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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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(String::from(org_id), 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,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- Ok(Json(policy.to_json()))
- }
- #[allow(unused_variables)]
- #[get("/organizations/<org_id>/tax")]
- fn get_organization_tax(org_id: &str, _headers: Headers) -> Json<Value> {
- // 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<Value> {
- // 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<Value> {
- // 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<String>, // ["uid=user,ou=People,dc=example,dc=com"]
- }
- #[derive(Deserialize, Debug)]
- #[allow(non_snake_case)]
- struct OrgImportUserData {
- Email: String, // "[email protected]"
- #[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<OrgImportGroupData>,
- OverwriteExisting: bool,
- Users: Vec<OrgImportUserData>,
- }
- #[post("/organizations/<org_id>/import", data = "<data>")]
- async fn import(org_id: &str, data: JsonUpcase<OrgImportData>, headers: Headers, mut conn: DbConn) -> 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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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(), String::from(org_id));
- 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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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(String::from(org_id)),
- 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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- user_org.delete(&mut conn).await?;
- }
- }
- }
- }
- Ok(())
- }
- // Pre web-vault v2022.9.x endpoint
- #[put("/organizations/<org_id>/users/<org_user_id>/deactivate")]
- async fn deactivate_organization_user(
- org_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> EmptyResult {
- _revoke_organization_user(org_id, org_user_id, &headers, &mut conn).await
- }
- // Pre web-vault v2022.9.x endpoint
- #[put("/organizations/<org_id>/users/deactivate", data = "<data>")]
- async fn bulk_deactivate_organization_user(
- org_id: &str,
- data: JsonUpcase<Value>,
- headers: AdminHeaders,
- conn: DbConn,
- ) -> Json<Value> {
- bulk_revoke_organization_user(org_id, data, headers, conn).await
- }
- #[put("/organizations/<org_id>/users/<org_user_id>/revoke")]
- async fn revoke_organization_user(
- org_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> EmptyResult {
- _revoke_organization_user(org_id, org_user_id, &headers, &mut conn).await
- }
- #[put("/organizations/<org_id>/users/revoke", data = "<data>")]
- async fn bulk_revoke_organization_user(
- org_id: &str,
- data: JsonUpcase<Value>,
- headers: AdminHeaders,
- mut 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 _revoke_organization_user(org_id, org_user_id, &headers, &mut conn).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,
- ) -> 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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.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/<org_id>/users/<org_user_id>/activate")]
- async fn activate_organization_user(
- org_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> EmptyResult {
- _restore_organization_user(org_id, org_user_id, &headers, &mut conn).await
- }
- // Pre web-vault v2022.9.x endpoint
- #[put("/organizations/<org_id>/users/activate", data = "<data>")]
- async fn bulk_activate_organization_user(
- org_id: &str,
- data: JsonUpcase<Value>,
- headers: AdminHeaders,
- conn: DbConn,
- ) -> Json<Value> {
- bulk_restore_organization_user(org_id, data, headers, conn).await
- }
- #[put("/organizations/<org_id>/users/<org_user_id>/restore")]
- async fn restore_organization_user(
- org_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> EmptyResult {
- _restore_organization_user(org_id, org_user_id, &headers, &mut conn).await
- }
- #[put("/organizations/<org_id>/users/restore", data = "<data>")]
- async fn bulk_restore_organization_user(
- org_id: &str,
- data: JsonUpcase<Value>,
- headers: AdminHeaders,
- mut 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 _restore_organization_user(org_id, org_user_id, &headers, &mut conn).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,
- ) -> 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,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.ip.ip,
- conn,
- )
- .await;
- }
- Some(_) => err!("User is already active"),
- None => err!("User not found in organization"),
- }
- Ok(())
- }
- #[get("/organizations/<org_id>/groups")]
- async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
- let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
- // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
- let groups = Group::find_by_organization(org_id, &mut conn).await;
- let mut groups_json = Vec::with_capacity(groups.len());
- for g in groups {
- groups_json.push(g.to_json_details(&mut conn).await)
- }
- groups_json
- } else {
- // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
- // so just act as if there are no groups.
- Vec::with_capacity(0)
- };
- Ok(Json(json!({
- "Data": groups,
- "Object": "list",
- "ContinuationToken": null,
- })))
- }
- #[derive(Deserialize)]
- #[allow(non_snake_case)]
- struct GroupRequest {
- Name: String,
- AccessAll: Option<bool>,
- ExternalId: Option<String>,
- Collections: Vec<SelectionReadOnly>,
- Users: Vec<String>,
- }
- impl GroupRequest {
- pub fn to_group(&self, organizations_uuid: &str) -> ApiResult<Group> {
- 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<Group> {
- 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_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_collection_user_details_read_only(collection_user: &CollectionUser) -> SelectionReadOnly {
- SelectionReadOnly {
- Id: collection_user.user_uuid.clone(),
- ReadOnly: collection_user.read_only,
- HidePasswords: collection_user.hide_passwords,
- }
- }
- pub fn to_json(&self) -> Value {
- json!(self)
- }
- }
- #[post("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
- async fn post_group(
- org_id: &str,
- group_id: &str,
- data: JsonUpcase<GroupRequest>,
- headers: AdminHeaders,
- conn: DbConn,
- ) -> JsonResult {
- put_group(org_id, group_id, data, headers, conn).await
- }
- #[post("/organizations/<org_id>/groups", data = "<data>")]
- async fn post_groups(
- org_id: &str,
- headers: AdminHeaders,
- data: JsonUpcase<GroupRequest>,
- mut conn: DbConn,
- ) -> 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,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- add_update_group(group, group_request.Collections, group_request.Users, org_id, &headers, &mut conn).await
- }
- #[put("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
- async fn put_group(
- org_id: &str,
- group_id: &str,
- data: JsonUpcase<GroupRequest>,
- 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,
- 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?;
- GroupUser::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,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- add_update_group(updated_group, group_request.Collections, group_request.Users, org_id, &headers, &mut conn).await
- }
- async fn add_update_group(
- mut group: Group,
- collections: Vec<SelectionReadOnly>,
- users: Vec<String>,
- org_id: &str,
- headers: &AdminHeaders,
- 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?;
- }
- for assigned_user_id in users {
- let mut user_entry = GroupUser::new(group.uuid.clone(), assigned_user_id.clone());
- user_entry.save(conn).await?;
- log_event(
- EventType::OrganizationUserUpdatedGroups as i32,
- &assigned_user_id,
- org_id,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.ip.ip,
- 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/<group_id>/details")]
- async fn get_group_details(_org_id: &str, group_id: &str, _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!"),
- };
- Ok(Json(group.to_json_details(&mut conn).await))
- }
- #[post("/organizations/<org_id>/groups/<group_id>/delete")]
- async fn post_delete_group(org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult {
- _delete_group(org_id, group_id, &headers, &mut conn).await
- }
- #[delete("/organizations/<org_id>/groups/<group_id>")]
- async fn delete_group(org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult {
- _delete_group(org_id, group_id, &headers, &mut conn).await
- }
- async fn _delete_group(org_id: &str, group_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult {
- if !CONFIG.org_groups_enabled() {
- err!("Group support is disabled");
- }
- let group = match Group::find_by_uuid(group_id, 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,
- &headers.ip.ip,
- conn,
- )
- .await;
- group.delete(conn).await
- }
- #[delete("/organizations/<org_id>/groups", data = "<data>")]
- async fn bulk_delete_groups(
- org_id: &str,
- data: JsonUpcase<OrgBulkIds>,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> EmptyResult {
- if !CONFIG.org_groups_enabled() {
- err!("Group support is disabled");
- }
- let data: OrgBulkIds = data.into_inner().data;
- for group_id in data.Ids {
- _delete_group(org_id, &group_id, &headers, &mut conn).await?
- }
- Ok(())
- }
- #[get("/organizations/<_org_id>/groups/<group_id>")]
- async fn get_group(_org_id: &str, group_id: &str, _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/<group_id>/users")]
- async fn get_group_users(_org_id: &str, group_id: &str, _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<String> = 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/<org_id>/groups/<group_id>/users", data = "<data>")]
- async fn put_group_users(
- org_id: &str,
- group_id: &str,
- headers: AdminHeaders,
- data: JsonVec<String>,
- mut conn: DbConn,
- ) -> 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(String::from(group_id), assigned_user_id.clone());
- user_entry.save(&mut conn).await?;
- log_event(
- EventType::OrganizationUserUpdatedGroups as i32,
- &assigned_user_id,
- org_id,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- }
- Ok(())
- }
- #[get("/organizations/<_org_id>/users/<user_id>/groups")]
- async fn get_user_groups(_org_id: &str, user_id: &str, _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<String> =
- 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<String>,
- }
- #[post("/organizations/<org_id>/users/<org_user_id>/groups", data = "<data>")]
- async fn post_user_groups(
- org_id: &str,
- org_user_id: &str,
- data: JsonUpcase<OrganizationUserUpdateGroupsRequest>,
- headers: AdminHeaders,
- conn: DbConn,
- ) -> EmptyResult {
- put_user_groups(org_id, org_user_id, data, headers, conn).await
- }
- #[put("/organizations/<org_id>/users/<org_user_id>/groups", data = "<data>")]
- async fn put_user_groups(
- org_id: &str,
- org_user_id: &str,
- data: JsonUpcase<OrganizationUserUpdateGroupsRequest>,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> 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(), String::from(org_user_id));
- group_user.save(&mut conn).await?;
- }
- log_event(
- EventType::OrganizationUserUpdatedGroups as i32,
- org_user_id,
- org_id,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- Ok(())
- }
- #[post("/organizations/<org_id>/groups/<group_id>/delete-user/<org_user_id>")]
- async fn post_delete_group_user(
- org_id: &str,
- group_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- conn: DbConn,
- ) -> EmptyResult {
- delete_group_user(org_id, group_id, org_user_id, headers, conn).await
- }
- #[delete("/organizations/<org_id>/groups/<group_id>/users/<org_user_id>")]
- async fn delete_group_user(
- org_id: &str,
- group_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- mut conn: DbConn,
- ) -> 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,
- &headers.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<String>,
- }
- #[derive(Deserialize)]
- #[allow(non_snake_case)]
- struct OrganizationUserResetPasswordRequest {
- NewMasterPasswordHash: String,
- Key: String,
- }
- #[get("/organizations/<org_id>/keys")]
- async fn get_organization_keys(org_id: &str, 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/<org_id>/users/<org_user_id>/reset-password", data = "<data>")]
- async fn put_reset_password(
- org_id: &str,
- org_user_id: &str,
- headers: AdminHeaders,
- data: JsonUpcase<OrganizationUserResetPasswordRequest>,
- mut conn: DbConn,
- 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 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 {
- err!(format!("Error sending user reset password email: {e:#?}"));
- }
- let reset_request = data.into_inner().data;
- let mut user = user;
- user.set_password(reset_request.NewMasterPasswordHash.as_str(), Some(reset_request.Key), true, None);
- user.save(&mut conn).await?;
- nt.send_logout(&user, None, &mut conn).await;
- log_event(
- EventType::OrganizationUserAdminResetPassword as i32,
- org_user_id,
- org_id,
- headers.user.uuid.clone(),
- headers.device.atype,
- &headers.ip.ip,
- &mut conn,
- )
- .await;
- Ok(())
- }
- #[get("/organizations/<org_id>/users/<org_user_id>/reset-password-details")]
- async fn get_reset_password_details(
- org_id: &str,
- org_user_id: &str,
- 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?;
- // https://github.com/bitwarden/server/blob/3b50ccb9f804efaacdc46bed5b60e5b28eddefcf/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs#L111
- Ok(Json(json!({
- "Object": "organizationUserResetPasswordDetails",
- "Kdf":user.client_kdf_type,
- "KdfIterations":user.client_kdf_iter,
- "KdfMemory":user.client_kdf_memory,
- "KdfParallelism":user.client_kdf_parallelism,
- "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/<org_id>/users/<org_user_id>/reset-password-enrollment", data = "<data>")]
- async fn put_reset_password_enrollment(
- org_id: &str,
- org_user_id: &str,
- headers: Headers,
- data: JsonUpcase<OrganizationUserResetPasswordEnrollmentRequest>,
- mut conn: DbConn,
- ) -> 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, &headers.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/<org_id>/export")]
- async fn get_org_export(org_id: &str, headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
- 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),
- }))
- }
- }
|