mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 02:08:20 +02:00 
			
		
		
		
	Change API and structs to camelCase (#4386)
* Change API inputs/outputs and structs to camelCase * Fix fields and password history * Use convert_json_key_lcase_first * Make sends lowercase * Update admin and templates * Update org revoke * Fix sends expecting size to be a string on mobile * Convert two-factor providers to string
This commit is contained in:
		| @@ -265,8 +265,8 @@ fn admin_page_login() -> ApiResult<Html<String>> { | ||||
|     render_admin_login(None, None) | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct InviteData { | ||||
|     email: String, | ||||
| } | ||||
| @@ -326,9 +326,9 @@ async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> { | ||||
|     let mut users_json = Vec::with_capacity(users.len()); | ||||
|     for u in users { | ||||
|         let mut usr = u.to_json(&mut conn).await; | ||||
|         usr["UserEnabled"] = json!(u.enabled); | ||||
|         usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); | ||||
|         usr["LastActive"] = match u.last_active(&mut conn).await { | ||||
|         usr["userEnabled"] = json!(u.enabled); | ||||
|         usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); | ||||
|         usr["lastActive"] = match u.last_active(&mut conn).await { | ||||
|             Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)), | ||||
|             None => json!(None::<String>), | ||||
|         }; | ||||
| @@ -475,7 +475,7 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| struct UserOrgTypeData { | ||||
|     user_type: NumberOrString, | ||||
|     user_uuid: String, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -12,7 +12,7 @@ use serde_json::Value; | ||||
|  | ||||
| use crate::util::NumberOrString; | ||||
| use crate::{ | ||||
|     api::{self, core::log_event, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordOrOtpData, UpdateType}, | ||||
|     api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, | ||||
|     auth::Headers, | ||||
|     crypto, | ||||
|     db::{models::*, DbConn, DbPool}, | ||||
| @@ -141,15 +141,15 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json<Value> | ||||
|     }; | ||||
|  | ||||
|     Json(json!({ | ||||
|         "Profile": user_json, | ||||
|         "Folders": folders_json, | ||||
|         "Collections": collections_json, | ||||
|         "Policies": policies_json, | ||||
|         "Ciphers": ciphers_json, | ||||
|         "Domains": domains_json, | ||||
|         "Sends": sends_json, | ||||
|         "profile": user_json, | ||||
|         "folders": folders_json, | ||||
|         "collections": collections_json, | ||||
|         "policies": policies_json, | ||||
|         "ciphers": ciphers_json, | ||||
|         "domains": domains_json, | ||||
|         "sends": sends_json, | ||||
|         "unofficialServer": true, | ||||
|         "Object": "sync" | ||||
|         "object": "sync" | ||||
|     })) | ||||
| } | ||||
|  | ||||
| @@ -167,9 +167,9 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json<Value> { | ||||
|     } | ||||
|  | ||||
|     Json(json!({ | ||||
|       "Data": ciphers_json, | ||||
|       "Object": "list", | ||||
|       "ContinuationToken": null | ||||
|       "data": ciphers_json, | ||||
|       "object": "list", | ||||
|       "continuationToken": null | ||||
|     })) | ||||
| } | ||||
|  | ||||
| @@ -198,17 +198,17 @@ async fn get_cipher_details(uuid: &str, headers: Headers, conn: DbConn) -> JsonR | ||||
|     get_cipher(uuid, headers, conn).await | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct CipherData { | ||||
|     // Id is optional as it is included only in bulk share | ||||
|     pub Id: Option<String>, | ||||
|     pub id: Option<String>, | ||||
|     // Folder id is not included in import | ||||
|     FolderId: Option<String>, | ||||
|     folder_id: Option<String>, | ||||
|     // TODO: Some of these might appear all the time, no need for Option | ||||
|     pub OrganizationId: Option<String>, | ||||
|     pub organization_id: Option<String>, | ||||
|  | ||||
|     Key: Option<String>, | ||||
|     key: Option<String>, | ||||
|  | ||||
|     /* | ||||
|     Login = 1, | ||||
| @@ -216,27 +216,27 @@ pub struct CipherData { | ||||
|     Card = 3, | ||||
|     Identity = 4 | ||||
|     */ | ||||
|     pub Type: i32, | ||||
|     pub Name: String, | ||||
|     pub Notes: Option<String>, | ||||
|     Fields: Option<Value>, | ||||
|     pub r#type: i32, | ||||
|     pub name: String, | ||||
|     pub notes: Option<String>, | ||||
|     fields: Option<Value>, | ||||
|  | ||||
|     // Only one of these should exist, depending on type | ||||
|     Login: Option<Value>, | ||||
|     SecureNote: Option<Value>, | ||||
|     Card: Option<Value>, | ||||
|     Identity: Option<Value>, | ||||
|     login: Option<Value>, | ||||
|     secure_note: Option<Value>, | ||||
|     card: Option<Value>, | ||||
|     identity: Option<Value>, | ||||
|  | ||||
|     Favorite: Option<bool>, | ||||
|     Reprompt: Option<i32>, | ||||
|     favorite: Option<bool>, | ||||
|     reprompt: Option<i32>, | ||||
|  | ||||
|     PasswordHistory: Option<Value>, | ||||
|     password_history: Option<Value>, | ||||
|  | ||||
|     // These are used during key rotation | ||||
|     // 'Attachments' is unused, contains map of {id: filename} | ||||
|     #[serde(rename = "Attachments")] | ||||
|     _Attachments: Option<Value>, | ||||
|     Attachments2: Option<HashMap<String, Attachments2Data>>, | ||||
|     #[allow(dead_code)] | ||||
|     attachments: Option<Value>, | ||||
|     attachments2: Option<HashMap<String, Attachments2Data>>, | ||||
|  | ||||
|     // The revision datetime (in ISO 8601 format) of the client's local copy | ||||
|     // of the cipher. This is used to prevent a client from updating a cipher | ||||
| @@ -244,31 +244,26 @@ pub struct CipherData { | ||||
|     // loss. It's not an error when no value is provided; this can happen | ||||
|     // when using older client versions, or if the operation doesn't involve | ||||
|     // updating an existing cipher. | ||||
|     LastKnownRevisionDate: Option<String>, | ||||
|     last_known_revision_date: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct PartialCipherData { | ||||
|     FolderId: Option<String>, | ||||
|     Favorite: bool, | ||||
|     folder_id: Option<String>, | ||||
|     favorite: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Attachments2Data { | ||||
|     FileName: String, | ||||
|     Key: String, | ||||
|     file_name: String, | ||||
|     key: String, | ||||
| } | ||||
|  | ||||
| /// Called when an org admin clones an org cipher. | ||||
| #[post("/ciphers/admin", data = "<data>")] | ||||
| async fn post_ciphers_admin( | ||||
|     data: JsonUpcase<ShareCipherData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
| async fn post_ciphers_admin(data: Json<ShareCipherData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     post_ciphers_create(data, headers, conn, nt).await | ||||
| } | ||||
|  | ||||
| @@ -277,25 +272,25 @@ async fn post_ciphers_admin( | ||||
| /// `organizationId` is null. | ||||
| #[post("/ciphers/create", data = "<data>")] | ||||
| async fn post_ciphers_create( | ||||
|     data: JsonUpcase<ShareCipherData>, | ||||
|     data: Json<ShareCipherData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
|     let mut data: ShareCipherData = data.into_inner().data; | ||||
|     let mut data: ShareCipherData = data.into_inner(); | ||||
|  | ||||
|     // Check if there are one more more collections selected when this cipher is part of an organization. | ||||
|     // err if this is not the case before creating an empty cipher. | ||||
|     if data.Cipher.OrganizationId.is_some() && data.CollectionIds.is_empty() { | ||||
|     if data.cipher.organization_id.is_some() && data.collection_ids.is_empty() { | ||||
|         err!("You must select at least one collection."); | ||||
|     } | ||||
|  | ||||
|     // This check is usually only needed in update_cipher_from_data(), but we | ||||
|     // need it here as well to avoid creating an empty cipher in the call to | ||||
|     // cipher.save() below. | ||||
|     enforce_personal_ownership_policy(Some(&data.Cipher), &headers, &mut conn).await?; | ||||
|     enforce_personal_ownership_policy(Some(&data.cipher), &headers, &mut conn).await?; | ||||
|  | ||||
|     let mut cipher = Cipher::new(data.Cipher.Type, data.Cipher.Name.clone()); | ||||
|     let mut cipher = Cipher::new(data.cipher.r#type, data.cipher.name.clone()); | ||||
|     cipher.user_uuid = Some(headers.user.uuid.clone()); | ||||
|     cipher.save(&mut conn).await?; | ||||
|  | ||||
| @@ -305,23 +300,23 @@ async fn post_ciphers_create( | ||||
|     // the current time, so the stale data check will end up failing down the | ||||
|     // line. Since this function only creates new ciphers (whether by cloning | ||||
|     // or otherwise), we can just ignore this field entirely. | ||||
|     data.Cipher.LastKnownRevisionDate = None; | ||||
|     data.cipher.last_known_revision_date = None; | ||||
|  | ||||
|     share_cipher_by_uuid(&cipher.uuid, data, &headers, &mut conn, &nt).await | ||||
| } | ||||
|  | ||||
| /// Called when creating a new user-owned cipher. | ||||
| #[post("/ciphers", data = "<data>")] | ||||
| async fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     let mut data: CipherData = data.into_inner().data; | ||||
| async fn post_ciphers(data: Json<CipherData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     let mut data: CipherData = data.into_inner(); | ||||
|  | ||||
|     // The web/browser clients set this field to null as expected, but the | ||||
|     // mobile clients seem to set the invalid value `0001-01-01T00:00:00`, | ||||
|     // which results in a warning message being logged. This field isn't | ||||
|     // needed when creating a new cipher, so just ignore it unconditionally. | ||||
|     data.LastKnownRevisionDate = None; | ||||
|     data.last_known_revision_date = None; | ||||
|  | ||||
|     let mut cipher = Cipher::new(data.Type, data.Name.clone()); | ||||
|     let mut cipher = Cipher::new(data.r#type, data.name.clone()); | ||||
|     update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherCreate).await?; | ||||
|  | ||||
|     Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) | ||||
| @@ -339,7 +334,7 @@ async fn enforce_personal_ownership_policy( | ||||
|     headers: &Headers, | ||||
|     conn: &mut DbConn, | ||||
| ) -> EmptyResult { | ||||
|     if data.is_none() || data.unwrap().OrganizationId.is_none() { | ||||
|     if data.is_none() || data.unwrap().organization_id.is_none() { | ||||
|         let user_uuid = &headers.user.uuid; | ||||
|         let policy_type = OrgPolicyType::PersonalOwnership; | ||||
|         if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, None, conn).await { | ||||
| @@ -363,7 +358,7 @@ pub async fn update_cipher_from_data( | ||||
|     // Check that the client isn't updating an existing cipher with stale data. | ||||
|     // And only perform this check when not importing ciphers, else the date/time check will fail. | ||||
|     if ut != UpdateType::None { | ||||
|         if let Some(dt) = data.LastKnownRevisionDate { | ||||
|         if let Some(dt) = data.last_known_revision_date { | ||||
|             match NaiveDateTime::parse_from_str(&dt, "%+") { | ||||
|                 // ISO 8601 format | ||||
|                 Err(err) => warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err), | ||||
| @@ -375,20 +370,20 @@ pub async fn update_cipher_from_data( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.OrganizationId { | ||||
|     if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.organization_id { | ||||
|         err!("Organization mismatch. Please resync the client before updating the cipher") | ||||
|     } | ||||
|  | ||||
|     if let Some(note) = &data.Notes { | ||||
|     if let Some(note) = &data.notes { | ||||
|         if note.len() > 10_000 { | ||||
|             err!("The field Notes exceeds the maximum encrypted value length of 10000 characters.") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Check if this cipher is being transferred from a personal to an organization vault | ||||
|     let transfer_cipher = cipher.organization_uuid.is_none() && data.OrganizationId.is_some(); | ||||
|     let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some(); | ||||
|  | ||||
|     if let Some(org_id) = data.OrganizationId { | ||||
|     if let Some(org_id) = data.organization_id { | ||||
|         match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await { | ||||
|             None => err!("You don't have permission to add item to organization"), | ||||
|             Some(org_user) => { | ||||
| @@ -412,7 +407,7 @@ pub async fn update_cipher_from_data( | ||||
|         cipher.user_uuid = Some(headers.user.uuid.clone()); | ||||
|     } | ||||
|  | ||||
|     if let Some(ref folder_id) = data.FolderId { | ||||
|     if let Some(ref folder_id) = data.folder_id { | ||||
|         match Folder::find_by_uuid(folder_id, conn).await { | ||||
|             Some(folder) => { | ||||
|                 if folder.user_uuid != headers.user.uuid { | ||||
| @@ -424,7 +419,7 @@ pub async fn update_cipher_from_data( | ||||
|     } | ||||
|  | ||||
|     // Modify attachments name and keys when rotating | ||||
|     if let Some(attachments) = data.Attachments2 { | ||||
|     if let Some(attachments) = data.attachments2 { | ||||
|         for (id, attachment) in attachments { | ||||
|             let mut saved_att = match Attachment::find_by_id(&id, conn).await { | ||||
|                 Some(att) => att, | ||||
| @@ -445,8 +440,8 @@ pub async fn update_cipher_from_data( | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             saved_att.akey = Some(attachment.Key); | ||||
|             saved_att.file_name = attachment.FileName; | ||||
|             saved_att.akey = Some(attachment.key); | ||||
|             saved_att.file_name = attachment.file_name; | ||||
|  | ||||
|             saved_att.save(conn).await?; | ||||
|         } | ||||
| @@ -460,44 +455,44 @@ pub async fn update_cipher_from_data( | ||||
|     fn _clean_cipher_data(mut json_data: Value) -> Value { | ||||
|         if json_data.is_array() { | ||||
|             json_data.as_array_mut().unwrap().iter_mut().for_each(|ref mut f| { | ||||
|                 f.as_object_mut().unwrap().remove("Response"); | ||||
|                 f.as_object_mut().unwrap().remove("response"); | ||||
|             }); | ||||
|         }; | ||||
|         json_data | ||||
|     } | ||||
|  | ||||
|     let type_data_opt = match data.Type { | ||||
|         1 => data.Login, | ||||
|         2 => data.SecureNote, | ||||
|         3 => data.Card, | ||||
|         4 => data.Identity, | ||||
|     let type_data_opt = match data.r#type { | ||||
|         1 => data.login, | ||||
|         2 => data.secure_note, | ||||
|         3 => data.card, | ||||
|         4 => data.identity, | ||||
|         _ => err!("Invalid type"), | ||||
|     }; | ||||
|  | ||||
|     let type_data = match type_data_opt { | ||||
|         Some(mut data) => { | ||||
|             // Remove the 'Response' key from the base object. | ||||
|             data.as_object_mut().unwrap().remove("Response"); | ||||
|             data.as_object_mut().unwrap().remove("response"); | ||||
|             // Remove the 'Response' key from every Uri. | ||||
|             if data["Uris"].is_array() { | ||||
|                 data["Uris"] = _clean_cipher_data(data["Uris"].clone()); | ||||
|             if data["uris"].is_array() { | ||||
|                 data["uris"] = _clean_cipher_data(data["uris"].clone()); | ||||
|             } | ||||
|             data | ||||
|         } | ||||
|         None => err!("Data missing"), | ||||
|     }; | ||||
|  | ||||
|     cipher.key = data.Key; | ||||
|     cipher.name = data.Name; | ||||
|     cipher.notes = data.Notes; | ||||
|     cipher.fields = data.Fields.map(|f| _clean_cipher_data(f).to_string()); | ||||
|     cipher.key = data.key; | ||||
|     cipher.name = data.name; | ||||
|     cipher.notes = data.notes; | ||||
|     cipher.fields = data.fields.map(|f| _clean_cipher_data(f).to_string()); | ||||
|     cipher.data = type_data.to_string(); | ||||
|     cipher.password_history = data.PasswordHistory.map(|f| f.to_string()); | ||||
|     cipher.reprompt = data.Reprompt; | ||||
|     cipher.password_history = data.password_history.map(|f| f.to_string()); | ||||
|     cipher.reprompt = data.reprompt; | ||||
|  | ||||
|     cipher.save(conn).await?; | ||||
|     cipher.move_to_folder(data.FolderId, &headers.user.uuid, conn).await?; | ||||
|     cipher.set_favorite(data.Favorite, &headers.user.uuid, conn).await?; | ||||
|     cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?; | ||||
|     cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?; | ||||
|  | ||||
|     if ut != UpdateType::None { | ||||
|         // Only log events for organizational ciphers | ||||
| @@ -533,43 +528,43 @@ pub async fn update_cipher_from_data( | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct ImportData { | ||||
|     Ciphers: Vec<CipherData>, | ||||
|     Folders: Vec<FolderData>, | ||||
|     FolderRelationships: Vec<RelationsData>, | ||||
|     ciphers: Vec<CipherData>, | ||||
|     folders: Vec<FolderData>, | ||||
|     folder_relationships: Vec<RelationsData>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct RelationsData { | ||||
|     // Cipher id | ||||
|     Key: usize, | ||||
|     key: usize, | ||||
|     // Folder id | ||||
|     Value: usize, | ||||
|     value: usize, | ||||
| } | ||||
|  | ||||
| #[post("/ciphers/import", data = "<data>")] | ||||
| async fn post_ciphers_import( | ||||
|     data: JsonUpcase<ImportData>, | ||||
|     data: Json<ImportData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
|     enforce_personal_ownership_policy(None, &headers, &mut conn).await?; | ||||
|  | ||||
|     let data: ImportData = data.into_inner().data; | ||||
|     let data: ImportData = data.into_inner(); | ||||
|  | ||||
|     // 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)?; | ||||
|     Cipher::validate_notes(&data.ciphers)?; | ||||
|  | ||||
|     // Read and create the folders | ||||
|     let mut folders: Vec<_> = Vec::new(); | ||||
|     for folder in data.Folders.into_iter() { | ||||
|         let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.Name); | ||||
|     for folder in data.folders.into_iter() { | ||||
|         let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name); | ||||
|         new_folder.save(&mut conn).await?; | ||||
|  | ||||
|         folders.push(new_folder); | ||||
| @@ -578,16 +573,16 @@ async fn post_ciphers_import( | ||||
|     // Read the relations between folders and ciphers | ||||
|     let mut relations_map = HashMap::new(); | ||||
|  | ||||
|     for relation in data.FolderRelationships { | ||||
|         relations_map.insert(relation.Key, relation.Value); | ||||
|     for relation in data.folder_relationships { | ||||
|         relations_map.insert(relation.key, relation.value); | ||||
|     } | ||||
|  | ||||
|     // Read and create the ciphers | ||||
|     for (index, mut cipher_data) in data.Ciphers.into_iter().enumerate() { | ||||
|     for (index, mut cipher_data) in data.ciphers.into_iter().enumerate() { | ||||
|         let folder_uuid = relations_map.get(&index).map(|i| folders[*i].uuid.clone()); | ||||
|         cipher_data.FolderId = folder_uuid; | ||||
|         cipher_data.folder_id = folder_uuid; | ||||
|  | ||||
|         let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); | ||||
|         let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone()); | ||||
|         update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await?; | ||||
|     } | ||||
|  | ||||
| @@ -602,7 +597,7 @@ async fn post_ciphers_import( | ||||
| #[put("/ciphers/<uuid>/admin", data = "<data>")] | ||||
| async fn put_cipher_admin( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<CipherData>, | ||||
|     data: Json<CipherData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -613,7 +608,7 @@ async fn put_cipher_admin( | ||||
| #[post("/ciphers/<uuid>/admin", data = "<data>")] | ||||
| async fn post_cipher_admin( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<CipherData>, | ||||
|     data: Json<CipherData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -622,25 +617,19 @@ async fn post_cipher_admin( | ||||
| } | ||||
|  | ||||
| #[post("/ciphers/<uuid>", data = "<data>")] | ||||
| async fn post_cipher( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<CipherData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
| async fn post_cipher(uuid: &str, data: Json<CipherData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     put_cipher(uuid, data, headers, conn, nt).await | ||||
| } | ||||
|  | ||||
| #[put("/ciphers/<uuid>", data = "<data>")] | ||||
| async fn put_cipher( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<CipherData>, | ||||
|     data: Json<CipherData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
|     let data: CipherData = data.into_inner().data; | ||||
|     let data: CipherData = data.into_inner(); | ||||
|  | ||||
|     let mut cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { | ||||
|         Some(cipher) => cipher, | ||||
| @@ -662,12 +651,7 @@ async fn put_cipher( | ||||
| } | ||||
|  | ||||
| #[post("/ciphers/<uuid>/partial", data = "<data>")] | ||||
| async fn post_cipher_partial( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<PartialCipherData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
| ) -> JsonResult { | ||||
| async fn post_cipher_partial(uuid: &str, data: Json<PartialCipherData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
|     put_cipher_partial(uuid, data, headers, conn).await | ||||
| } | ||||
|  | ||||
| @@ -675,18 +659,18 @@ async fn post_cipher_partial( | ||||
| #[put("/ciphers/<uuid>/partial", data = "<data>")] | ||||
| async fn put_cipher_partial( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<PartialCipherData>, | ||||
|     data: Json<PartialCipherData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
|     let data: PartialCipherData = data.into_inner().data; | ||||
|     let data: PartialCipherData = data.into_inner(); | ||||
|  | ||||
|     let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { | ||||
|         Some(cipher) => cipher, | ||||
|         None => err!("Cipher doesn't exist"), | ||||
|     }; | ||||
|  | ||||
|     if let Some(ref folder_id) = data.FolderId { | ||||
|     if let Some(ref folder_id) = data.folder_id { | ||||
|         match Folder::find_by_uuid(folder_id, &mut conn).await { | ||||
|             Some(folder) => { | ||||
|                 if folder.user_uuid != headers.user.uuid { | ||||
| @@ -698,23 +682,23 @@ async fn put_cipher_partial( | ||||
|     } | ||||
|  | ||||
|     // Move cipher | ||||
|     cipher.move_to_folder(data.FolderId.clone(), &headers.user.uuid, &mut conn).await?; | ||||
|     cipher.move_to_folder(data.folder_id.clone(), &headers.user.uuid, &mut conn).await?; | ||||
|     // Update favorite | ||||
|     cipher.set_favorite(Some(data.Favorite), &headers.user.uuid, &mut conn).await?; | ||||
|     cipher.set_favorite(Some(data.favorite), &headers.user.uuid, &mut conn).await?; | ||||
|  | ||||
|     Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct CollectionsAdminData { | ||||
|     CollectionIds: Vec<String>, | ||||
|     collection_ids: Vec<String>, | ||||
| } | ||||
|  | ||||
| #[put("/ciphers/<uuid>/collections", data = "<data>")] | ||||
| async fn put_collections_update( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<CollectionsAdminData>, | ||||
|     data: Json<CollectionsAdminData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -725,7 +709,7 @@ async fn put_collections_update( | ||||
| #[post("/ciphers/<uuid>/collections", data = "<data>")] | ||||
| async fn post_collections_update( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<CollectionsAdminData>, | ||||
|     data: Json<CollectionsAdminData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -736,7 +720,7 @@ async fn post_collections_update( | ||||
| #[put("/ciphers/<uuid>/collections-admin", data = "<data>")] | ||||
| async fn put_collections_admin( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<CollectionsAdminData>, | ||||
|     data: Json<CollectionsAdminData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -747,12 +731,12 @@ async fn put_collections_admin( | ||||
| #[post("/ciphers/<uuid>/collections-admin", data = "<data>")] | ||||
| async fn post_collections_admin( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<CollectionsAdminData>, | ||||
|     data: Json<CollectionsAdminData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
|     let data: CollectionsAdminData = data.into_inner().data; | ||||
|     let data: CollectionsAdminData = data.into_inner(); | ||||
|  | ||||
|     let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { | ||||
|         Some(cipher) => cipher, | ||||
| @@ -763,7 +747,7 @@ async fn post_collections_admin( | ||||
|         err!("Cipher is not write accessible") | ||||
|     } | ||||
|  | ||||
|     let posted_collections: HashSet<String> = data.CollectionIds.iter().cloned().collect(); | ||||
|     let posted_collections: HashSet<String> = data.collection_ids.iter().cloned().collect(); | ||||
|     let current_collections: HashSet<String> = | ||||
|         cipher.get_collections(headers.user.uuid.clone(), &mut conn).await.iter().cloned().collect(); | ||||
|  | ||||
| @@ -811,21 +795,21 @@ async fn post_collections_admin( | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct ShareCipherData { | ||||
|     Cipher: CipherData, | ||||
|     CollectionIds: Vec<String>, | ||||
|     cipher: CipherData, | ||||
|     collection_ids: Vec<String>, | ||||
| } | ||||
|  | ||||
| #[post("/ciphers/<uuid>/share", data = "<data>")] | ||||
| async fn post_cipher_share( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<ShareCipherData>, | ||||
|     data: Json<ShareCipherData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
|     let data: ShareCipherData = data.into_inner().data; | ||||
|     let data: ShareCipherData = data.into_inner(); | ||||
|  | ||||
|     share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await | ||||
| } | ||||
| @@ -833,53 +817,53 @@ async fn post_cipher_share( | ||||
| #[put("/ciphers/<uuid>/share", data = "<data>")] | ||||
| async fn put_cipher_share( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<ShareCipherData>, | ||||
|     data: Json<ShareCipherData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
|     let data: ShareCipherData = data.into_inner().data; | ||||
|     let data: ShareCipherData = data.into_inner(); | ||||
|  | ||||
|     share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct ShareSelectedCipherData { | ||||
|     Ciphers: Vec<CipherData>, | ||||
|     CollectionIds: Vec<String>, | ||||
|     ciphers: Vec<CipherData>, | ||||
|     collection_ids: Vec<String>, | ||||
| } | ||||
|  | ||||
| #[put("/ciphers/share", data = "<data>")] | ||||
| async fn put_cipher_share_selected( | ||||
|     data: JsonUpcase<ShareSelectedCipherData>, | ||||
|     data: Json<ShareSelectedCipherData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
|     let mut data: ShareSelectedCipherData = data.into_inner().data; | ||||
|     let mut data: ShareSelectedCipherData = data.into_inner(); | ||||
|  | ||||
|     if data.Ciphers.is_empty() { | ||||
|     if data.ciphers.is_empty() { | ||||
|         err!("You must select at least one cipher.") | ||||
|     } | ||||
|  | ||||
|     if data.CollectionIds.is_empty() { | ||||
|     if data.collection_ids.is_empty() { | ||||
|         err!("You must select at least one collection.") | ||||
|     } | ||||
|  | ||||
|     for cipher in data.Ciphers.iter() { | ||||
|         if cipher.Id.is_none() { | ||||
|     for cipher in data.ciphers.iter() { | ||||
|         if cipher.id.is_none() { | ||||
|             err!("Request missing ids field") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     while let Some(cipher) = data.Ciphers.pop() { | ||||
|     while let Some(cipher) = data.ciphers.pop() { | ||||
|         let mut shared_cipher_data = ShareCipherData { | ||||
|             Cipher: cipher, | ||||
|             CollectionIds: data.CollectionIds.clone(), | ||||
|             cipher, | ||||
|             collection_ids: data.collection_ids.clone(), | ||||
|         }; | ||||
|  | ||||
|         match shared_cipher_data.Cipher.Id.take() { | ||||
|         match shared_cipher_data.cipher.id.take() { | ||||
|             Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &mut conn, &nt).await?, | ||||
|             None => err!("Request missing ids field"), | ||||
|         }; | ||||
| @@ -908,8 +892,8 @@ async fn share_cipher_by_uuid( | ||||
|  | ||||
|     let mut shared_to_collections = vec![]; | ||||
|  | ||||
|     if let Some(organization_uuid) = &data.Cipher.OrganizationId { | ||||
|         for uuid in &data.CollectionIds { | ||||
|     if let Some(organization_uuid) = &data.cipher.organization_id { | ||||
|         for uuid in &data.collection_ids { | ||||
|             match Collection::find_by_uuid_and_org(uuid, organization_uuid, conn).await { | ||||
|                 None => err!("Invalid collection ID provided"), | ||||
|                 Some(collection) => { | ||||
| @@ -925,13 +909,13 @@ async fn share_cipher_by_uuid( | ||||
|     }; | ||||
|  | ||||
|     // When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate. | ||||
|     let ut = if data.Cipher.LastKnownRevisionDate.is_some() { | ||||
|     let ut = if data.cipher.last_known_revision_date.is_some() { | ||||
|         UpdateType::SyncCipherUpdate | ||||
|     } else { | ||||
|         UpdateType::SyncCipherCreate | ||||
|     }; | ||||
|  | ||||
|     update_cipher_from_data(&mut cipher, data.Cipher, headers, Some(shared_to_collections), conn, nt, ut).await?; | ||||
|     update_cipher_from_data(&mut cipher, data.cipher, headers, Some(shared_to_collections), conn, nt, ut).await?; | ||||
|  | ||||
|     Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await)) | ||||
| } | ||||
| @@ -961,12 +945,12 @@ async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut c | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct AttachmentRequestData { | ||||
|     Key: String, | ||||
|     FileName: String, | ||||
|     FileSize: NumberOrString, | ||||
|     AdminRequest: Option<bool>, // true when attaching from an org vault view | ||||
|     key: String, | ||||
|     file_name: String, | ||||
|     file_size: NumberOrString, | ||||
|     admin_request: Option<bool>, // true when attaching from an org vault view | ||||
| } | ||||
|  | ||||
| enum FileUploadType { | ||||
| @@ -981,7 +965,7 @@ enum FileUploadType { | ||||
| #[post("/ciphers/<uuid>/attachment/v2", data = "<data>")] | ||||
| async fn post_attachment_v2( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<AttachmentRequestData>, | ||||
|     data: Json<AttachmentRequestData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
| @@ -994,28 +978,28 @@ async fn post_attachment_v2( | ||||
|         err!("Cipher is not write accessible") | ||||
|     } | ||||
|  | ||||
|     let data: AttachmentRequestData = data.into_inner().data; | ||||
|     let file_size = data.FileSize.into_i64()?; | ||||
|     let data: AttachmentRequestData = data.into_inner(); | ||||
|     let file_size = data.file_size.into_i64()?; | ||||
|  | ||||
|     if file_size < 0 { | ||||
|         err!("Attachment size can't be negative") | ||||
|     } | ||||
|     let attachment_id = crypto::generate_attachment_id(); | ||||
|     let attachment = | ||||
|         Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.FileName, file_size, Some(data.Key)); | ||||
|         Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.file_name, file_size, Some(data.key)); | ||||
|     attachment.save(&mut conn).await.expect("Error saving attachment"); | ||||
|  | ||||
|     let url = format!("/ciphers/{}/attachment/{}", cipher.uuid, attachment_id); | ||||
|     let response_key = match data.AdminRequest { | ||||
|         Some(b) if b => "CipherMiniResponse", | ||||
|         _ => "CipherResponse", | ||||
|     let response_key = match data.admin_request { | ||||
|         Some(b) if b => "cipherMiniResponse", | ||||
|         _ => "cipherResponse", | ||||
|     }; | ||||
|  | ||||
|     Ok(Json(json!({ // AttachmentUploadDataResponseModel | ||||
|         "Object": "attachment-fileUpload", | ||||
|         "AttachmentId": attachment_id, | ||||
|         "Url": url, | ||||
|         "FileUploadType": FileUploadType::Direct as i32, | ||||
|         "object": "attachment-fileUpload", | ||||
|         "attachmentId": attachment_id, | ||||
|         "url": url, | ||||
|         "fileUploadType": FileUploadType::Direct as i32, | ||||
|         response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await, | ||||
|     }))) | ||||
| } | ||||
| @@ -1350,38 +1334,23 @@ async fn delete_cipher_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: | ||||
| } | ||||
|  | ||||
| #[delete("/ciphers", data = "<data>")] | ||||
| async fn delete_cipher_selected( | ||||
|     data: JsonUpcase<Value>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
| async fn delete_cipher_selected(data: Json<Value>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { | ||||
|     _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete | ||||
| } | ||||
|  | ||||
| #[post("/ciphers/delete", data = "<data>")] | ||||
| async fn delete_cipher_selected_post( | ||||
|     data: JsonUpcase<Value>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
| async fn delete_cipher_selected_post(data: Json<Value>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { | ||||
|     _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete | ||||
| } | ||||
|  | ||||
| #[put("/ciphers/delete", data = "<data>")] | ||||
| async fn delete_cipher_selected_put( | ||||
|     data: JsonUpcase<Value>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
| async fn delete_cipher_selected_put(data: Json<Value>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { | ||||
|     _delete_multiple_ciphers(data, headers, conn, true, nt).await // soft delete | ||||
| } | ||||
|  | ||||
| #[delete("/ciphers/admin", data = "<data>")] | ||||
| async fn delete_cipher_selected_admin( | ||||
|     data: JsonUpcase<Value>, | ||||
|     data: Json<Value>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -1391,7 +1360,7 @@ async fn delete_cipher_selected_admin( | ||||
|  | ||||
| #[post("/ciphers/delete-admin", data = "<data>")] | ||||
| async fn delete_cipher_selected_post_admin( | ||||
|     data: JsonUpcase<Value>, | ||||
|     data: Json<Value>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -1401,7 +1370,7 @@ async fn delete_cipher_selected_post_admin( | ||||
|  | ||||
| #[put("/ciphers/delete-admin", data = "<data>")] | ||||
| async fn delete_cipher_selected_put_admin( | ||||
|     data: JsonUpcase<Value>, | ||||
|     data: Json<Value>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -1420,33 +1389,28 @@ async fn restore_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn | ||||
| } | ||||
|  | ||||
| #[put("/ciphers/restore", data = "<data>")] | ||||
| async fn restore_cipher_selected( | ||||
|     data: JsonUpcase<Value>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
| async fn restore_cipher_selected(data: Json<Value>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     _restore_multiple_ciphers(data, &headers, &mut conn, &nt).await | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct MoveCipherData { | ||||
|     FolderId: Option<String>, | ||||
|     Ids: Vec<String>, | ||||
|     folder_id: Option<String>, | ||||
|     ids: Vec<String>, | ||||
| } | ||||
|  | ||||
| #[post("/ciphers/move", data = "<data>")] | ||||
| async fn move_cipher_selected( | ||||
|     data: JsonUpcase<MoveCipherData>, | ||||
|     data: Json<MoveCipherData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
|     let data = data.into_inner().data; | ||||
|     let data = data.into_inner(); | ||||
|     let user_uuid = headers.user.uuid; | ||||
|  | ||||
|     if let Some(ref folder_id) = data.FolderId { | ||||
|     if let Some(ref folder_id) = data.folder_id { | ||||
|         match Folder::find_by_uuid(folder_id, &mut conn).await { | ||||
|             Some(folder) => { | ||||
|                 if folder.user_uuid != user_uuid { | ||||
| @@ -1457,7 +1421,7 @@ async fn move_cipher_selected( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     for uuid in data.Ids { | ||||
|     for uuid in data.ids { | ||||
|         let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await { | ||||
|             Some(cipher) => cipher, | ||||
|             None => err!("Cipher doesn't exist"), | ||||
| @@ -1468,7 +1432,7 @@ async fn move_cipher_selected( | ||||
|         } | ||||
|  | ||||
|         // Move cipher | ||||
|         cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &mut conn).await?; | ||||
|         cipher.move_to_folder(data.folder_id.clone(), &user_uuid, &mut conn).await?; | ||||
|  | ||||
|         nt.send_cipher_update( | ||||
|             UpdateType::SyncCipherUpdate, | ||||
| @@ -1486,7 +1450,7 @@ async fn move_cipher_selected( | ||||
|  | ||||
| #[put("/ciphers/move", data = "<data>")] | ||||
| async fn move_cipher_selected_put( | ||||
|     data: JsonUpcase<MoveCipherData>, | ||||
|     data: Json<MoveCipherData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -1503,12 +1467,12 @@ struct OrganizationId { | ||||
| #[post("/ciphers/purge?<organization..>", data = "<data>")] | ||||
| async fn delete_all( | ||||
|     organization: Option<OrganizationId>, | ||||
|     data: JsonUpcase<PasswordOrOtpData>, | ||||
|     data: Json<PasswordOrOtpData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner().data; | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let mut user = headers.user; | ||||
|  | ||||
|     data.validate(&user, true, &mut conn).await?; | ||||
| @@ -1616,13 +1580,13 @@ async fn _delete_cipher_by_uuid( | ||||
| } | ||||
|  | ||||
| async fn _delete_multiple_ciphers( | ||||
|     data: JsonUpcase<Value>, | ||||
|     data: Json<Value>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     soft_delete: bool, | ||||
|     nt: Notify<'_>, | ||||
| ) -> EmptyResult { | ||||
|     let data: Value = data.into_inner().data; | ||||
|     let data: Value = data.into_inner(); | ||||
|  | ||||
|     let uuids = match data.get("Ids") { | ||||
|         Some(ids) => match ids.as_array() { | ||||
| @@ -1681,12 +1645,12 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon | ||||
| } | ||||
|  | ||||
| async fn _restore_multiple_ciphers( | ||||
|     data: JsonUpcase<Value>, | ||||
|     data: Json<Value>, | ||||
|     headers: &Headers, | ||||
|     conn: &mut DbConn, | ||||
|     nt: &Notify<'_>, | ||||
| ) -> JsonResult { | ||||
|     let data: Value = data.into_inner().data; | ||||
|     let data: Value = data.into_inner(); | ||||
|  | ||||
|     let uuids = match data.get("Ids") { | ||||
|         Some(ids) => match ids.as_array() { | ||||
| @@ -1705,9 +1669,9 @@ async fn _restore_multiple_ciphers( | ||||
|     } | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|       "Data": ciphers, | ||||
|       "Object": "list", | ||||
|       "ContinuationToken": null | ||||
|       "data": ciphers, | ||||
|       "object": "list", | ||||
|       "continuationToken": null | ||||
|     }))) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ use serde_json::Value; | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::{CipherSyncData, CipherSyncType}, | ||||
|         EmptyResult, JsonResult, JsonUpcase, | ||||
|         EmptyResult, JsonResult, | ||||
|     }, | ||||
|     auth::{decode_emergency_access_invite, Headers}, | ||||
|     db::{models::*, DbConn, DbPool}, | ||||
| @@ -43,19 +43,19 @@ pub fn routes() -> Vec<Route> { | ||||
| async fn get_contacts(headers: Headers, mut conn: DbConn) -> Json<Value> { | ||||
|     if !CONFIG.emergency_access_allowed() { | ||||
|         return Json(json!({ | ||||
|             "Data": [{ | ||||
|                 "Id": "", | ||||
|                 "Status": 2, | ||||
|                 "Type": 0, | ||||
|                 "WaitTimeDays": 0, | ||||
|                 "GranteeId": "", | ||||
|                 "Email": "", | ||||
|                 "Name": "NOTE: Emergency Access is disabled!", | ||||
|                 "Object": "emergencyAccessGranteeDetails", | ||||
|             "data": [{ | ||||
|                 "id": "", | ||||
|                 "status": 2, | ||||
|                 "type": 0, | ||||
|                 "waitTimeDays": 0, | ||||
|                 "granteeId": "", | ||||
|                 "email": "", | ||||
|                 "name": "NOTE: Emergency Access is disabled!", | ||||
|                 "object": "emergencyAccessGranteeDetails", | ||||
|  | ||||
|             }], | ||||
|             "Object": "list", | ||||
|             "ContinuationToken": null | ||||
|             "object": "list", | ||||
|             "continuationToken": null | ||||
|         })); | ||||
|     } | ||||
|     let emergency_access_list = EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &mut conn).await; | ||||
| @@ -67,9 +67,9 @@ async fn get_contacts(headers: Headers, mut conn: DbConn) -> Json<Value> { | ||||
|     } | ||||
|  | ||||
|     Json(json!({ | ||||
|       "Data": emergency_access_list_json, | ||||
|       "Object": "list", | ||||
|       "ContinuationToken": null | ||||
|       "data": emergency_access_list_json, | ||||
|       "object": "list", | ||||
|       "continuationToken": null | ||||
|     })) | ||||
| } | ||||
|  | ||||
| @@ -86,9 +86,9 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> Json<Value> { | ||||
|     } | ||||
|  | ||||
|     Json(json!({ | ||||
|       "Data": emergency_access_list_json, | ||||
|       "Object": "list", | ||||
|       "ContinuationToken": null | ||||
|       "data": emergency_access_list_json, | ||||
|       "object": "list", | ||||
|       "continuationToken": null | ||||
|     })) | ||||
| } | ||||
|  | ||||
| @@ -109,42 +109,38 @@ async fn get_emergency_access(emer_id: &str, mut conn: DbConn) -> JsonResult { | ||||
| // region put/post | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EmergencyAccessUpdateData { | ||||
|     Type: NumberOrString, | ||||
|     WaitTimeDays: i32, | ||||
|     KeyEncrypted: Option<String>, | ||||
|     r#type: NumberOrString, | ||||
|     wait_time_days: i32, | ||||
|     key_encrypted: Option<String>, | ||||
| } | ||||
|  | ||||
| #[put("/emergency-access/<emer_id>", data = "<data>")] | ||||
| async fn put_emergency_access(emer_id: &str, data: JsonUpcase<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult { | ||||
| async fn put_emergency_access(emer_id: &str, data: Json<EmergencyAccessUpdateData>, conn: DbConn) -> JsonResult { | ||||
|     post_emergency_access(emer_id, data, conn).await | ||||
| } | ||||
|  | ||||
| #[post("/emergency-access/<emer_id>", data = "<data>")] | ||||
| async fn post_emergency_access( | ||||
|     emer_id: &str, | ||||
|     data: JsonUpcase<EmergencyAccessUpdateData>, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
| async fn post_emergency_access(emer_id: &str, data: Json<EmergencyAccessUpdateData>, mut conn: DbConn) -> JsonResult { | ||||
|     check_emergency_access_enabled()?; | ||||
|  | ||||
|     let data: EmergencyAccessUpdateData = data.into_inner().data; | ||||
|     let data: EmergencyAccessUpdateData = data.into_inner(); | ||||
|  | ||||
|     let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { | ||||
|         Some(emergency_access) => emergency_access, | ||||
|         None => err!("Emergency access not valid."), | ||||
|     }; | ||||
|  | ||||
|     let new_type = match EmergencyAccessType::from_str(&data.Type.into_string()) { | ||||
|     let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) { | ||||
|         Some(new_type) => new_type as i32, | ||||
|         None => err!("Invalid emergency access type."), | ||||
|     }; | ||||
|  | ||||
|     emergency_access.atype = new_type; | ||||
|     emergency_access.wait_time_days = data.WaitTimeDays; | ||||
|     if data.KeyEncrypted.is_some() { | ||||
|         emergency_access.key_encrypted = data.KeyEncrypted; | ||||
|     emergency_access.wait_time_days = data.wait_time_days; | ||||
|     if data.key_encrypted.is_some() { | ||||
|         emergency_access.key_encrypted = data.key_encrypted; | ||||
|     } | ||||
|  | ||||
|     emergency_access.save(&mut conn).await?; | ||||
| @@ -184,24 +180,24 @@ async fn post_delete_emergency_access(emer_id: &str, headers: Headers, conn: DbC | ||||
| // region invite | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EmergencyAccessInviteData { | ||||
|     Email: String, | ||||
|     Type: NumberOrString, | ||||
|     WaitTimeDays: i32, | ||||
|     email: String, | ||||
|     r#type: NumberOrString, | ||||
|     wait_time_days: i32, | ||||
| } | ||||
|  | ||||
| #[post("/emergency-access/invite", data = "<data>")] | ||||
| async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
| async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
|     check_emergency_access_enabled()?; | ||||
|  | ||||
|     let data: EmergencyAccessInviteData = data.into_inner().data; | ||||
|     let email = data.Email.to_lowercase(); | ||||
|     let wait_time_days = data.WaitTimeDays; | ||||
|     let data: EmergencyAccessInviteData = data.into_inner(); | ||||
|     let email = data.email.to_lowercase(); | ||||
|     let wait_time_days = data.wait_time_days; | ||||
|  | ||||
|     let emergency_access_status = EmergencyAccessStatus::Invited as i32; | ||||
|  | ||||
|     let new_type = match EmergencyAccessType::from_str(&data.Type.into_string()) { | ||||
|     let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) { | ||||
|         Some(new_type) => new_type as i32, | ||||
|         None => err!("Invalid emergency access type."), | ||||
|     }; | ||||
| @@ -319,17 +315,17 @@ async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> Emp | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct AcceptData { | ||||
|     Token: String, | ||||
|     token: String, | ||||
| } | ||||
|  | ||||
| #[post("/emergency-access/<emer_id>/accept", data = "<data>")] | ||||
| async fn accept_invite(emer_id: &str, data: JsonUpcase<AcceptData>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
| async fn accept_invite(emer_id: &str, data: Json<AcceptData>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
|     check_emergency_access_enabled()?; | ||||
|  | ||||
|     let data: AcceptData = data.into_inner().data; | ||||
|     let token = &data.Token; | ||||
|     let data: AcceptData = data.into_inner(); | ||||
|     let token = &data.token; | ||||
|     let claims = decode_emergency_access_invite(token)?; | ||||
|  | ||||
|     // This can happen if the user who received the invite used a different email to signup. | ||||
| @@ -374,23 +370,23 @@ async fn accept_invite(emer_id: &str, data: JsonUpcase<AcceptData>, headers: Hea | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct ConfirmData { | ||||
|     Key: String, | ||||
|     key: String, | ||||
| } | ||||
|  | ||||
| #[post("/emergency-access/<emer_id>/confirm", data = "<data>")] | ||||
| async fn confirm_emergency_access( | ||||
|     emer_id: &str, | ||||
|     data: JsonUpcase<ConfirmData>, | ||||
|     data: Json<ConfirmData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
|     check_emergency_access_enabled()?; | ||||
|  | ||||
|     let confirming_user = headers.user; | ||||
|     let data: ConfirmData = data.into_inner().data; | ||||
|     let key = data.Key; | ||||
|     let data: ConfirmData = data.into_inner(); | ||||
|     let key = data.key; | ||||
|  | ||||
|     let mut emergency_access = match EmergencyAccess::find_by_uuid(emer_id, &mut conn).await { | ||||
|         Some(emer) => emer, | ||||
| @@ -585,9 +581,9 @@ async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn | ||||
|     } | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|       "Ciphers": ciphers_json, | ||||
|       "KeyEncrypted": &emergency_access.key_encrypted, | ||||
|       "Object": "emergencyAccessView", | ||||
|       "ciphers": ciphers_json, | ||||
|       "keyEncrypted": &emergency_access.key_encrypted, | ||||
|       "object": "emergencyAccessView", | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| @@ -611,35 +607,35 @@ async fn takeover_emergency_access(emer_id: &str, headers: Headers, mut conn: Db | ||||
|     }; | ||||
|  | ||||
|     let result = json!({ | ||||
|         "Kdf": grantor_user.client_kdf_type, | ||||
|         "KdfIterations": grantor_user.client_kdf_iter, | ||||
|         "KdfMemory": grantor_user.client_kdf_memory, | ||||
|         "KdfParallelism": grantor_user.client_kdf_parallelism, | ||||
|         "KeyEncrypted": &emergency_access.key_encrypted, | ||||
|         "Object": "emergencyAccessTakeover", | ||||
|         "kdf": grantor_user.client_kdf_type, | ||||
|         "kdfIterations": grantor_user.client_kdf_iter, | ||||
|         "kdfMemory": grantor_user.client_kdf_memory, | ||||
|         "kdfParallelism": grantor_user.client_kdf_parallelism, | ||||
|         "keyEncrypted": &emergency_access.key_encrypted, | ||||
|         "object": "emergencyAccessTakeover", | ||||
|     }); | ||||
|  | ||||
|     Ok(Json(result)) | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EmergencyAccessPasswordData { | ||||
|     NewMasterPasswordHash: String, | ||||
|     Key: String, | ||||
|     new_master_password_hash: String, | ||||
|     key: String, | ||||
| } | ||||
|  | ||||
| #[post("/emergency-access/<emer_id>/password", data = "<data>")] | ||||
| async fn password_emergency_access( | ||||
|     emer_id: &str, | ||||
|     data: JsonUpcase<EmergencyAccessPasswordData>, | ||||
|     data: Json<EmergencyAccessPasswordData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
| ) -> EmptyResult { | ||||
|     check_emergency_access_enabled()?; | ||||
|  | ||||
|     let data: EmergencyAccessPasswordData = data.into_inner().data; | ||||
|     let new_master_password_hash = &data.NewMasterPasswordHash; | ||||
|     let data: EmergencyAccessPasswordData = data.into_inner(); | ||||
|     let new_master_password_hash = &data.new_master_password_hash; | ||||
|     //let key = &data.Key; | ||||
|  | ||||
|     let requesting_user = headers.user; | ||||
| @@ -658,7 +654,7 @@ async fn password_emergency_access( | ||||
|     }; | ||||
|  | ||||
|     // change grantor_user password | ||||
|     grantor_user.set_password(new_master_password_hash, Some(data.Key), true, None); | ||||
|     grantor_user.set_password(new_master_password_hash, Some(data.key), true, None); | ||||
|     grantor_user.save(&mut conn).await?; | ||||
|  | ||||
|     // Disable TwoFactor providers since they will otherwise block logins | ||||
| @@ -696,9 +692,9 @@ async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: Db | ||||
|     let policies_json: Vec<Value> = policies.await.iter().map(OrgPolicy::to_json).collect(); | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Data": policies_json, | ||||
|         "Object": "list", | ||||
|         "ContinuationToken": null | ||||
|         "data": policies_json, | ||||
|         "object": "list", | ||||
|         "continuationToken": null | ||||
|     }))) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ use rocket::{form::FromForm, serde::json::Json, Route}; | ||||
| use serde_json::Value; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{EmptyResult, JsonResult, JsonUpcaseVec}, | ||||
|     api::{EmptyResult, JsonResult}, | ||||
|     auth::{AdminHeaders, Headers}, | ||||
|     db::{ | ||||
|         models::{Cipher, Event, UserOrganization}, | ||||
| @@ -22,7 +22,6 @@ pub fn routes() -> Vec<Route> { | ||||
| } | ||||
|  | ||||
| #[derive(FromForm)] | ||||
| #[allow(non_snake_case)] | ||||
| struct EventRange { | ||||
|     start: String, | ||||
|     end: String, | ||||
| @@ -53,9 +52,9 @@ async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, | ||||
|     }; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Data": events_json, | ||||
|         "Object": "list", | ||||
|         "ContinuationToken": get_continuation_token(&events_json), | ||||
|         "data": events_json, | ||||
|         "object": "list", | ||||
|         "continuationToken": get_continuation_token(&events_json), | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| @@ -85,9 +84,9 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, | ||||
|     }; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Data": events_json, | ||||
|         "Object": "list", | ||||
|         "ContinuationToken": get_continuation_token(&events_json), | ||||
|         "data": events_json, | ||||
|         "object": "list", | ||||
|         "continuationToken": get_continuation_token(&events_json), | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| @@ -119,9 +118,9 @@ async fn get_user_events( | ||||
|     }; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Data": events_json, | ||||
|         "Object": "list", | ||||
|         "ContinuationToken": get_continuation_token(&events_json), | ||||
|         "data": events_json, | ||||
|         "object": "list", | ||||
|         "continuationToken": get_continuation_token(&events_json), | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| @@ -145,33 +144,33 @@ pub fn main_routes() -> Vec<Route> { | ||||
|     routes![post_events_collect,] | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EventCollection { | ||||
|     // Mandatory | ||||
|     Type: i32, | ||||
|     Date: String, | ||||
|     r#type: i32, | ||||
|     date: String, | ||||
|  | ||||
|     // Optional | ||||
|     CipherId: Option<String>, | ||||
|     OrganizationId: Option<String>, | ||||
|     cipher_id: Option<String>, | ||||
|     organization_id: Option<String>, | ||||
| } | ||||
|  | ||||
| // Upstream: | ||||
| // https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Events/Controllers/CollectController.cs | ||||
| // https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs | ||||
| #[post("/collect", format = "application/json", data = "<data>")] | ||||
| async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
| async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
|     if !CONFIG.org_events_enabled() { | ||||
|         return Ok(()); | ||||
|     } | ||||
|  | ||||
|     for event in data.iter().map(|d| &d.data) { | ||||
|         let event_date = parse_date(&event.Date); | ||||
|         match event.Type { | ||||
|     for event in data.iter() { | ||||
|         let event_date = parse_date(&event.date); | ||||
|         match event.r#type { | ||||
|             1000..=1099 => { | ||||
|                 _log_user_event( | ||||
|                     event.Type, | ||||
|                     event.r#type, | ||||
|                     &headers.user.uuid, | ||||
|                     headers.device.atype, | ||||
|                     Some(event_date), | ||||
| @@ -181,9 +180,9 @@ async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Head | ||||
|                 .await; | ||||
|             } | ||||
|             1600..=1699 => { | ||||
|                 if let Some(org_uuid) = &event.OrganizationId { | ||||
|                 if let Some(org_uuid) = &event.organization_id { | ||||
|                     _log_event( | ||||
|                         event.Type, | ||||
|                         event.r#type, | ||||
|                         org_uuid, | ||||
|                         org_uuid, | ||||
|                         &headers.user.uuid, | ||||
| @@ -196,11 +195,11 @@ async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Head | ||||
|                 } | ||||
|             } | ||||
|             _ => { | ||||
|                 if let Some(cipher_uuid) = &event.CipherId { | ||||
|                 if let Some(cipher_uuid) = &event.cipher_id { | ||||
|                     if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await { | ||||
|                         if let Some(org_uuid) = cipher.organization_uuid { | ||||
|                             _log_event( | ||||
|                                 event.Type, | ||||
|                                 event.r#type, | ||||
|                                 cipher_uuid, | ||||
|                                 &org_uuid, | ||||
|                                 &headers.user.uuid, | ||||
|   | ||||
| @@ -2,7 +2,7 @@ use rocket::serde::json::Json; | ||||
| use serde_json::Value; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, | ||||
|     api::{EmptyResult, JsonResult, Notify, UpdateType}, | ||||
|     auth::Headers, | ||||
|     db::{models::*, DbConn}, | ||||
| }; | ||||
| @@ -17,9 +17,9 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json<Value> { | ||||
|     let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect(); | ||||
|  | ||||
|     Json(json!({ | ||||
|       "Data": folders_json, | ||||
|       "Object": "list", | ||||
|       "ContinuationToken": null, | ||||
|       "data": folders_json, | ||||
|       "object": "list", | ||||
|       "continuationToken": null, | ||||
|     })) | ||||
| } | ||||
|  | ||||
| @@ -38,16 +38,16 @@ async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct FolderData { | ||||
|     pub Name: String, | ||||
|     pub name: String, | ||||
| } | ||||
|  | ||||
| #[post("/folders", data = "<data>")] | ||||
| async fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     let data: FolderData = data.into_inner().data; | ||||
| async fn post_folders(data: Json<FolderData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     let data: FolderData = data.into_inner(); | ||||
|  | ||||
|     let mut folder = Folder::new(headers.user.uuid, data.Name); | ||||
|     let mut folder = Folder::new(headers.user.uuid, data.name); | ||||
|  | ||||
|     folder.save(&mut conn).await?; | ||||
|     nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid, &mut conn).await; | ||||
| @@ -56,25 +56,19 @@ async fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, mut conn: | ||||
| } | ||||
|  | ||||
| #[post("/folders/<uuid>", data = "<data>")] | ||||
| async fn post_folder( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<FolderData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
| async fn post_folder(uuid: &str, data: Json<FolderData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     put_folder(uuid, data, headers, conn, nt).await | ||||
| } | ||||
|  | ||||
| #[put("/folders/<uuid>", data = "<data>")] | ||||
| async fn put_folder( | ||||
|     uuid: &str, | ||||
|     data: JsonUpcase<FolderData>, | ||||
|     data: Json<FolderData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
|     let data: FolderData = data.into_inner().data; | ||||
|     let data: FolderData = data.into_inner(); | ||||
|  | ||||
|     let mut folder = match Folder::find_by_uuid(uuid, &mut conn).await { | ||||
|         Some(folder) => folder, | ||||
| @@ -85,7 +79,7 @@ async fn put_folder( | ||||
|         err!("Folder belongs to another user") | ||||
|     } | ||||
|  | ||||
|     folder.name = data.Name; | ||||
|     folder.name = data.name; | ||||
|  | ||||
|     folder.save(&mut conn).await?; | ||||
|     nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid, &mut conn).await; | ||||
|   | ||||
| @@ -49,19 +49,19 @@ pub fn events_routes() -> Vec<Route> { | ||||
| use rocket::{serde::json::Json, serde::json::Value, Catcher, Route}; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{JsonResult, JsonUpcase, Notify, UpdateType}, | ||||
|     api::{JsonResult, Notify, UpdateType}, | ||||
|     auth::Headers, | ||||
|     db::DbConn, | ||||
|     error::Error, | ||||
|     util::{get_reqwest_client, parse_experimental_client_feature_flags}, | ||||
| }; | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct GlobalDomain { | ||||
|     Type: i32, | ||||
|     Domains: Vec<String>, | ||||
|     Excluded: bool, | ||||
|     r#type: i32, | ||||
|     domains: Vec<String>, | ||||
|     excluded: bool, | ||||
| } | ||||
|  | ||||
| const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json"); | ||||
| @@ -81,38 +81,38 @@ fn _get_eq_domains(headers: Headers, no_excluded: bool) -> Json<Value> { | ||||
|     let mut globals: Vec<GlobalDomain> = from_str(GLOBAL_DOMAINS).unwrap(); | ||||
|  | ||||
|     for global in &mut globals { | ||||
|         global.Excluded = excluded_globals.contains(&global.Type); | ||||
|         global.excluded = excluded_globals.contains(&global.r#type); | ||||
|     } | ||||
|  | ||||
|     if no_excluded { | ||||
|         globals.retain(|g| !g.Excluded); | ||||
|         globals.retain(|g| !g.excluded); | ||||
|     } | ||||
|  | ||||
|     Json(json!({ | ||||
|         "EquivalentDomains": equivalent_domains, | ||||
|         "GlobalEquivalentDomains": globals, | ||||
|         "Object": "domains", | ||||
|         "equivalentDomains": equivalent_domains, | ||||
|         "globalEquivalentDomains": globals, | ||||
|         "object": "domains", | ||||
|     })) | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EquivDomainData { | ||||
|     ExcludedGlobalEquivalentDomains: Option<Vec<i32>>, | ||||
|     EquivalentDomains: Option<Vec<Vec<String>>>, | ||||
|     excluded_global_equivalent_domains: Option<Vec<i32>>, | ||||
|     equivalent_domains: Option<Vec<Vec<String>>>, | ||||
| } | ||||
|  | ||||
| #[post("/settings/domains", data = "<data>")] | ||||
| async fn post_eq_domains( | ||||
|     data: JsonUpcase<EquivDomainData>, | ||||
|     data: Json<EquivDomainData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
|     let data: EquivDomainData = data.into_inner().data; | ||||
|     let data: EquivDomainData = data.into_inner(); | ||||
|  | ||||
|     let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default(); | ||||
|     let equivalent_domains = data.EquivalentDomains.unwrap_or_default(); | ||||
|     let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default(); | ||||
|     let equivalent_domains = data.equivalent_domains.unwrap_or_default(); | ||||
|  | ||||
|     let mut user = headers.user; | ||||
|     use serde_json::to_string; | ||||
| @@ -128,12 +128,7 @@ async fn post_eq_domains( | ||||
| } | ||||
|  | ||||
| #[put("/settings/domains", data = "<data>")] | ||||
| async fn put_eq_domains( | ||||
|     data: JsonUpcase<EquivDomainData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
| async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     post_eq_domains(data, headers, conn, nt).await | ||||
| } | ||||
|  | ||||
| @@ -157,15 +152,15 @@ async fn hibp_breach(username: &str) -> JsonResult { | ||||
|         Ok(Json(value)) | ||||
|     } else { | ||||
|         Ok(Json(json!([{ | ||||
|             "Name": "HaveIBeenPwned", | ||||
|             "Title": "Manual HIBP Check", | ||||
|             "Domain": "haveibeenpwned.com", | ||||
|             "BreachDate": "2019-08-18T00:00:00Z", | ||||
|             "AddedDate": "2019-08-18T00:00:00Z", | ||||
|             "Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{username}\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/account/{username}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>"), | ||||
|             "LogoPath": "vw_static/hibp.png", | ||||
|             "PwnCount": 0, | ||||
|             "DataClasses": [ | ||||
|             "name": "HaveIBeenPwned", | ||||
|             "title": "Manual HIBP Check", | ||||
|             "domain": "haveibeenpwned.com", | ||||
|             "breachDate": "2019-08-18T00:00:00Z", | ||||
|             "addedDate": "2019-08-18T00:00:00Z", | ||||
|             "description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{username}\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/account/{username}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>"), | ||||
|             "logoPath": "vw_static/hibp.png", | ||||
|             "pwnCount": 0, | ||||
|             "dataClasses": [ | ||||
|                 "Error - No API key set!" | ||||
|             ] | ||||
|         }]))) | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,13 +1,14 @@ | ||||
| use chrono::Utc; | ||||
| use rocket::{ | ||||
|     request::{self, FromRequest, Outcome}, | ||||
|     serde::json::Json, | ||||
|     Request, Route, | ||||
| }; | ||||
|  | ||||
| use std::collections::HashSet; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{EmptyResult, JsonUpcase}, | ||||
|     api::EmptyResult, | ||||
|     auth, | ||||
|     db::{models::*, DbConn}, | ||||
|     mail, CONFIG, | ||||
| @@ -18,43 +19,43 @@ pub fn routes() -> Vec<Route> { | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct OrgImportGroupData { | ||||
|     Name: String, | ||||
|     ExternalId: String, | ||||
|     MemberExternalIds: Vec<String>, | ||||
|     name: String, | ||||
|     external_id: String, | ||||
|     member_external_ids: Vec<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct OrgImportUserData { | ||||
|     Email: String, | ||||
|     ExternalId: String, | ||||
|     Deleted: bool, | ||||
|     email: String, | ||||
|     external_id: String, | ||||
|     deleted: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct OrgImportData { | ||||
|     Groups: Vec<OrgImportGroupData>, | ||||
|     Members: Vec<OrgImportUserData>, | ||||
|     OverwriteExisting: bool, | ||||
|     // LargeImport: bool, // For now this will not be used, upstream uses this to prevent syncs of more then 2000 users or groups without the flag set. | ||||
|     groups: Vec<OrgImportGroupData>, | ||||
|     members: Vec<OrgImportUserData>, | ||||
|     overwrite_existing: bool, | ||||
|     // largeImport: bool, // For now this will not be used, upstream uses this to prevent syncs of more then 2000 users or groups without the flag set. | ||||
| } | ||||
|  | ||||
| #[post("/public/organization/import", data = "<data>")] | ||||
| async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult { | ||||
| async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult { | ||||
|     // Most of the logic for this function can be found here | ||||
|     // https://github.com/bitwarden/server/blob/fd892b2ff4547648a276734fb2b14a8abae2c6f5/src/Core/Services/Implementations/OrganizationService.cs#L1797 | ||||
|  | ||||
|     let org_id = token.0; | ||||
|     let data = data.into_inner().data; | ||||
|     let data = data.into_inner(); | ||||
|  | ||||
|     for user_data in &data.Members { | ||||
|         if user_data.Deleted { | ||||
|     for user_data in &data.members { | ||||
|         if user_data.deleted { | ||||
|             // If user is marked for deletion and it exists, revoke it | ||||
|             if let Some(mut user_org) = | ||||
|                 UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await | ||||
|                 UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await | ||||
|             { | ||||
|                 // Only revoke a user if it is not the last confirmed owner | ||||
|                 let revoked = if user_org.atype == UserOrgType::Owner | ||||
| @@ -72,27 +73,27 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co | ||||
|                     user_org.revoke() | ||||
|                 }; | ||||
|  | ||||
|                 let ext_modified = user_org.set_external_id(Some(user_data.ExternalId.clone())); | ||||
|                 let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone())); | ||||
|                 if revoked || ext_modified { | ||||
|                     user_org.save(&mut conn).await?; | ||||
|                 } | ||||
|             } | ||||
|         // If user is part of the organization, restore it | ||||
|         } else if let Some(mut user_org) = | ||||
|             UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await | ||||
|             UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await | ||||
|         { | ||||
|             let restored = user_org.restore(); | ||||
|             let ext_modified = user_org.set_external_id(Some(user_data.ExternalId.clone())); | ||||
|             let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone())); | ||||
|             if restored || ext_modified { | ||||
|                 user_org.save(&mut conn).await?; | ||||
|             } | ||||
|         } else { | ||||
|             // If user is not part of the organization | ||||
|             let user = match User::find_by_mail(&user_data.Email, &mut conn).await { | ||||
|             let user = match User::find_by_mail(&user_data.email, &mut conn).await { | ||||
|                 Some(user) => user, // exists in vaultwarden | ||||
|                 None => { | ||||
|                     // User does not exist yet | ||||
|                     let mut new_user = User::new(user_data.Email.clone()); | ||||
|                     let mut new_user = User::new(user_data.email.clone()); | ||||
|                     new_user.save(&mut conn).await?; | ||||
|  | ||||
|                     if !CONFIG.mail_enabled() { | ||||
| @@ -109,7 +110,7 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co | ||||
|             }; | ||||
|  | ||||
|             let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); | ||||
|             new_org_user.set_external_id(Some(user_data.ExternalId.clone())); | ||||
|             new_org_user.set_external_id(Some(user_data.external_id.clone())); | ||||
|             new_org_user.access_all = false; | ||||
|             new_org_user.atype = UserOrgType::User as i32; | ||||
|             new_org_user.status = user_org_status; | ||||
| @@ -123,7 +124,7 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co | ||||
|                 }; | ||||
|  | ||||
|                 mail::send_invite( | ||||
|                     &user_data.Email, | ||||
|                     &user_data.email, | ||||
|                     &user.uuid, | ||||
|                     Some(org_id.clone()), | ||||
|                     Some(new_org_user.uuid), | ||||
| @@ -136,13 +137,17 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co | ||||
|     } | ||||
|  | ||||
|     if CONFIG.org_groups_enabled() { | ||||
|         for group_data in &data.Groups { | ||||
|             let group_uuid = match Group::find_by_external_id_and_org(&group_data.ExternalId, &org_id, &mut conn).await | ||||
|         for group_data in &data.groups { | ||||
|             let group_uuid = match Group::find_by_external_id_and_org(&group_data.external_id, &org_id, &mut conn).await | ||||
|             { | ||||
|                 Some(group) => group.uuid, | ||||
|                 None => { | ||||
|                     let mut group = | ||||
|                         Group::new(org_id.clone(), group_data.Name.clone(), false, Some(group_data.ExternalId.clone())); | ||||
|                     let mut group = Group::new( | ||||
|                         org_id.clone(), | ||||
|                         group_data.name.clone(), | ||||
|                         false, | ||||
|                         Some(group_data.external_id.clone()), | ||||
|                     ); | ||||
|                     group.save(&mut conn).await?; | ||||
|                     group.uuid | ||||
|                 } | ||||
| @@ -150,7 +155,7 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co | ||||
|  | ||||
|             GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?; | ||||
|  | ||||
|             for ext_id in &group_data.MemberExternalIds { | ||||
|             for ext_id in &group_data.member_external_ids { | ||||
|                 if let Some(user_org) = UserOrganization::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await | ||||
|                 { | ||||
|                     let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone()); | ||||
| @@ -163,9 +168,9 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co | ||||
|     } | ||||
|  | ||||
|     // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) | ||||
|     if data.OverwriteExisting { | ||||
|     if data.overwrite_existing { | ||||
|         // Generate a HashSet to quickly verify if a member is listed or not. | ||||
|         let sync_members: HashSet<String> = data.Members.into_iter().map(|m| m.ExternalId).collect(); | ||||
|         let sync_members: HashSet<String> = data.members.into_iter().map(|m| m.external_id).collect(); | ||||
|         for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await { | ||||
|             if let Some(ref user_external_id) = user_org.external_id { | ||||
|                 if !sync_members.contains(user_external_id) { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ use rocket::serde::json::Json; | ||||
| use serde_json::Value; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, | ||||
|     api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType}, | ||||
|     auth::{ClientIp, Headers, Host}, | ||||
|     db::{models::*, DbConn, DbPool}, | ||||
|     util::{NumberOrString, SafeString}, | ||||
| @@ -48,26 +48,26 @@ pub async fn purge_sends(pool: DbPool) { | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct SendData { | ||||
|     Type: i32, | ||||
|     Key: String, | ||||
|     Password: Option<String>, | ||||
|     MaxAccessCount: Option<NumberOrString>, | ||||
|     ExpirationDate: Option<DateTime<Utc>>, | ||||
|     DeletionDate: DateTime<Utc>, | ||||
|     Disabled: bool, | ||||
|     HideEmail: Option<bool>, | ||||
|     r#type: i32, | ||||
|     key: String, | ||||
|     password: Option<String>, | ||||
|     max_access_count: Option<NumberOrString>, | ||||
|     expiration_date: Option<DateTime<Utc>>, | ||||
|     deletion_date: DateTime<Utc>, | ||||
|     disabled: bool, | ||||
|     hide_email: Option<bool>, | ||||
|  | ||||
|     // Data field | ||||
|     Name: String, | ||||
|     Notes: Option<String>, | ||||
|     Text: Option<Value>, | ||||
|     File: Option<Value>, | ||||
|     FileLength: Option<NumberOrString>, | ||||
|     name: String, | ||||
|     notes: Option<String>, | ||||
|     text: Option<Value>, | ||||
|     file: Option<Value>, | ||||
|     file_length: Option<NumberOrString>, | ||||
|  | ||||
|     // Used for key rotations | ||||
|     pub Id: Option<String>, | ||||
|     pub id: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Enforces the `Disable Send` policy. A non-owner/admin user belonging to | ||||
| @@ -96,7 +96,7 @@ async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> Em | ||||
| /// Ref: https://bitwarden.com/help/article/policies/#send-options | ||||
| async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &mut DbConn) -> EmptyResult { | ||||
|     let user_uuid = &headers.user.uuid; | ||||
|     let hide_email = data.HideEmail.unwrap_or(false); | ||||
|     let hide_email = data.hide_email.unwrap_or(false); | ||||
|     if hide_email && OrgPolicy::is_hide_email_disabled(user_uuid, conn).await { | ||||
|         err!( | ||||
|             "Due to an Enterprise Policy, you are not allowed to hide your email address \ | ||||
| @@ -107,40 +107,40 @@ async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, c | ||||
| } | ||||
|  | ||||
| fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> { | ||||
|     let data_val = if data.Type == SendType::Text as i32 { | ||||
|         data.Text | ||||
|     } else if data.Type == SendType::File as i32 { | ||||
|         data.File | ||||
|     let data_val = if data.r#type == SendType::Text as i32 { | ||||
|         data.text | ||||
|     } else if data.r#type == SendType::File as i32 { | ||||
|         data.file | ||||
|     } else { | ||||
|         err!("Invalid Send type") | ||||
|     }; | ||||
|  | ||||
|     let data_str = if let Some(mut d) = data_val { | ||||
|         d.as_object_mut().and_then(|o| o.remove("Response")); | ||||
|         d.as_object_mut().and_then(|o| o.remove("response")); | ||||
|         serde_json::to_string(&d)? | ||||
|     } else { | ||||
|         err!("Send data not provided"); | ||||
|     }; | ||||
|  | ||||
|     if data.DeletionDate > Utc::now() + TimeDelta::try_days(31).unwrap() { | ||||
|     if data.deletion_date > Utc::now() + TimeDelta::try_days(31).unwrap() { | ||||
|         err!( | ||||
|             "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     let mut send = Send::new(data.Type, data.Name, data_str, data.Key, data.DeletionDate.naive_utc()); | ||||
|     let mut send = Send::new(data.r#type, data.name, data_str, data.key, data.deletion_date.naive_utc()); | ||||
|     send.user_uuid = Some(user_uuid); | ||||
|     send.notes = data.Notes; | ||||
|     send.max_access_count = match data.MaxAccessCount { | ||||
|     send.notes = data.notes; | ||||
|     send.max_access_count = match data.max_access_count { | ||||
|         Some(m) => Some(m.into_i32()?), | ||||
|         _ => None, | ||||
|     }; | ||||
|     send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); | ||||
|     send.disabled = data.Disabled; | ||||
|     send.hide_email = data.HideEmail; | ||||
|     send.atype = data.Type; | ||||
|     send.expiration_date = data.expiration_date.map(|d| d.naive_utc()); | ||||
|     send.disabled = data.disabled; | ||||
|     send.hide_email = data.hide_email; | ||||
|     send.atype = data.r#type; | ||||
|  | ||||
|     send.set_password(data.Password.as_deref()); | ||||
|     send.set_password(data.password.as_deref()); | ||||
|  | ||||
|     Ok(send) | ||||
| } | ||||
| @@ -151,9 +151,9 @@ async fn get_sends(headers: Headers, mut conn: DbConn) -> Json<Value> { | ||||
|     let sends_json: Vec<Value> = sends.await.iter().map(|s| s.to_json()).collect(); | ||||
|  | ||||
|     Json(json!({ | ||||
|       "Data": sends_json, | ||||
|       "Object": "list", | ||||
|       "ContinuationToken": null | ||||
|       "data": sends_json, | ||||
|       "object": "list", | ||||
|       "continuationToken": null | ||||
|     })) | ||||
| } | ||||
|  | ||||
| @@ -172,13 +172,13 @@ async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult | ||||
| } | ||||
|  | ||||
| #[post("/sends", data = "<data>")] | ||||
| async fn post_send(data: JsonUpcase<SendData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
| async fn post_send(data: Json<SendData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     enforce_disable_send_policy(&headers, &mut conn).await?; | ||||
|  | ||||
|     let data: SendData = data.into_inner().data; | ||||
|     let data: SendData = data.into_inner(); | ||||
|     enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; | ||||
|  | ||||
|     if data.Type == SendType::File as i32 { | ||||
|     if data.r#type == SendType::File as i32 { | ||||
|         err!("File sends should use /api/sends/file") | ||||
|     } | ||||
|  | ||||
| @@ -198,7 +198,7 @@ async fn post_send(data: JsonUpcase<SendData>, headers: Headers, mut conn: DbCon | ||||
|  | ||||
| #[derive(FromForm)] | ||||
| struct UploadData<'f> { | ||||
|     model: Json<crate::util::UpCase<SendData>>, | ||||
|     model: Json<SendData>, | ||||
|     data: TempFile<'f>, | ||||
| } | ||||
|  | ||||
| @@ -218,7 +218,7 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn: | ||||
|         model, | ||||
|         mut data, | ||||
|     } = data.into_inner(); | ||||
|     let model = model.into_inner().data; | ||||
|     let model = model.into_inner(); | ||||
|  | ||||
|     let Some(size) = data.len().to_i64() else { | ||||
|         err!("Invalid send size"); | ||||
| @@ -266,9 +266,9 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn: | ||||
|  | ||||
|     let mut data_value: Value = serde_json::from_str(&send.data)?; | ||||
|     if let Some(o) = data_value.as_object_mut() { | ||||
|         o.insert(String::from("Id"), Value::String(file_id)); | ||||
|         o.insert(String::from("Size"), Value::Number(size.into())); | ||||
|         o.insert(String::from("SizeName"), Value::String(crate::util::get_display_size(size))); | ||||
|         o.insert(String::from("id"), Value::String(file_id)); | ||||
|         o.insert(String::from("size"), Value::Number(size.into())); | ||||
|         o.insert(String::from("sizeName"), Value::String(crate::util::get_display_size(size))); | ||||
|     } | ||||
|     send.data = serde_json::to_string(&data_value)?; | ||||
|  | ||||
| @@ -288,18 +288,18 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn: | ||||
|  | ||||
| // Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L190 | ||||
| #[post("/sends/file/v2", data = "<data>")] | ||||
| async fn post_send_file_v2(data: JsonUpcase<SendData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
| async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     enforce_disable_send_policy(&headers, &mut conn).await?; | ||||
|  | ||||
|     let data = data.into_inner().data; | ||||
|     let data = data.into_inner(); | ||||
|  | ||||
|     if data.Type != SendType::File as i32 { | ||||
|     if data.r#type != SendType::File as i32 { | ||||
|         err!("Send content is not a file"); | ||||
|     } | ||||
|  | ||||
|     enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; | ||||
|  | ||||
|     let file_length = match &data.FileLength { | ||||
|     let file_length = match &data.file_length { | ||||
|         Some(m) => m.into_i64()?, | ||||
|         _ => err!("Invalid send length"), | ||||
|     }; | ||||
| @@ -334,9 +334,9 @@ async fn post_send_file_v2(data: JsonUpcase<SendData>, headers: Headers, mut con | ||||
|  | ||||
|     let mut data_value: Value = serde_json::from_str(&send.data)?; | ||||
|     if let Some(o) = data_value.as_object_mut() { | ||||
|         o.insert(String::from("Id"), Value::String(file_id.clone())); | ||||
|         o.insert(String::from("Size"), Value::Number(file_length.into())); | ||||
|         o.insert(String::from("SizeName"), Value::String(crate::util::get_display_size(file_length))); | ||||
|         o.insert(String::from("id"), Value::String(file_id.clone())); | ||||
|         o.insert(String::from("size"), Value::Number(file_length.into())); | ||||
|         o.insert(String::from("sizeName"), Value::String(crate::util::get_display_size(file_length))); | ||||
|     } | ||||
|     send.data = serde_json::to_string(&data_value)?; | ||||
|     send.save(&mut conn).await?; | ||||
| @@ -395,15 +395,15 @@ async fn post_send_file_v2_data( | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct SendAccessData { | ||||
|     pub Password: Option<String>, | ||||
|     pub password: Option<String>, | ||||
| } | ||||
|  | ||||
| #[post("/sends/access/<access_id>", data = "<data>")] | ||||
| async fn post_access( | ||||
|     access_id: &str, | ||||
|     data: JsonUpcase<SendAccessData>, | ||||
|     data: Json<SendAccessData>, | ||||
|     mut conn: DbConn, | ||||
|     ip: ClientIp, | ||||
|     nt: Notify<'_>, | ||||
| @@ -434,7 +434,7 @@ async fn post_access( | ||||
|     } | ||||
|  | ||||
|     if send.password_hash.is_some() { | ||||
|         match data.into_inner().data.Password { | ||||
|         match data.into_inner().password { | ||||
|             Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } | ||||
|             Some(_) => err!("Invalid password", format!("IP: {}.", ip.ip)), | ||||
|             None => err_code!("Password not provided", format!("IP: {}.", ip.ip), 401), | ||||
| @@ -464,7 +464,7 @@ async fn post_access( | ||||
| async fn post_access_file( | ||||
|     send_id: &str, | ||||
|     file_id: &str, | ||||
|     data: JsonUpcase<SendAccessData>, | ||||
|     data: Json<SendAccessData>, | ||||
|     host: Host, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| @@ -495,7 +495,7 @@ async fn post_access_file( | ||||
|     } | ||||
|  | ||||
|     if send.password_hash.is_some() { | ||||
|         match data.into_inner().data.Password { | ||||
|         match data.into_inner().password { | ||||
|             Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } | ||||
|             Some(_) => err!("Invalid password."), | ||||
|             None => err_code!("Password not provided", 401), | ||||
| @@ -518,9 +518,9 @@ async fn post_access_file( | ||||
|     let token_claims = crate::auth::generate_send_claims(send_id, file_id); | ||||
|     let token = crate::auth::encode_jwt(&token_claims); | ||||
|     Ok(Json(json!({ | ||||
|         "Object": "send-fileDownload", | ||||
|         "Id": file_id, | ||||
|         "Url": format!("{}/api/sends/{}/{}?t={}", &host.host, send_id, file_id, token) | ||||
|         "object": "send-fileDownload", | ||||
|         "id": file_id, | ||||
|         "url": format!("{}/api/sends/{}/{}?t={}", &host.host, send_id, file_id, token) | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| @@ -535,16 +535,10 @@ async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Opt | ||||
| } | ||||
|  | ||||
| #[put("/sends/<id>", data = "<data>")] | ||||
| async fn put_send( | ||||
|     id: &str, | ||||
|     data: JsonUpcase<SendData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
|     nt: Notify<'_>, | ||||
| ) -> JsonResult { | ||||
| async fn put_send(id: &str, data: Json<SendData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { | ||||
|     enforce_disable_send_policy(&headers, &mut conn).await?; | ||||
|  | ||||
|     let data: SendData = data.into_inner().data; | ||||
|     let data: SendData = data.into_inner(); | ||||
|     enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?; | ||||
|  | ||||
|     let mut send = match Send::find_by_uuid(id, &mut conn).await { | ||||
| @@ -569,11 +563,11 @@ pub async fn update_send_from_data( | ||||
|         err!("Send is not owned by user") | ||||
|     } | ||||
|  | ||||
|     if send.atype != data.Type { | ||||
|     if send.atype != data.r#type { | ||||
|         err!("Sends can't change type") | ||||
|     } | ||||
|  | ||||
|     if data.DeletionDate > Utc::now() + TimeDelta::try_days(31).unwrap() { | ||||
|     if data.deletion_date > Utc::now() + TimeDelta::try_days(31).unwrap() { | ||||
|         err!( | ||||
|             "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." | ||||
|         ); | ||||
| @@ -581,9 +575,9 @@ pub async fn update_send_from_data( | ||||
|  | ||||
|     // When updating a file Send, we receive nulls in the File field, as it's immutable, | ||||
|     // so we only need to update the data field in the Text case | ||||
|     if data.Type == SendType::Text as i32 { | ||||
|         let data_str = if let Some(mut d) = data.Text { | ||||
|             d.as_object_mut().and_then(|d| d.remove("Response")); | ||||
|     if data.r#type == SendType::Text as i32 { | ||||
|         let data_str = if let Some(mut d) = data.text { | ||||
|             d.as_object_mut().and_then(|d| d.remove("response")); | ||||
|             serde_json::to_string(&d)? | ||||
|         } else { | ||||
|             err!("Send data not provided"); | ||||
| @@ -591,20 +585,20 @@ pub async fn update_send_from_data( | ||||
|         send.data = data_str; | ||||
|     } | ||||
|  | ||||
|     send.name = data.Name; | ||||
|     send.akey = data.Key; | ||||
|     send.deletion_date = data.DeletionDate.naive_utc(); | ||||
|     send.notes = data.Notes; | ||||
|     send.max_access_count = match data.MaxAccessCount { | ||||
|     send.name = data.name; | ||||
|     send.akey = data.key; | ||||
|     send.deletion_date = data.deletion_date.naive_utc(); | ||||
|     send.notes = data.notes; | ||||
|     send.max_access_count = match data.max_access_count { | ||||
|         Some(m) => Some(m.into_i32()?), | ||||
|         _ => None, | ||||
|     }; | ||||
|     send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); | ||||
|     send.hide_email = data.HideEmail; | ||||
|     send.disabled = data.Disabled; | ||||
|     send.expiration_date = data.expiration_date.map(|d| d.naive_utc()); | ||||
|     send.hide_email = data.hide_email; | ||||
|     send.disabled = data.disabled; | ||||
|  | ||||
|     // Only change the value if it's present | ||||
|     if let Some(password) = data.Password { | ||||
|     if let Some(password) = data.password { | ||||
|         send.set_password(Some(&password)); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -3,10 +3,7 @@ use rocket::serde::json::Json; | ||||
| use rocket::Route; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, | ||||
|         PasswordOrOtpData, | ||||
|     }, | ||||
|     api::{core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, PasswordOrOtpData}, | ||||
|     auth::{ClientIp, Headers}, | ||||
|     crypto, | ||||
|     db::{ | ||||
| @@ -23,8 +20,8 @@ pub fn routes() -> Vec<Route> { | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/get-authenticator", data = "<data>")] | ||||
| async fn generate_authenticator(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner().data; | ||||
| async fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     data.validate(&user, false, &mut conn).await?; | ||||
| @@ -38,36 +35,32 @@ async fn generate_authenticator(data: JsonUpcase<PasswordOrOtpData>, headers: He | ||||
|     }; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Enabled": enabled, | ||||
|         "Key": key, | ||||
|         "Object": "twoFactorAuthenticator" | ||||
|         "enabled": enabled, | ||||
|         "key": key, | ||||
|         "object": "twoFactorAuthenticator" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EnableAuthenticatorData { | ||||
|     Key: String, | ||||
|     Token: NumberOrString, | ||||
|     MasterPasswordHash: Option<String>, | ||||
|     Otp: Option<String>, | ||||
|     key: String, | ||||
|     token: NumberOrString, | ||||
|     master_password_hash: Option<String>, | ||||
|     otp: Option<String>, | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/authenticator", data = "<data>")] | ||||
| async fn activate_authenticator( | ||||
|     data: JsonUpcase<EnableAuthenticatorData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
|     let data: EnableAuthenticatorData = data.into_inner().data; | ||||
|     let key = data.Key; | ||||
|     let token = data.Token.into_string(); | ||||
| async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EnableAuthenticatorData = data.into_inner(); | ||||
|     let key = data.key; | ||||
|     let token = data.token.into_string(); | ||||
|  | ||||
|     let mut user = headers.user; | ||||
|  | ||||
|     PasswordOrOtpData { | ||||
|         MasterPasswordHash: data.MasterPasswordHash, | ||||
|         Otp: data.Otp, | ||||
|         master_password_hash: data.master_password_hash, | ||||
|         otp: data.otp, | ||||
|     } | ||||
|     .validate(&user, true, &mut conn) | ||||
|     .await?; | ||||
| @@ -90,18 +83,14 @@ async fn activate_authenticator( | ||||
|     log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Enabled": true, | ||||
|         "Key": key, | ||||
|         "Object": "twoFactorAuthenticator" | ||||
|         "enabled": true, | ||||
|         "key": key, | ||||
|         "object": "twoFactorAuthenticator" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| #[put("/two-factor/authenticator", data = "<data>")] | ||||
| async fn activate_authenticator_put( | ||||
|     data: JsonUpcase<EnableAuthenticatorData>, | ||||
|     headers: Headers, | ||||
|     conn: DbConn, | ||||
| ) -> JsonResult { | ||||
| async fn activate_authenticator_put(data: Json<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
|     activate_authenticator(data, headers, conn).await | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ use rocket::Route; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::log_user_event, core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, | ||||
|         core::log_user_event, core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, | ||||
|         PasswordOrOtpData, | ||||
|     }, | ||||
|     auth::Headers, | ||||
| @@ -92,8 +92,8 @@ impl DuoStatus { | ||||
| const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>"; | ||||
|  | ||||
| #[post("/two-factor/get-duo", data = "<data>")] | ||||
| async fn get_duo(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner().data; | ||||
| async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     data.validate(&user, false, &mut conn).await?; | ||||
| @@ -109,16 +109,16 @@ async fn get_duo(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn | ||||
|  | ||||
|     let json = if let Some(data) = data { | ||||
|         json!({ | ||||
|             "Enabled": enabled, | ||||
|             "Host": data.host, | ||||
|             "SecretKey": data.sk, | ||||
|             "IntegrationKey": data.ik, | ||||
|             "Object": "twoFactorDuo" | ||||
|             "enabled": enabled, | ||||
|             "host": data.host, | ||||
|             "secretKey": data.sk, | ||||
|             "integrationKey": data.ik, | ||||
|             "object": "twoFactorDuo" | ||||
|         }) | ||||
|     } else { | ||||
|         json!({ | ||||
|             "Enabled": enabled, | ||||
|             "Object": "twoFactorDuo" | ||||
|             "enabled": enabled, | ||||
|             "object": "twoFactorDuo" | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
| @@ -126,21 +126,21 @@ async fn get_duo(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case, dead_code)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EnableDuoData { | ||||
|     Host: String, | ||||
|     SecretKey: String, | ||||
|     IntegrationKey: String, | ||||
|     MasterPasswordHash: Option<String>, | ||||
|     Otp: Option<String>, | ||||
|     host: String, | ||||
|     secret_key: String, | ||||
|     integration_key: String, | ||||
|     master_password_hash: Option<String>, | ||||
|     otp: Option<String>, | ||||
| } | ||||
|  | ||||
| impl From<EnableDuoData> for DuoData { | ||||
|     fn from(d: EnableDuoData) -> Self { | ||||
|         Self { | ||||
|             host: d.Host, | ||||
|             ik: d.IntegrationKey, | ||||
|             sk: d.SecretKey, | ||||
|             host: d.host, | ||||
|             ik: d.integration_key, | ||||
|             sk: d.secret_key, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -151,17 +151,17 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool { | ||||
|         st.is_empty() || s == DISABLED_MESSAGE_DEFAULT | ||||
|     } | ||||
|  | ||||
|     !empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey) | ||||
|     !empty_or_default(&data.host) && !empty_or_default(&data.secret_key) && !empty_or_default(&data.integration_key) | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/duo", data = "<data>")] | ||||
| async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EnableDuoData = data.into_inner().data; | ||||
| async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EnableDuoData = data.into_inner(); | ||||
|     let mut user = headers.user; | ||||
|  | ||||
|     PasswordOrOtpData { | ||||
|         MasterPasswordHash: data.MasterPasswordHash.clone(), | ||||
|         Otp: data.Otp.clone(), | ||||
|         master_password_hash: data.master_password_hash.clone(), | ||||
|         otp: data.otp.clone(), | ||||
|     } | ||||
|     .validate(&user, true, &mut conn) | ||||
|     .await?; | ||||
| @@ -184,16 +184,16 @@ async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut con | ||||
|     log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Enabled": true, | ||||
|         "Host": data.host, | ||||
|         "SecretKey": data.sk, | ||||
|         "IntegrationKey": data.ik, | ||||
|         "Object": "twoFactorDuo" | ||||
|         "enabled": true, | ||||
|         "host": data.host, | ||||
|         "secretKey": data.sk, | ||||
|         "integrationKey": data.ik, | ||||
|         "object": "twoFactorDuo" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| #[put("/two-factor/duo", data = "<data>")] | ||||
| async fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
| async fn activate_duo_put(data: Json<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
|     activate_duo(data, headers, conn).await | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ use rocket::Route; | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::{log_user_event, two_factor::_generate_recover_code}, | ||||
|         EmptyResult, JsonResult, JsonUpcase, PasswordOrOtpData, | ||||
|         EmptyResult, JsonResult, PasswordOrOtpData, | ||||
|     }, | ||||
|     auth::Headers, | ||||
|     crypto, | ||||
| @@ -22,28 +22,28 @@ pub fn routes() -> Vec<Route> { | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct SendEmailLoginData { | ||||
|     Email: String, | ||||
|     MasterPasswordHash: String, | ||||
|     email: String, | ||||
|     master_password_hash: String, | ||||
| } | ||||
|  | ||||
| /// User is trying to login and wants to use email 2FA. | ||||
| /// Does not require Bearer token | ||||
| #[post("/two-factor/send-email-login", data = "<data>")] // JsonResult | ||||
| async fn send_email_login(data: JsonUpcase<SendEmailLoginData>, mut conn: DbConn) -> EmptyResult { | ||||
|     let data: SendEmailLoginData = data.into_inner().data; | ||||
| async fn send_email_login(data: Json<SendEmailLoginData>, mut conn: DbConn) -> EmptyResult { | ||||
|     let data: SendEmailLoginData = data.into_inner(); | ||||
|  | ||||
|     use crate::db::models::User; | ||||
|  | ||||
|     // Get the user | ||||
|     let user = match User::find_by_mail(&data.Email, &mut conn).await { | ||||
|     let user = match User::find_by_mail(&data.email, &mut conn).await { | ||||
|         Some(user) => user, | ||||
|         None => err!("Username or password is incorrect. Try again."), | ||||
|     }; | ||||
|  | ||||
|     // Check password | ||||
|     if !user.check_valid_password(&data.MasterPasswordHash) { | ||||
|     if !user.check_valid_password(&data.master_password_hash) { | ||||
|         err!("Username or password is incorrect. Try again.") | ||||
|     } | ||||
|  | ||||
| @@ -76,8 +76,8 @@ pub async fn send_token(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { | ||||
|  | ||||
| /// When user clicks on Manage email 2FA show the user the related information | ||||
| #[post("/two-factor/get-email", data = "<data>")] | ||||
| async fn get_email(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner().data; | ||||
| async fn get_email(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     data.validate(&user, false, &mut conn).await?; | ||||
| @@ -92,30 +92,30 @@ async fn get_email(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut co | ||||
|         }; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Email": mfa_email, | ||||
|         "Enabled": enabled, | ||||
|         "Object": "twoFactorEmail" | ||||
|         "email": mfa_email, | ||||
|         "enabled": enabled, | ||||
|         "object": "twoFactorEmail" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct SendEmailData { | ||||
|     /// Email where 2FA codes will be sent to, can be different than user email account. | ||||
|     Email: String, | ||||
|     MasterPasswordHash: Option<String>, | ||||
|     Otp: Option<String>, | ||||
|     email: String, | ||||
|     master_password_hash: Option<String>, | ||||
|     otp: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Send a verification email to the specified email address to check whether it exists/belongs to user. | ||||
| #[post("/two-factor/send-email", data = "<data>")] | ||||
| async fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
|     let data: SendEmailData = data.into_inner().data; | ||||
| async fn send_email(data: Json<SendEmailData>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
|     let data: SendEmailData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     PasswordOrOtpData { | ||||
|         MasterPasswordHash: data.MasterPasswordHash, | ||||
|         Otp: data.Otp, | ||||
|         master_password_hash: data.master_password_hash, | ||||
|         otp: data.otp, | ||||
|     } | ||||
|     .validate(&user, false, &mut conn) | ||||
|     .await?; | ||||
| @@ -131,7 +131,7 @@ async fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, mut conn: | ||||
|     } | ||||
|  | ||||
|     let generated_token = crypto::generate_email_token(CONFIG.email_token_size()); | ||||
|     let twofactor_data = EmailTokenData::new(data.Email, generated_token); | ||||
|     let twofactor_data = EmailTokenData::new(data.email, generated_token); | ||||
|  | ||||
|     // Uses EmailVerificationChallenge as type to show that it's not verified yet. | ||||
|     let twofactor = TwoFactor::new(user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json()); | ||||
| @@ -143,24 +143,24 @@ async fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, mut conn: | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EmailData { | ||||
|     Email: String, | ||||
|     Token: String, | ||||
|     MasterPasswordHash: Option<String>, | ||||
|     Otp: Option<String>, | ||||
|     email: String, | ||||
|     token: String, | ||||
|     master_password_hash: Option<String>, | ||||
|     otp: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Verify email belongs to user and can be used for 2FA email codes. | ||||
| #[put("/two-factor/email", data = "<data>")] | ||||
| async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EmailData = data.into_inner().data; | ||||
| async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EmailData = data.into_inner(); | ||||
|     let mut user = headers.user; | ||||
|  | ||||
|     // This is the last step in the verification process, delete the otp directly afterwards | ||||
|     PasswordOrOtpData { | ||||
|         MasterPasswordHash: data.MasterPasswordHash, | ||||
|         Otp: data.Otp, | ||||
|         master_password_hash: data.master_password_hash, | ||||
|         otp: data.otp, | ||||
|     } | ||||
|     .validate(&user, true, &mut conn) | ||||
|     .await?; | ||||
| @@ -176,7 +176,7 @@ async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn) | ||||
|         _ => err!("No token available"), | ||||
|     }; | ||||
|  | ||||
|     if !crypto::ct_eq(issued_token, data.Token) { | ||||
|     if !crypto::ct_eq(issued_token, data.token) { | ||||
|         err!("Token is invalid") | ||||
|     } | ||||
|  | ||||
| @@ -190,9 +190,9 @@ async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn) | ||||
|     log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Email": email_data.email, | ||||
|         "Enabled": "true", | ||||
|         "Object": "twoFactorEmail" | ||||
|         "email": email_data.email, | ||||
|         "enabled": "true", | ||||
|         "object": "twoFactorEmail" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ use serde_json::Value; | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::{log_event, log_user_event}, | ||||
|         EmptyResult, JsonResult, JsonUpcase, PasswordOrOtpData, | ||||
|         EmptyResult, JsonResult, PasswordOrOtpData, | ||||
|     }, | ||||
|     auth::{ClientHeaders, Headers}, | ||||
|     crypto, | ||||
| @@ -50,52 +50,52 @@ async fn get_twofactor(headers: Headers, mut conn: DbConn) -> Json<Value> { | ||||
|     let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect(); | ||||
|  | ||||
|     Json(json!({ | ||||
|         "Data": twofactors_json, | ||||
|         "Object": "list", | ||||
|         "ContinuationToken": null, | ||||
|         "data": twofactors_json, | ||||
|         "object": "list", | ||||
|         "continuationToken": null, | ||||
|     })) | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/get-recover", data = "<data>")] | ||||
| async fn get_recover(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner().data; | ||||
| async fn get_recover(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     data.validate(&user, true, &mut conn).await?; | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Code": user.totp_recover, | ||||
|         "Object": "twoFactorRecover" | ||||
|         "code": user.totp_recover, | ||||
|         "object": "twoFactorRecover" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct RecoverTwoFactor { | ||||
|     MasterPasswordHash: String, | ||||
|     Email: String, | ||||
|     RecoveryCode: String, | ||||
|     master_password_hash: String, | ||||
|     email: String, | ||||
|     recovery_code: String, | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/recover", data = "<data>")] | ||||
| async fn recover(data: JsonUpcase<RecoverTwoFactor>, client_headers: ClientHeaders, mut conn: DbConn) -> JsonResult { | ||||
|     let data: RecoverTwoFactor = data.into_inner().data; | ||||
| async fn recover(data: Json<RecoverTwoFactor>, client_headers: ClientHeaders, mut conn: DbConn) -> JsonResult { | ||||
|     let data: RecoverTwoFactor = data.into_inner(); | ||||
|  | ||||
|     use crate::db::models::User; | ||||
|  | ||||
|     // Get the user | ||||
|     let mut user = match User::find_by_mail(&data.Email, &mut conn).await { | ||||
|     let mut user = match User::find_by_mail(&data.email, &mut conn).await { | ||||
|         Some(user) => user, | ||||
|         None => err!("Username or password is incorrect. Try again."), | ||||
|     }; | ||||
|  | ||||
|     // Check password | ||||
|     if !user.check_valid_password(&data.MasterPasswordHash) { | ||||
|     if !user.check_valid_password(&data.master_password_hash) { | ||||
|         err!("Username or password is incorrect. Try again.") | ||||
|     } | ||||
|  | ||||
|     // Check if recovery code is correct | ||||
|     if !user.check_valid_recovery_code(&data.RecoveryCode) { | ||||
|     if !user.check_valid_recovery_code(&data.recovery_code) { | ||||
|         err!("Recovery code is incorrect. Try again.") | ||||
|     } | ||||
|  | ||||
| @@ -127,27 +127,27 @@ async fn _generate_recover_code(user: &mut User, conn: &mut DbConn) { | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct DisableTwoFactorData { | ||||
|     MasterPasswordHash: Option<String>, | ||||
|     Otp: Option<String>, | ||||
|     Type: NumberOrString, | ||||
|     master_password_hash: Option<String>, | ||||
|     otp: Option<String>, | ||||
|     r#type: NumberOrString, | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/disable", data = "<data>")] | ||||
| async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: DisableTwoFactorData = data.into_inner().data; | ||||
| async fn disable_twofactor(data: Json<DisableTwoFactorData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: DisableTwoFactorData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     // Delete directly after a valid token has been provided | ||||
|     PasswordOrOtpData { | ||||
|         MasterPasswordHash: data.MasterPasswordHash, | ||||
|         Otp: data.Otp, | ||||
|         master_password_hash: data.master_password_hash, | ||||
|         otp: data.otp, | ||||
|     } | ||||
|     .validate(&user, true, &mut conn) | ||||
|     .await?; | ||||
|  | ||||
|     let type_ = data.Type.into_i32()?; | ||||
|     let type_ = data.r#type.into_i32()?; | ||||
|  | ||||
|     if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await { | ||||
|         twofactor.delete(&mut conn).await?; | ||||
| @@ -160,14 +160,14 @@ async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Head | ||||
|     } | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Enabled": false, | ||||
|         "Type": type_, | ||||
|         "Object": "twoFactorProvider" | ||||
|         "enabled": false, | ||||
|         "type": type_, | ||||
|         "object": "twoFactorProvider" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| #[put("/two-factor/disable", data = "<data>")] | ||||
| async fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
| async fn disable_twofactor_put(data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
|     disable_twofactor(data, headers, conn).await | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| use chrono::{DateTime, TimeDelta, Utc}; | ||||
| use rocket::Route; | ||||
| use rocket::{serde::json::Json, Route}; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{EmptyResult, JsonUpcase}, | ||||
|     api::EmptyResult, | ||||
|     auth::Headers, | ||||
|     crypto, | ||||
|     db::{ | ||||
| @@ -18,7 +18,7 @@ pub fn routes() -> Vec<Route> { | ||||
| } | ||||
|  | ||||
| /// Data stored in the TwoFactor table in the db | ||||
| #[derive(Serialize, Deserialize, Debug)] | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct ProtectedActionData { | ||||
|     /// Token issued to validate the protected action | ||||
|     pub token: String, | ||||
| @@ -82,23 +82,24 @@ async fn request_otp(headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct ProtectedActionVerify { | ||||
|     OTP: String, | ||||
|     #[serde(rename = "OTP", alias = "otp")] | ||||
|     otp: String, | ||||
| } | ||||
|  | ||||
| #[post("/accounts/verify-otp", data = "<data>")] | ||||
| async fn verify_otp(data: JsonUpcase<ProtectedActionVerify>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
| async fn verify_otp(data: Json<ProtectedActionVerify>, headers: Headers, mut conn: DbConn) -> EmptyResult { | ||||
|     if !CONFIG.mail_enabled() { | ||||
|         err!("Email is disabled for this server. Either enable email or login using your master password instead of login via device."); | ||||
|     } | ||||
|  | ||||
|     let user = headers.user; | ||||
|     let data: ProtectedActionVerify = data.into_inner().data; | ||||
|     let data: ProtectedActionVerify = data.into_inner(); | ||||
|  | ||||
|     // Delete the token after one validation attempt | ||||
|     // This endpoint only gets called for the vault export, and doesn't need a second attempt | ||||
|     validate_protected_action_otp(&data.OTP, &user.uuid, true, &mut conn).await | ||||
|     validate_protected_action_otp(&data.otp, &user.uuid, true, &mut conn).await | ||||
| } | ||||
|  | ||||
| pub async fn validate_protected_action_otp( | ||||
|   | ||||
| @@ -7,7 +7,7 @@ use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::{log_user_event, two_factor::_generate_recover_code}, | ||||
|         EmptyResult, JsonResult, JsonUpcase, PasswordOrOtpData, | ||||
|         EmptyResult, JsonResult, PasswordOrOtpData, | ||||
|     }, | ||||
|     auth::Headers, | ||||
|     db::{ | ||||
| @@ -96,20 +96,20 @@ pub struct WebauthnRegistration { | ||||
| impl WebauthnRegistration { | ||||
|     fn to_json(&self) -> Value { | ||||
|         json!({ | ||||
|             "Id": self.id, | ||||
|             "Name": self.name, | ||||
|             "id": self.id, | ||||
|             "name": self.name, | ||||
|             "migrated": self.migrated, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/get-webauthn", data = "<data>")] | ||||
| async fn get_webauthn(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
| async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     if !CONFIG.domain_set() { | ||||
|         err!("`DOMAIN` environment variable is not set. Webauthn disabled") | ||||
|     } | ||||
|  | ||||
|     let data: PasswordOrOtpData = data.into_inner().data; | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     data.validate(&user, false, &mut conn).await?; | ||||
| @@ -118,19 +118,15 @@ async fn get_webauthn(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut | ||||
|     let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect(); | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Enabled": enabled, | ||||
|         "Keys": registrations_json, | ||||
|         "Object": "twoFactorWebAuthn" | ||||
|         "enabled": enabled, | ||||
|         "keys": registrations_json, | ||||
|         "object": "twoFactorWebAuthn" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/get-webauthn-challenge", data = "<data>")] | ||||
| async fn generate_webauthn_challenge( | ||||
|     data: JsonUpcase<PasswordOrOtpData>, | ||||
|     headers: Headers, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner().data; | ||||
| async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     data.validate(&user, false, &mut conn).await?; | ||||
| @@ -161,102 +157,94 @@ async fn generate_webauthn_challenge( | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EnableWebauthnData { | ||||
|     Id: NumberOrString, // 1..5 | ||||
|     Name: String, | ||||
|     DeviceResponse: RegisterPublicKeyCredentialCopy, | ||||
|     MasterPasswordHash: Option<String>, | ||||
|     Otp: Option<String>, | ||||
|     id: NumberOrString, // 1..5 | ||||
|     name: String, | ||||
|     device_response: RegisterPublicKeyCredentialCopy, | ||||
|     master_password_hash: Option<String>, | ||||
|     otp: Option<String>, | ||||
| } | ||||
|  | ||||
| // This is copied from RegisterPublicKeyCredential to change the Response objects casing | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct RegisterPublicKeyCredentialCopy { | ||||
|     pub Id: String, | ||||
|     pub RawId: Base64UrlSafeData, | ||||
|     pub Response: AuthenticatorAttestationResponseRawCopy, | ||||
|     pub Type: String, | ||||
|     pub id: String, | ||||
|     pub raw_id: Base64UrlSafeData, | ||||
|     pub response: AuthenticatorAttestationResponseRawCopy, | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| // This is copied from AuthenticatorAttestationResponseRaw to change clientDataJSON to clientDataJson | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct AuthenticatorAttestationResponseRawCopy { | ||||
|     pub AttestationObject: Base64UrlSafeData, | ||||
|     pub ClientDataJson: Base64UrlSafeData, | ||||
|     #[serde(rename = "AttestationObject", alias = "attestationObject")] | ||||
|     pub attestation_object: Base64UrlSafeData, | ||||
|     #[serde(rename = "clientDataJson", alias = "clientDataJSON")] | ||||
|     pub client_data_json: Base64UrlSafeData, | ||||
| } | ||||
|  | ||||
| impl From<RegisterPublicKeyCredentialCopy> for RegisterPublicKeyCredential { | ||||
|     fn from(r: RegisterPublicKeyCredentialCopy) -> Self { | ||||
|         Self { | ||||
|             id: r.Id, | ||||
|             raw_id: r.RawId, | ||||
|             id: r.id, | ||||
|             raw_id: r.raw_id, | ||||
|             response: AuthenticatorAttestationResponseRaw { | ||||
|                 attestation_object: r.Response.AttestationObject, | ||||
|                 client_data_json: r.Response.ClientDataJson, | ||||
|                 attestation_object: r.response.attestation_object, | ||||
|                 client_data_json: r.response.client_data_json, | ||||
|             }, | ||||
|             type_: r.Type, | ||||
|             type_: r.r#type, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // This is copied from PublicKeyCredential to change the Response objects casing | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct PublicKeyCredentialCopy { | ||||
|     pub Id: String, | ||||
|     pub RawId: Base64UrlSafeData, | ||||
|     pub Response: AuthenticatorAssertionResponseRawCopy, | ||||
|     pub Extensions: Option<AuthenticationExtensionsClientOutputsCopy>, | ||||
|     pub Type: String, | ||||
|     pub id: String, | ||||
|     pub raw_id: Base64UrlSafeData, | ||||
|     pub response: AuthenticatorAssertionResponseRawCopy, | ||||
|     pub extensions: Option<AuthenticationExtensionsClientOutputs>, | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| // This is copied from AuthenticatorAssertionResponseRaw to change clientDataJSON to clientDataJson | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct AuthenticatorAssertionResponseRawCopy { | ||||
|     pub AuthenticatorData: Base64UrlSafeData, | ||||
|     pub ClientDataJson: Base64UrlSafeData, | ||||
|     pub Signature: Base64UrlSafeData, | ||||
|     pub UserHandle: Option<Base64UrlSafeData>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| pub struct AuthenticationExtensionsClientOutputsCopy { | ||||
|     #[serde(default)] | ||||
|     pub Appid: bool, | ||||
|     pub authenticator_data: Base64UrlSafeData, | ||||
|     #[serde(rename = "clientDataJson", alias = "clientDataJSON")] | ||||
|     pub client_data_json: Base64UrlSafeData, | ||||
|     pub signature: Base64UrlSafeData, | ||||
|     pub user_handle: Option<Base64UrlSafeData>, | ||||
| } | ||||
|  | ||||
| impl From<PublicKeyCredentialCopy> for PublicKeyCredential { | ||||
|     fn from(r: PublicKeyCredentialCopy) -> Self { | ||||
|         Self { | ||||
|             id: r.Id, | ||||
|             raw_id: r.RawId, | ||||
|             id: r.id, | ||||
|             raw_id: r.raw_id, | ||||
|             response: AuthenticatorAssertionResponseRaw { | ||||
|                 authenticator_data: r.Response.AuthenticatorData, | ||||
|                 client_data_json: r.Response.ClientDataJson, | ||||
|                 signature: r.Response.Signature, | ||||
|                 user_handle: r.Response.UserHandle, | ||||
|                 authenticator_data: r.response.authenticator_data, | ||||
|                 client_data_json: r.response.client_data_json, | ||||
|                 signature: r.response.signature, | ||||
|                 user_handle: r.response.user_handle, | ||||
|             }, | ||||
|             extensions: r.Extensions.map(|e| AuthenticationExtensionsClientOutputs { | ||||
|                 appid: e.Appid, | ||||
|             }), | ||||
|             type_: r.Type, | ||||
|             extensions: r.extensions, | ||||
|             type_: r.r#type, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/webauthn", data = "<data>")] | ||||
| async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EnableWebauthnData = data.into_inner().data; | ||||
| async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EnableWebauthnData = data.into_inner(); | ||||
|     let mut user = headers.user; | ||||
|  | ||||
|     PasswordOrOtpData { | ||||
|         MasterPasswordHash: data.MasterPasswordHash, | ||||
|         Otp: data.Otp, | ||||
|         master_password_hash: data.master_password_hash, | ||||
|         otp: data.otp, | ||||
|     } | ||||
|     .validate(&user, true, &mut conn) | ||||
|     .await?; | ||||
| @@ -274,13 +262,13 @@ async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Header | ||||
|  | ||||
|     // Verify the credentials with the saved state | ||||
|     let (credential, _data) = | ||||
|         WebauthnConfig::load().register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?; | ||||
|         WebauthnConfig::load().register_credential(&data.device_response.into(), &state, |_| Ok(false))?; | ||||
|  | ||||
|     let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1; | ||||
|     // TODO: Check for repeated ID's | ||||
|     registrations.push(WebauthnRegistration { | ||||
|         id: data.Id.into_i32()?, | ||||
|         name: data.Name, | ||||
|         id: data.id.into_i32()?, | ||||
|         name: data.name, | ||||
|         migrated: false, | ||||
|  | ||||
|         credential, | ||||
| @@ -296,28 +284,28 @@ async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Header | ||||
|  | ||||
|     let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect(); | ||||
|     Ok(Json(json!({ | ||||
|         "Enabled": true, | ||||
|         "Keys": keys_json, | ||||
|         "Object": "twoFactorU2f" | ||||
|         "enabled": true, | ||||
|         "keys": keys_json, | ||||
|         "object": "twoFactorU2f" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| #[put("/two-factor/webauthn", data = "<data>")] | ||||
| async fn activate_webauthn_put(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
| async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
|     activate_webauthn(data, headers, conn).await | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct DeleteU2FData { | ||||
|     Id: NumberOrString, | ||||
|     MasterPasswordHash: String, | ||||
|     id: NumberOrString, | ||||
|     master_password_hash: String, | ||||
| } | ||||
|  | ||||
| #[delete("/two-factor/webauthn", data = "<data>")] | ||||
| async fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let id = data.data.Id.into_i32()?; | ||||
|     if !headers.user.check_valid_password(&data.data.MasterPasswordHash) { | ||||
| async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let id = data.id.into_i32()?; | ||||
|     if !headers.user.check_valid_password(&data.master_password_hash) { | ||||
|         err!("Invalid password"); | ||||
|     } | ||||
|  | ||||
| @@ -358,9 +346,9 @@ async fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, mut | ||||
|     let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect(); | ||||
|  | ||||
|     Ok(Json(json!({ | ||||
|         "Enabled": true, | ||||
|         "Keys": keys_json, | ||||
|         "Object": "twoFactorU2f" | ||||
|         "enabled": true, | ||||
|         "keys": keys_json, | ||||
|         "object": "twoFactorU2f" | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| @@ -413,8 +401,8 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     let rsp: crate::util::UpCase<PublicKeyCredentialCopy> = serde_json::from_str(response)?; | ||||
|     let rsp: PublicKeyCredential = rsp.data.into(); | ||||
|     let rsp: PublicKeyCredentialCopy = serde_json::from_str(response)?; | ||||
|     let rsp: PublicKeyCredential = rsp.into(); | ||||
|  | ||||
|     let mut registrations = get_webauthn_registrations(user_uuid, conn).await?.1; | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ use yubico::{config::Config, verify_async}; | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::{log_user_event, two_factor::_generate_recover_code}, | ||||
|         EmptyResult, JsonResult, JsonUpcase, PasswordOrOtpData, | ||||
|         EmptyResult, JsonResult, PasswordOrOtpData, | ||||
|     }, | ||||
|     auth::Headers, | ||||
|     db::{ | ||||
| @@ -21,28 +21,30 @@ pub fn routes() -> Vec<Route> { | ||||
|     routes![generate_yubikey, activate_yubikey, activate_yubikey_put,] | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct EnableYubikeyData { | ||||
|     Key1: Option<String>, | ||||
|     Key2: Option<String>, | ||||
|     Key3: Option<String>, | ||||
|     Key4: Option<String>, | ||||
|     Key5: Option<String>, | ||||
|     Nfc: bool, | ||||
|     MasterPasswordHash: Option<String>, | ||||
|     Otp: Option<String>, | ||||
|     key1: Option<String>, | ||||
|     key2: Option<String>, | ||||
|     key3: Option<String>, | ||||
|     key4: Option<String>, | ||||
|     key5: Option<String>, | ||||
|     nfc: bool, | ||||
|     master_password_hash: Option<String>, | ||||
|     otp: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct YubikeyMetadata { | ||||
|     Keys: Vec<String>, | ||||
|     pub Nfc: bool, | ||||
|     #[serde(rename = "keys", alias = "Keys")] | ||||
|     keys: Vec<String>, | ||||
|     #[serde(rename = "nfc", alias = "Nfc")] | ||||
|     pub nfc: bool, | ||||
| } | ||||
|  | ||||
| fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> { | ||||
|     let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5]; | ||||
|     let data_keys = [&data.key1, &data.key2, &data.key3, &data.key4, &data.key5]; | ||||
|  | ||||
|     data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect() | ||||
| } | ||||
| @@ -81,11 +83,11 @@ async fn verify_yubikey_otp(otp: String) -> EmptyResult { | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/get-yubikey", data = "<data>")] | ||||
| async fn generate_yubikey(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
| async fn generate_yubikey(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     // Make sure the credentials are set | ||||
|     get_yubico_credentials()?; | ||||
|  | ||||
|     let data: PasswordOrOtpData = data.into_inner().data; | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
|     data.validate(&user, false, &mut conn).await?; | ||||
| @@ -98,29 +100,29 @@ async fn generate_yubikey(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, | ||||
|     if let Some(r) = r { | ||||
|         let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?; | ||||
|  | ||||
|         let mut result = jsonify_yubikeys(yubikey_metadata.Keys); | ||||
|         let mut result = jsonify_yubikeys(yubikey_metadata.keys); | ||||
|  | ||||
|         result["Enabled"] = Value::Bool(true); | ||||
|         result["Nfc"] = Value::Bool(yubikey_metadata.Nfc); | ||||
|         result["Object"] = Value::String("twoFactorU2f".to_owned()); | ||||
|         result["enabled"] = Value::Bool(true); | ||||
|         result["nfc"] = Value::Bool(yubikey_metadata.nfc); | ||||
|         result["object"] = Value::String("twoFactorU2f".to_owned()); | ||||
|  | ||||
|         Ok(Json(result)) | ||||
|     } else { | ||||
|         Ok(Json(json!({ | ||||
|             "Enabled": false, | ||||
|             "Object": "twoFactorU2f", | ||||
|             "enabled": false, | ||||
|             "object": "twoFactorU2f", | ||||
|         }))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/yubikey", data = "<data>")] | ||||
| async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EnableYubikeyData = data.into_inner().data; | ||||
| async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
|     let data: EnableYubikeyData = data.into_inner(); | ||||
|     let mut user = headers.user; | ||||
|  | ||||
|     PasswordOrOtpData { | ||||
|         MasterPasswordHash: data.MasterPasswordHash.clone(), | ||||
|         Otp: data.Otp.clone(), | ||||
|         master_password_hash: data.master_password_hash.clone(), | ||||
|         otp: data.otp.clone(), | ||||
|     } | ||||
|     .validate(&user, true, &mut conn) | ||||
|     .await?; | ||||
| @@ -136,8 +138,8 @@ async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, | ||||
|  | ||||
|     if yubikeys.is_empty() { | ||||
|         return Ok(Json(json!({ | ||||
|             "Enabled": false, | ||||
|             "Object": "twoFactorU2f", | ||||
|             "enabled": false, | ||||
|             "object": "twoFactorU2f", | ||||
|         }))); | ||||
|     } | ||||
|  | ||||
| @@ -154,8 +156,8 @@ async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, | ||||
|     let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (x[..12]).to_owned()).collect(); | ||||
|  | ||||
|     let yubikey_metadata = YubikeyMetadata { | ||||
|         Keys: yubikey_ids, | ||||
|         Nfc: data.Nfc, | ||||
|         keys: yubikey_ids, | ||||
|         nfc: data.nfc, | ||||
|     }; | ||||
|  | ||||
|     yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap(); | ||||
| @@ -165,17 +167,17 @@ async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, | ||||
|  | ||||
|     log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn).await; | ||||
|  | ||||
|     let mut result = jsonify_yubikeys(yubikey_metadata.Keys); | ||||
|     let mut result = jsonify_yubikeys(yubikey_metadata.keys); | ||||
|  | ||||
|     result["Enabled"] = Value::Bool(true); | ||||
|     result["Nfc"] = Value::Bool(yubikey_metadata.Nfc); | ||||
|     result["Object"] = Value::String("twoFactorU2f".to_owned()); | ||||
|     result["enabled"] = Value::Bool(true); | ||||
|     result["nfc"] = Value::Bool(yubikey_metadata.nfc); | ||||
|     result["object"] = Value::String("twoFactorU2f".to_owned()); | ||||
|  | ||||
|     Ok(Json(result)) | ||||
| } | ||||
|  | ||||
| #[put("/two-factor/yubikey", data = "<data>")] | ||||
| async fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
| async fn activate_yubikey_put(data: Json<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
|     activate_yubikey(data, headers, conn).await | ||||
| } | ||||
|  | ||||
| @@ -187,7 +189,7 @@ pub async fn validate_yubikey_login(response: &str, twofactor_data: &str) -> Emp | ||||
|     let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata"); | ||||
|     let response_id = &response[..12]; | ||||
|  | ||||
|     if !yubikey_metadata.Keys.contains(&response_id.to_owned()) { | ||||
|     if !yubikey_metadata.keys.contains(&response_id.to_owned()) { | ||||
|         err!("Given Yubikey is not registered"); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -15,7 +15,7 @@ use crate::{ | ||||
|             two_factor::{authenticator, duo, email, enforce_2fa_policy, webauthn, yubikey}, | ||||
|         }, | ||||
|         push::register_push_device, | ||||
|         ApiResult, EmptyResult, JsonResult, JsonUpcase, | ||||
|         ApiResult, EmptyResult, JsonResult, | ||||
|     }, | ||||
|     auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, | ||||
|     db::{models::*, DbConn}, | ||||
| @@ -564,8 +564,11 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo | ||||
|     let mut result = json!({ | ||||
|         "error" : "invalid_grant", | ||||
|         "error_description" : "Two factor required.", | ||||
|         "TwoFactorProviders" : providers, | ||||
|         "TwoFactorProviders2" : {} // { "0" : null } | ||||
|         "TwoFactorProviders" : providers.iter().map(ToString::to_string).collect::<Vec<String>>(), | ||||
|         "TwoFactorProviders2" : {}, // { "0" : null } | ||||
|         "MasterPasswordPolicy": { | ||||
|             "Object": "masterPasswordPolicy" | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     for provider in providers { | ||||
| @@ -602,7 +605,7 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo | ||||
|                 let yubikey_metadata: yubikey::YubikeyMetadata = serde_json::from_str(&twofactor.data)?; | ||||
|  | ||||
|                 result["TwoFactorProviders2"][provider.to_string()] = json!({ | ||||
|                     "Nfc": yubikey_metadata.Nfc, | ||||
|                     "Nfc": yubikey_metadata.nfc, | ||||
|                 }) | ||||
|             } | ||||
|  | ||||
| @@ -631,19 +634,18 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo | ||||
| } | ||||
|  | ||||
| #[post("/accounts/prelogin", data = "<data>")] | ||||
| async fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> { | ||||
| async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> { | ||||
|     _prelogin(data, conn).await | ||||
| } | ||||
|  | ||||
| #[post("/accounts/register", data = "<data>")] | ||||
| async fn identity_register(data: JsonUpcase<RegisterData>, conn: DbConn) -> JsonResult { | ||||
| async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult { | ||||
|     _register(data, conn).await | ||||
| } | ||||
|  | ||||
| // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts | ||||
| // https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs | ||||
| #[derive(Debug, Clone, Default, FromForm)] | ||||
| #[allow(non_snake_case)] | ||||
| struct ConnectData { | ||||
|     #[field(name = uncased("grant_type"))] | ||||
|     #[field(name = uncased("granttype"))] | ||||
|   | ||||
| @@ -33,23 +33,18 @@ pub use crate::api::{ | ||||
|     web::static_files, | ||||
| }; | ||||
| use crate::db::{models::User, DbConn}; | ||||
| use crate::util; | ||||
|  | ||||
| // Type aliases for API methods results | ||||
| type ApiResult<T> = Result<T, crate::error::Error>; | ||||
| pub type JsonResult = ApiResult<Json<Value>>; | ||||
| pub type EmptyResult = ApiResult<()>; | ||||
|  | ||||
| type JsonUpcase<T> = Json<util::UpCase<T>>; | ||||
| type JsonUpcaseVec<T> = Json<Vec<util::UpCase<T>>>; | ||||
| type JsonVec<T> = Json<Vec<T>>; | ||||
|  | ||||
| // Common structs representing JSON data received | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct PasswordOrOtpData { | ||||
|     MasterPasswordHash: Option<String>, | ||||
|     Otp: Option<String>, | ||||
|     master_password_hash: Option<String>, | ||||
|     otp: Option<String>, | ||||
| } | ||||
|  | ||||
| impl PasswordOrOtpData { | ||||
| @@ -59,7 +54,7 @@ impl PasswordOrOtpData { | ||||
|     pub async fn validate(&self, user: &User, delete_if_valid: bool, conn: &mut DbConn) -> EmptyResult { | ||||
|         use crate::api::core::two_factor::protected_actions::validate_protected_action_otp; | ||||
|  | ||||
|         match (self.MasterPasswordHash.as_deref(), self.Otp.as_deref()) { | ||||
|         match (self.master_password_hash.as_deref(), self.otp.as_deref()) { | ||||
|             (Some(pw_hash), None) => { | ||||
|                 if !user.check_valid_password(pw_hash) { | ||||
|                     err!("Invalid password"); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user