Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-05-02 15:32:38 +02:00
Change API inputs/outputs and structs to camelCase
Dieser Commit ist enthalten in:
Ursprung
93636eb3c3
Commit
909e0d7672
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
|
|
@ -6,7 +6,7 @@ use serde_json::Value;
|
|||
use crate::{
|
||||
api::{
|
||||
core::{log_user_event, two_factor::email},
|
||||
register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, JsonUpcase, Notify,
|
||||
register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, Notify,
|
||||
PasswordOrOtpData, UpdateType,
|
||||
},
|
||||
auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},
|
||||
|
@ -63,29 +63,29 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||
]
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterData {
|
||||
Email: String,
|
||||
Kdf: Option<i32>,
|
||||
KdfIterations: Option<i32>,
|
||||
KdfMemory: Option<i32>,
|
||||
KdfParallelism: Option<i32>,
|
||||
Key: String,
|
||||
Keys: Option<KeysData>,
|
||||
MasterPasswordHash: String,
|
||||
MasterPasswordHint: Option<String>,
|
||||
Name: Option<String>,
|
||||
Token: Option<String>,
|
||||
email: String,
|
||||
kdf: Option<i32>,
|
||||
kdf_iterations: Option<i32>,
|
||||
kdf_memory: Option<i32>,
|
||||
kdf_parallelism: Option<i32>,
|
||||
key: String,
|
||||
keys: Option<KeysData>,
|
||||
master_password_hash: String,
|
||||
master_password_hint: Option<String>,
|
||||
name: Option<String>,
|
||||
token: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
OrganizationUserId: Option<String>,
|
||||
organization_user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KeysData {
|
||||
EncryptedPrivateKey: String,
|
||||
PublicKey: String,
|
||||
encrypted_private_key: String,
|
||||
public_key: String,
|
||||
}
|
||||
|
||||
/// Trims whitespace from password hints, and converts blank password hints to `None`.
|
||||
|
@ -120,17 +120,17 @@ async fn is_email_2fa_required(org_user_uuid: Option<String>, conn: &mut DbConn)
|
|||
}
|
||||
|
||||
#[post("/accounts/register", data = "<data>")]
|
||||
async fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> JsonResult {
|
||||
async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||
_register(data, conn).await
|
||||
}
|
||||
|
||||
pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> JsonResult {
|
||||
let data: RegisterData = data.into_inner().data;
|
||||
let email = data.Email.to_lowercase();
|
||||
pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult {
|
||||
let data: RegisterData = data.into_inner();
|
||||
let email = data.email.to_lowercase();
|
||||
|
||||
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
|
||||
// This also prevents issues with very long usernames causing to large JWT's. See #2419
|
||||
if let Some(ref name) = data.Name {
|
||||
if let Some(ref name) = data.name {
|
||||
if name.len() > 50 {
|
||||
err!("The field Name must be a string with a maximum length of 50.");
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
|||
|
||||
// Check against the password hint setting here so if it fails, the user
|
||||
// can retry without losing their invitation below.
|
||||
let password_hint = clean_password_hint(&data.MasterPasswordHint);
|
||||
let password_hint = clean_password_hint(&data.master_password_hint);
|
||||
enforce_password_hint_setting(&password_hint)?;
|
||||
|
||||
let mut verified_by_invite = false;
|
||||
|
@ -149,7 +149,7 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
|||
err!("Registration not allowed or user already exists")
|
||||
}
|
||||
|
||||
if let Some(token) = data.Token {
|
||||
if let Some(token) = data.token {
|
||||
let claims = decode_invite(&token)?;
|
||||
if claims.email == email {
|
||||
// Verify the email address when signing up via a valid invite token
|
||||
|
@ -188,28 +188,28 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
|||
// Make sure we don't leave a lingering invitation.
|
||||
Invitation::take(&email, &mut conn).await;
|
||||
|
||||
if let Some(client_kdf_type) = data.Kdf {
|
||||
if let Some(client_kdf_type) = data.kdf {
|
||||
user.client_kdf_type = client_kdf_type;
|
||||
}
|
||||
|
||||
if let Some(client_kdf_iter) = data.KdfIterations {
|
||||
if let Some(client_kdf_iter) = data.kdf_iterations {
|
||||
user.client_kdf_iter = client_kdf_iter;
|
||||
}
|
||||
|
||||
user.client_kdf_memory = data.KdfMemory;
|
||||
user.client_kdf_parallelism = data.KdfParallelism;
|
||||
user.client_kdf_memory = data.kdf_memory;
|
||||
user.client_kdf_parallelism = data.kdf_parallelism;
|
||||
|
||||
user.set_password(&data.MasterPasswordHash, Some(data.Key), true, None);
|
||||
user.set_password(&data.master_password_hash, Some(data.key), true, None);
|
||||
user.password_hint = password_hint;
|
||||
|
||||
// Add extra fields if present
|
||||
if let Some(name) = data.Name {
|
||||
if let Some(name) = data.name {
|
||||
user.name = name;
|
||||
}
|
||||
|
||||
if let Some(keys) = data.Keys {
|
||||
user.private_key = Some(keys.EncryptedPrivateKey);
|
||||
user.public_key = Some(keys.PublicKey);
|
||||
if let Some(keys) = data.keys {
|
||||
user.private_key = Some(keys.encrypted_private_key);
|
||||
user.public_key = Some(keys.public_key);
|
||||
}
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
|
@ -223,15 +223,15 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
|||
error!("Error sending welcome email: {:#?}", e);
|
||||
}
|
||||
|
||||
if verified_by_invite && is_email_2fa_required(data.OrganizationUserId, &mut conn).await {
|
||||
if verified_by_invite && is_email_2fa_required(data.organization_user_id, &mut conn).await {
|
||||
let _ = email::activate_email_2fa(&user, &mut conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
user.save(&mut conn).await?;
|
||||
Ok(Json(json!({
|
||||
"Object": "register",
|
||||
"CaptchaBypassToken": "",
|
||||
"object": "register",
|
||||
"captchaBypassToken": "",
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -240,57 +240,57 @@ async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
|||
Json(headers.user.to_json(&mut conn).await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProfileData {
|
||||
// Culture: String, // Ignored, always use en-US
|
||||
// MasterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData
|
||||
Name: String,
|
||||
// culture: String, // Ignored, always use en-US
|
||||
// masterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[put("/accounts/profile", data = "<data>")]
|
||||
async fn put_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
async fn put_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
post_profile(data, headers, conn).await
|
||||
}
|
||||
|
||||
#[post("/accounts/profile", data = "<data>")]
|
||||
async fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let data: ProfileData = data.into_inner().data;
|
||||
async fn post_profile(data: Json<ProfileData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let data: ProfileData = data.into_inner();
|
||||
|
||||
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
|
||||
// This also prevents issues with very long usernames causing to large JWT's. See #2419
|
||||
if data.Name.len() > 50 {
|
||||
if data.name.len() > 50 {
|
||||
err!("The field Name must be a string with a maximum length of 50.");
|
||||
}
|
||||
|
||||
let mut user = headers.user;
|
||||
user.name = data.Name;
|
||||
user.name = data.name;
|
||||
|
||||
user.save(&mut conn).await?;
|
||||
Ok(Json(user.to_json(&mut conn).await))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AvatarData {
|
||||
AvatarColor: Option<String>,
|
||||
avatar_color: Option<String>,
|
||||
}
|
||||
|
||||
#[put("/accounts/avatar", data = "<data>")]
|
||||
async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let data: AvatarData = data.into_inner().data;
|
||||
async fn put_avatar(data: Json<AvatarData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let data: AvatarData = data.into_inner();
|
||||
|
||||
// It looks like it only supports the 6 hex color format.
|
||||
// If you try to add the short value it will not show that color.
|
||||
// Check and force 7 chars, including the #.
|
||||
if let Some(color) = &data.AvatarColor {
|
||||
if let Some(color) = &data.avatar_color {
|
||||
if color.len() != 7 {
|
||||
err!("The field AvatarColor must be a HTML/Hex color code with a length of 7 characters")
|
||||
}
|
||||
}
|
||||
|
||||
let mut user = headers.user;
|
||||
user.avatar_color = data.AvatarColor;
|
||||
user.avatar_color = data.avatar_color;
|
||||
|
||||
user.save(&mut conn).await?;
|
||||
Ok(Json(user.to_json(&mut conn).await))
|
||||
|
@ -305,62 +305,57 @@ async fn get_public_keys(uuid: &str, _headers: Headers, mut conn: DbConn) -> Jso
|
|||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"UserId": user.uuid,
|
||||
"PublicKey": user.public_key,
|
||||
"Object":"userKey"
|
||||
"userId": user.uuid,
|
||||
"publicKey": user.public_key,
|
||||
"object":"userKey"
|
||||
})))
|
||||
}
|
||||
|
||||
#[post("/accounts/keys", data = "<data>")]
|
||||
async fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let data: KeysData = data.into_inner().data;
|
||||
async fn post_keys(data: Json<KeysData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
let data: KeysData = data.into_inner();
|
||||
|
||||
let mut user = headers.user;
|
||||
|
||||
user.private_key = Some(data.EncryptedPrivateKey);
|
||||
user.public_key = Some(data.PublicKey);
|
||||
user.private_key = Some(data.encrypted_private_key);
|
||||
user.public_key = Some(data.public_key);
|
||||
|
||||
user.save(&mut conn).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"PrivateKey": user.private_key,
|
||||
"PublicKey": user.public_key,
|
||||
"Object":"keys"
|
||||
"privateKey": user.private_key,
|
||||
"publicKey": user.public_key,
|
||||
"object":"keys"
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangePassData {
|
||||
MasterPasswordHash: String,
|
||||
NewMasterPasswordHash: String,
|
||||
MasterPasswordHint: Option<String>,
|
||||
Key: String,
|
||||
master_password_hash: String,
|
||||
new_master_password_hash: String,
|
||||
master_password_hint: Option<String>,
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/password", data = "<data>")]
|
||||
async fn post_password(
|
||||
data: JsonUpcase<ChangePassData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
let data: ChangePassData = data.into_inner().data;
|
||||
async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let data: ChangePassData = data.into_inner();
|
||||
let mut user = headers.user;
|
||||
|
||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||
if !user.check_valid_password(&data.master_password_hash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
user.password_hint = clean_password_hint(&data.MasterPasswordHint);
|
||||
user.password_hint = clean_password_hint(&data.master_password_hint);
|
||||
enforce_password_hint_setting(&user.password_hint)?;
|
||||
|
||||
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn)
|
||||
.await;
|
||||
|
||||
user.set_password(
|
||||
&data.NewMasterPasswordHash,
|
||||
Some(data.Key),
|
||||
&data.new_master_password_hash,
|
||||
Some(data.key),
|
||||
true,
|
||||
Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
|
||||
);
|
||||
|
@ -376,48 +371,48 @@ async fn post_password(
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangeKdfData {
|
||||
Kdf: i32,
|
||||
KdfIterations: i32,
|
||||
KdfMemory: Option<i32>,
|
||||
KdfParallelism: Option<i32>,
|
||||
kdf: i32,
|
||||
kdf_iterations: i32,
|
||||
kdf_memory: Option<i32>,
|
||||
kdf_parallelism: Option<i32>,
|
||||
|
||||
MasterPasswordHash: String,
|
||||
NewMasterPasswordHash: String,
|
||||
Key: String,
|
||||
master_password_hash: String,
|
||||
new_master_password_hash: String,
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/kdf", data = "<data>")]
|
||||
async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let data: ChangeKdfData = data.into_inner().data;
|
||||
async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let data: ChangeKdfData = data.into_inner();
|
||||
let mut user = headers.user;
|
||||
|
||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||
if !user.check_valid_password(&data.master_password_hash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
if data.Kdf == UserKdfType::Pbkdf2 as i32 && data.KdfIterations < 100_000 {
|
||||
if data.kdf == UserKdfType::Pbkdf2 as i32 && data.kdf_iterations < 100_000 {
|
||||
err!("PBKDF2 KDF iterations must be at least 100000.")
|
||||
}
|
||||
|
||||
if data.Kdf == UserKdfType::Argon2id as i32 {
|
||||
if data.KdfIterations < 1 {
|
||||
if data.kdf == UserKdfType::Argon2id as i32 {
|
||||
if data.kdf_iterations < 1 {
|
||||
err!("Argon2 KDF iterations must be at least 1.")
|
||||
}
|
||||
if let Some(m) = data.KdfMemory {
|
||||
if let Some(m) = data.kdf_memory {
|
||||
if !(15..=1024).contains(&m) {
|
||||
err!("Argon2 memory must be between 15 MB and 1024 MB.")
|
||||
}
|
||||
user.client_kdf_memory = data.KdfMemory;
|
||||
user.client_kdf_memory = data.kdf_memory;
|
||||
} else {
|
||||
err!("Argon2 memory parameter is required.")
|
||||
}
|
||||
if let Some(p) = data.KdfParallelism {
|
||||
if let Some(p) = data.kdf_parallelism {
|
||||
if !(1..=16).contains(&p) {
|
||||
err!("Argon2 parallelism must be between 1 and 16.")
|
||||
}
|
||||
user.client_kdf_parallelism = data.KdfParallelism;
|
||||
user.client_kdf_parallelism = data.kdf_parallelism;
|
||||
} else {
|
||||
err!("Argon2 parallelism parameter is required.")
|
||||
}
|
||||
|
@ -425,9 +420,9 @@ async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: D
|
|||
user.client_kdf_memory = None;
|
||||
user.client_kdf_parallelism = None;
|
||||
}
|
||||
user.client_kdf_iter = data.KdfIterations;
|
||||
user.client_kdf_type = data.Kdf;
|
||||
user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
|
||||
user.client_kdf_iter = data.kdf_iterations;
|
||||
user.client_kdf_type = data.kdf;
|
||||
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
|
||||
let save_result = user.save(&mut conn).await;
|
||||
|
||||
nt.send_logout(&user, Some(headers.device.uuid)).await;
|
||||
|
@ -436,29 +431,29 @@ async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: D
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UpdateFolderData {
|
||||
Id: String,
|
||||
Name: String,
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
use super::ciphers::CipherData;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KeyData {
|
||||
Ciphers: Vec<CipherData>,
|
||||
Folders: Vec<UpdateFolderData>,
|
||||
Key: String,
|
||||
PrivateKey: String,
|
||||
MasterPasswordHash: String,
|
||||
ciphers: Vec<CipherData>,
|
||||
folders: Vec<UpdateFolderData>,
|
||||
key: String,
|
||||
private_key: String,
|
||||
master_password_hash: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/key", data = "<data>")]
|
||||
async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let data: KeyData = data.into_inner().data;
|
||||
async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let data: KeyData = data.into_inner();
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
if !headers.user.check_valid_password(&data.master_password_hash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
|
@ -466,13 +461,13 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D
|
|||
// 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)?;
|
||||
|
||||
let user_uuid = &headers.user.uuid;
|
||||
|
||||
// Update folder data
|
||||
for folder_data in data.Folders {
|
||||
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &mut conn).await {
|
||||
for folder_data in data.folders {
|
||||
let mut saved_folder = match Folder::find_by_uuid(&folder_data.id, &mut conn).await {
|
||||
Some(folder) => folder,
|
||||
None => err!("Folder doesn't exist"),
|
||||
};
|
||||
|
@ -481,15 +476,15 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D
|
|||
err!("The folder is not owned by the user")
|
||||
}
|
||||
|
||||
saved_folder.name = folder_data.Name;
|
||||
saved_folder.name = folder_data.name;
|
||||
saved_folder.save(&mut conn).await?
|
||||
}
|
||||
|
||||
// Update cipher data
|
||||
use super::ciphers::update_cipher_from_data;
|
||||
|
||||
for cipher_data in data.Ciphers {
|
||||
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &mut conn).await {
|
||||
for cipher_data in data.ciphers {
|
||||
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.id.as_ref().unwrap(), &mut conn).await {
|
||||
Some(cipher) => cipher,
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
|
@ -508,8 +503,8 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D
|
|||
// Update user data
|
||||
let mut user = headers.user;
|
||||
|
||||
user.akey = data.Key;
|
||||
user.private_key = Some(data.PrivateKey);
|
||||
user.akey = data.key;
|
||||
user.private_key = Some(data.private_key);
|
||||
user.reset_security_stamp();
|
||||
|
||||
let save_result = user.save(&mut conn).await;
|
||||
|
@ -523,13 +518,8 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D
|
|||
}
|
||||
|
||||
#[post("/accounts/security-stamp", data = "<data>")]
|
||||
async fn post_sstamp(
|
||||
data: JsonUpcase<PasswordOrOtpData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
let data: PasswordOrOtpData = data.into_inner().data;
|
||||
async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let data: PasswordOrOtpData = data.into_inner();
|
||||
let mut user = headers.user;
|
||||
|
||||
data.validate(&user, true, &mut conn).await?;
|
||||
|
@ -544,84 +534,79 @@ async fn post_sstamp(
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EmailTokenData {
|
||||
MasterPasswordHash: String,
|
||||
NewEmail: String,
|
||||
master_password_hash: String,
|
||||
new_email: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/email-token", data = "<data>")]
|
||||
async fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
async fn post_email_token(data: Json<EmailTokenData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
if !CONFIG.email_change_allowed() {
|
||||
err!("Email change is not allowed.");
|
||||
}
|
||||
|
||||
let data: EmailTokenData = data.into_inner().data;
|
||||
let data: EmailTokenData = data.into_inner();
|
||||
let mut user = headers.user;
|
||||
|
||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||
if !user.check_valid_password(&data.master_password_hash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
if User::find_by_mail(&data.NewEmail, &mut conn).await.is_some() {
|
||||
if User::find_by_mail(&data.new_email, &mut conn).await.is_some() {
|
||||
err!("Email already in use");
|
||||
}
|
||||
|
||||
if !CONFIG.is_email_domain_allowed(&data.NewEmail) {
|
||||
if !CONFIG.is_email_domain_allowed(&data.new_email) {
|
||||
err!("Email domain not allowed");
|
||||
}
|
||||
|
||||
let token = crypto::generate_email_token(6);
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
if let Err(e) = mail::send_change_email(&data.NewEmail, &token).await {
|
||||
if let Err(e) = mail::send_change_email(&data.new_email, &token).await {
|
||||
error!("Error sending change-email email: {:#?}", e);
|
||||
}
|
||||
} else {
|
||||
debug!("Email change request for user ({}) to email ({}) with token ({})", user.uuid, data.NewEmail, token);
|
||||
debug!("Email change request for user ({}) to email ({}) with token ({})", user.uuid, data.new_email, token);
|
||||
}
|
||||
|
||||
user.email_new = Some(data.NewEmail);
|
||||
user.email_new = Some(data.new_email);
|
||||
user.email_new_token = Some(token);
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangeEmailData {
|
||||
MasterPasswordHash: String,
|
||||
NewEmail: String,
|
||||
master_password_hash: String,
|
||||
new_email: String,
|
||||
|
||||
Key: String,
|
||||
NewMasterPasswordHash: String,
|
||||
Token: NumberOrString,
|
||||
key: String,
|
||||
new_master_password_hash: String,
|
||||
token: NumberOrString,
|
||||
}
|
||||
|
||||
#[post("/accounts/email", data = "<data>")]
|
||||
async fn post_email(
|
||||
data: JsonUpcase<ChangeEmailData>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
async fn post_email(data: Json<ChangeEmailData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
if !CONFIG.email_change_allowed() {
|
||||
err!("Email change is not allowed.");
|
||||
}
|
||||
|
||||
let data: ChangeEmailData = data.into_inner().data;
|
||||
let data: ChangeEmailData = data.into_inner();
|
||||
let mut user = headers.user;
|
||||
|
||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||
if !user.check_valid_password(&data.master_password_hash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
if User::find_by_mail(&data.NewEmail, &mut conn).await.is_some() {
|
||||
if User::find_by_mail(&data.new_email, &mut conn).await.is_some() {
|
||||
err!("Email already in use");
|
||||
}
|
||||
|
||||
match user.email_new {
|
||||
Some(ref val) => {
|
||||
if val != &data.NewEmail {
|
||||
if val != &data.new_email {
|
||||
err!("Email change mismatch");
|
||||
}
|
||||
}
|
||||
|
@ -632,7 +617,7 @@ async fn post_email(
|
|||
// Only check the token if we sent out an email...
|
||||
match user.email_new_token {
|
||||
Some(ref val) => {
|
||||
if *val != data.Token.into_string() {
|
||||
if *val != data.token.into_string() {
|
||||
err!("Token mismatch");
|
||||
}
|
||||
}
|
||||
|
@ -643,11 +628,11 @@ async fn post_email(
|
|||
user.verified_at = None;
|
||||
}
|
||||
|
||||
user.email = data.NewEmail;
|
||||
user.email = data.new_email;
|
||||
user.email_new = None;
|
||||
user.email_new_token = None;
|
||||
|
||||
user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
|
||||
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
|
||||
|
||||
let save_result = user.save(&mut conn).await;
|
||||
|
||||
|
@ -672,22 +657,22 @@ async fn post_verify_email(headers: Headers) -> EmptyResult {
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VerifyEmailTokenData {
|
||||
UserId: String,
|
||||
Token: String,
|
||||
user_id: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/verify-email-token", data = "<data>")]
|
||||
async fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, mut conn: DbConn) -> EmptyResult {
|
||||
let data: VerifyEmailTokenData = data.into_inner().data;
|
||||
async fn post_verify_email_token(data: Json<VerifyEmailTokenData>, mut conn: DbConn) -> EmptyResult {
|
||||
let data: VerifyEmailTokenData = data.into_inner();
|
||||
|
||||
let mut user = match User::find_by_uuid(&data.UserId, &mut conn).await {
|
||||
let mut user = match User::find_by_uuid(&data.user_id, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("User doesn't exist"),
|
||||
};
|
||||
|
||||
let claims = match decode_verify_email(&data.Token) {
|
||||
let claims = match decode_verify_email(&data.token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => err!("Invalid claim"),
|
||||
};
|
||||
|
@ -705,17 +690,17 @@ async fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, mut con
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeleteRecoverData {
|
||||
Email: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/delete-recover", data = "<data>")]
|
||||
async fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, mut conn: DbConn) -> EmptyResult {
|
||||
let data: DeleteRecoverData = data.into_inner().data;
|
||||
async fn post_delete_recover(data: Json<DeleteRecoverData>, mut conn: DbConn) -> EmptyResult {
|
||||
let data: DeleteRecoverData = data.into_inner();
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
if let Some(user) = User::find_by_mail(&data.Email, &mut conn).await {
|
||||
if let Some(user) = User::find_by_mail(&data.email, &mut conn).await {
|
||||
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await {
|
||||
error!("Error sending delete account email: {:#?}", e);
|
||||
}
|
||||
|
@ -731,22 +716,22 @@ async fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, mut conn: DbCo
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DeleteRecoverTokenData {
|
||||
UserId: String,
|
||||
Token: String,
|
||||
user_id: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/delete-recover-token", data = "<data>")]
|
||||
async fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, mut conn: DbConn) -> EmptyResult {
|
||||
let data: DeleteRecoverTokenData = data.into_inner().data;
|
||||
async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, mut conn: DbConn) -> EmptyResult {
|
||||
let data: DeleteRecoverTokenData = data.into_inner();
|
||||
|
||||
let user = match User::find_by_uuid(&data.UserId, &mut conn).await {
|
||||
let user = match User::find_by_uuid(&data.user_id, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => err!("User doesn't exist"),
|
||||
};
|
||||
|
||||
let claims = match decode_delete(&data.Token) {
|
||||
let claims = match decode_delete(&data.token) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => err!("Invalid claim"),
|
||||
};
|
||||
|
@ -757,13 +742,13 @@ async fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, mut
|
|||
}
|
||||
|
||||
#[post("/accounts/delete", data = "<data>")]
|
||||
async fn post_delete_account(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
async fn post_delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
delete_account(data, headers, conn).await
|
||||
}
|
||||
|
||||
#[delete("/accounts", data = "<data>")]
|
||||
async fn delete_account(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
let data: PasswordOrOtpData = data.into_inner().data;
|
||||
async fn delete_account(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
let data: PasswordOrOtpData = data.into_inner();
|
||||
let user = headers.user;
|
||||
|
||||
data.validate(&user, true, &mut conn).await?;
|
||||
|
@ -778,21 +763,21 @@ fn revision_date(headers: Headers) -> JsonResult {
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PasswordHintData {
|
||||
Email: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/password-hint", data = "<data>")]
|
||||
async fn password_hint(data: JsonUpcase<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
|
||||
async fn password_hint(data: Json<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
|
||||
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() {
|
||||
err!("This server is not configured to provide password hints.");
|
||||
}
|
||||
|
||||
const NO_HINT: &str = "Sorry, you have no password hint...";
|
||||
|
||||
let data: PasswordHintData = data.into_inner().data;
|
||||
let email = &data.Email;
|
||||
let data: PasswordHintData = data.into_inner();
|
||||
let email = &data.email;
|
||||
|
||||
match User::find_by_mail(email, &mut conn).await {
|
||||
None => {
|
||||
|
@ -826,29 +811,29 @@ async fn password_hint(data: JsonUpcase<PasswordHintData>, mut conn: DbConn) ->
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PreloginData {
|
||||
Email: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
|
||||
pub async fn _prelogin(data: JsonUpcase<PreloginData>, mut conn: DbConn) -> Json<Value> {
|
||||
let data: PreloginData = data.into_inner().data;
|
||||
pub async fn _prelogin(data: Json<PreloginData>, mut conn: DbConn) -> Json<Value> {
|
||||
let data: PreloginData = data.into_inner();
|
||||
|
||||
let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.Email, &mut conn).await {
|
||||
let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &mut conn).await {
|
||||
Some(user) => (user.client_kdf_type, user.client_kdf_iter, user.client_kdf_memory, user.client_kdf_parallelism),
|
||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
|
||||
};
|
||||
|
||||
let result = json!({
|
||||
"Kdf": kdf_type,
|
||||
"KdfIterations": kdf_iter,
|
||||
"KdfMemory": kdf_mem,
|
||||
"KdfParallelism": kdf_para,
|
||||
"kdf": kdf_type,
|
||||
"kdfIterations": kdf_iter,
|
||||
"kdfMemory": kdf_mem,
|
||||
"kdfParallelism": kdf_para,
|
||||
});
|
||||
|
||||
Json(result)
|
||||
|
@ -856,27 +841,27 @@ pub async fn _prelogin(data: JsonUpcase<PreloginData>, mut conn: DbConn) -> Json
|
|||
|
||||
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SecretVerificationRequest {
|
||||
MasterPasswordHash: String,
|
||||
master_password_hash: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/verify-password", data = "<data>")]
|
||||
fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers) -> EmptyResult {
|
||||
let data: SecretVerificationRequest = data.into_inner().data;
|
||||
fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers) -> EmptyResult {
|
||||
let data: SecretVerificationRequest = data.into_inner();
|
||||
let user = headers.user;
|
||||
|
||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
||||
if !user.check_valid_password(&data.master_password_hash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _api_key(data: JsonUpcase<PasswordOrOtpData>, rotate: bool, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn _api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
use crate::util::format_date;
|
||||
|
||||
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?;
|
||||
|
@ -887,19 +872,19 @@ async fn _api_key(data: JsonUpcase<PasswordOrOtpData>, rotate: bool, headers: He
|
|||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"ApiKey": user.api_key,
|
||||
"RevisionDate": format_date(&user.updated_at),
|
||||
"Object": "apiKey",
|
||||
"apiKey": user.api_key,
|
||||
"revisionDate": format_date(&user.updated_at),
|
||||
"object": "apiKey",
|
||||
})))
|
||||
}
|
||||
|
||||
#[post("/accounts/api-key", data = "<data>")]
|
||||
async fn api_key(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
async fn api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
_api_key(data, false, headers, conn).await
|
||||
}
|
||||
|
||||
#[post("/accounts/rotate-api-key", data = "<data>")]
|
||||
async fn rotate_api_key(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
async fn rotate_api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
_api_key(data, true, headers, conn).await
|
||||
}
|
||||
|
||||
|
@ -960,20 +945,20 @@ impl<'r> FromRequest<'r> for KnownDevice {
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PushToken {
|
||||
PushToken: String,
|
||||
push_token: String,
|
||||
}
|
||||
|
||||
#[post("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
async fn post_device_token(uuid: &str, data: JsonUpcase<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
async fn post_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
put_device_token(uuid, data, headers, conn).await
|
||||
}
|
||||
|
||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
async fn put_device_token(uuid: &str, data: JsonUpcase<PushToken>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
let data = data.into_inner().data;
|
||||
let token = data.PushToken;
|
||||
async fn put_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||
let data = data.into_inner();
|
||||
let token = data.push_token;
|
||||
|
||||
let mut device = match Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await {
|
||||
Some(device) => device,
|
||||
|
@ -1028,12 +1013,12 @@ async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AuthRequestRequest {
|
||||
accessCode: String,
|
||||
deviceIdentifier: String,
|
||||
access_code: String,
|
||||
device_identifier: String,
|
||||
email: String,
|
||||
publicKey: String,
|
||||
public_key: String,
|
||||
#[serde(alias = "type")]
|
||||
_type: i32,
|
||||
}
|
||||
|
@ -1056,15 +1041,15 @@ async fn post_auth_request(
|
|||
|
||||
let mut auth_request = AuthRequest::new(
|
||||
user.uuid.clone(),
|
||||
data.deviceIdentifier.clone(),
|
||||
data.device_identifier.clone(),
|
||||
headers.device_type,
|
||||
headers.ip.ip.to_string(),
|
||||
data.accessCode,
|
||||
data.publicKey,
|
||||
data.access_code,
|
||||
data.public_key,
|
||||
);
|
||||
auth_request.save(&mut conn).await?;
|
||||
|
||||
nt.send_auth_request(&user.uuid, &auth_request.uuid, &data.deviceIdentifier, &mut conn).await;
|
||||
nt.send_auth_request(&user.uuid, &auth_request.uuid, &data.device_identifier, &mut conn).await;
|
||||
|
||||
Ok(Json(json!({
|
||||
"id": auth_request.uuid,
|
||||
|
@ -1110,12 +1095,12 @@ async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AuthResponseRequest {
|
||||
deviceIdentifier: String,
|
||||
device_identifier: String,
|
||||
key: String,
|
||||
masterPasswordHash: Option<String>,
|
||||
requestApproved: bool,
|
||||
master_password_hash: Option<String>,
|
||||
request_approved: bool,
|
||||
}
|
||||
|
||||
#[put("/auth-requests/<uuid>", data = "<data>")]
|
||||
|
@ -1134,15 +1119,15 @@ async fn put_auth_request(
|
|||
}
|
||||
};
|
||||
|
||||
auth_request.approved = Some(data.requestApproved);
|
||||
auth_request.approved = Some(data.request_approved);
|
||||
auth_request.enc_key = Some(data.key);
|
||||
auth_request.master_password_hash = data.masterPasswordHash;
|
||||
auth_request.response_device_id = Some(data.deviceIdentifier.clone());
|
||||
auth_request.master_password_hash = data.master_password_hash;
|
||||
auth_request.response_device_id = Some(data.device_identifier.clone());
|
||||
auth_request.save(&mut conn).await?;
|
||||
|
||||
if auth_request.approved.unwrap_or(false) {
|
||||
ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;
|
||||
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.deviceIdentifier, &mut conn).await;
|
||||
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await;
|
||||
}
|
||||
|
||||
let response_date_utc = auth_request.response_date.map(|response_date| response_date.and_utc());
|
||||
|
|
|
@ -11,7 +11,7 @@ use rocket::{
|
|||
use serde_json::Value;
|
||||
|
||||
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},
|
||||
|
@ -140,15 +140,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"
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -166,9 +166,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
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -197,17 +197,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
|
||||
OrganizationId: Option<String>,
|
||||
organization_id: Option<String>,
|
||||
|
||||
Key: Option<String>,
|
||||
key: Option<String>,
|
||||
|
||||
/*
|
||||
Login = 1,
|
||||
|
@ -215,27 +215,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
|
||||
|
@ -243,31 +243,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
|
||||
}
|
||||
|
||||
|
@ -276,25 +271,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?;
|
||||
|
||||
|
@ -304,23 +299,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, false, &mut conn, &nt, UpdateType::SyncCipherCreate).await?;
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
||||
|
@ -338,7 +333,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 {
|
||||
|
@ -362,7 +357,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),
|
||||
|
@ -374,20 +369,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) => {
|
||||
|
@ -411,7 +406,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 {
|
||||
|
@ -423,7 +418,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,
|
||||
|
@ -444,8 +439,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?;
|
||||
}
|
||||
|
@ -459,44 +454,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
|
||||
|
@ -525,43 +520,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);
|
||||
|
@ -570,16 +565,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, false, &mut conn, &nt, UpdateType::None).await?;
|
||||
}
|
||||
|
||||
|
@ -594,7 +589,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<'_>,
|
||||
|
@ -605,7 +600,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<'_>,
|
||||
|
@ -614,25 +609,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,
|
||||
|
@ -654,12 +643,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
|
||||
}
|
||||
|
||||
|
@ -667,18 +651,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 {
|
||||
|
@ -690,23 +674,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<'_>,
|
||||
|
@ -717,7 +701,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<'_>,
|
||||
|
@ -728,7 +712,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<'_>,
|
||||
|
@ -739,12 +723,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,
|
||||
|
@ -755,7 +739,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();
|
||||
|
||||
|
@ -803,21 +787,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
|
||||
}
|
||||
|
@ -825,53 +809,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"),
|
||||
};
|
||||
|
@ -900,8 +884,8 @@ async fn share_cipher_by_uuid(
|
|||
|
||||
let mut shared_to_collection = false;
|
||||
|
||||
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) => {
|
||||
|
@ -917,13 +901,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, shared_to_collection, conn, nt, ut).await?;
|
||||
update_cipher_from_data(&mut cipher, data.cipher, headers, shared_to_collection, conn, nt, ut).await?;
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await))
|
||||
}
|
||||
|
@ -953,12 +937,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: i64,
|
||||
AdminRequest: Option<bool>, // true when attaching from an org vault view
|
||||
key: String,
|
||||
file_name: String,
|
||||
file_size: i64,
|
||||
admin_request: Option<bool>, // true when attaching from an org vault view
|
||||
}
|
||||
|
||||
enum FileUploadType {
|
||||
|
@ -973,7 +957,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 {
|
||||
|
@ -986,26 +970,26 @@ async fn post_attachment_v2(
|
|||
err!("Cipher is not write accessible")
|
||||
}
|
||||
|
||||
let data: AttachmentRequestData = data.into_inner().data;
|
||||
if data.FileSize < 0 {
|
||||
let data: AttachmentRequestData = data.into_inner();
|
||||
if data.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, data.FileSize, Some(data.Key));
|
||||
Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.file_name, data.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 {
|
||||
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,
|
||||
})))
|
||||
}
|
||||
|
@ -1340,38 +1324,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<'_>,
|
||||
|
@ -1381,7 +1350,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<'_>,
|
||||
|
@ -1391,7 +1360,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<'_>,
|
||||
|
@ -1410,33 +1379,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 {
|
||||
|
@ -1447,7 +1411,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"),
|
||||
|
@ -1458,7 +1422,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,
|
||||
|
@ -1476,7 +1440,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<'_>,
|
||||
|
@ -1493,12 +1457,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?;
|
||||
|
@ -1606,13 +1570,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() {
|
||||
|
@ -1671,12 +1635,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() {
|
||||
|
@ -1695,9 +1659,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;
|
||||
|
@ -65,9 +65,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
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -84,9 +84,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
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -105,42 +105,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?;
|
||||
|
@ -180,24 +176,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."),
|
||||
};
|
||||
|
@ -325,17 +321,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.
|
||||
|
@ -403,23 +399,23 @@ async fn accept_invite_process(
|
|||
}
|
||||
|
||||
#[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,
|
||||
|
@ -614,9 +610,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",
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -640,35 +636,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;
|
||||
|
@ -687,7 +683,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
|
||||
|
@ -725,9 +721,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!"
|
||||
]
|
||||
}])))
|
||||
|
|
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
|
@ -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,12 +137,16 @@ 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(&group_data.ExternalId, &mut conn).await {
|
||||
for group_data in &data.groups {
|
||||
let group_uuid = match Group::find_by_external_id(&group_data.external_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
|
||||
}
|
||||
|
@ -149,7 +154,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());
|
||||
|
@ -162,9 +167,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,23 +48,23 @@ pub async fn purge_sends(pool: DbPool) {
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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>,
|
||||
}
|
||||
|
||||
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
|
||||
|
@ -93,7 +93,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 \
|
||||
|
@ -104,40 +104,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)
|
||||
}
|
||||
|
@ -148,9 +148,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
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -169,13 +169,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")
|
||||
}
|
||||
|
||||
|
@ -195,7 +195,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>,
|
||||
}
|
||||
|
||||
|
@ -215,7 +215,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");
|
||||
|
@ -263,9 +263,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)?;
|
||||
|
||||
|
@ -285,18 +285,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"),
|
||||
};
|
||||
|
@ -331,9 +331,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?;
|
||||
|
@ -392,15 +392,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<'_>,
|
||||
|
@ -431,7 +431,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),
|
||||
|
@ -461,7 +461,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<'_>,
|
||||
|
@ -492,7 +492,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),
|
||||
|
@ -515,9 +515,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)
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -532,16 +532,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 {
|
||||
|
@ -553,15 +547,15 @@ async fn put_send(
|
|||
err!("Send is not owned by user")
|
||||
}
|
||||
|
||||
if send.atype != data.Type {
|
||||
if send.atype != data.r#type {
|
||||
err!("Sends can't change type")
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
@ -569,25 +563,25 @@ async fn put_send(
|
|||
send.data = data_str;
|
||||
}
|
||||
|
||||
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."
|
||||
);
|
||||
}
|
||||
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};
|
|||
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()
|
||||
}
|
||||
|
@ -84,11 +86,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?;
|
||||
|
@ -101,29 +103,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?;
|
||||
|
@ -139,8 +141,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",
|
||||
})));
|
||||
}
|
||||
|
||||
|
@ -157,8 +159,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();
|
||||
|
@ -168,17 +170,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
|
||||
}
|
||||
|
||||
|
@ -190,7 +192,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},
|
||||
|
@ -120,14 +120,14 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
|||
"expires_in": expires_in,
|
||||
"token_type": "Bearer",
|
||||
"refresh_token": device.refresh_token,
|
||||
"Key": user.akey,
|
||||
"PrivateKey": user.private_key,
|
||||
"key": user.akey,
|
||||
"privateKey": user.private_key,
|
||||
|
||||
"Kdf": user.client_kdf_type,
|
||||
"KdfIterations": user.client_kdf_iter,
|
||||
"KdfMemory": user.client_kdf_memory,
|
||||
"KdfParallelism": user.client_kdf_parallelism,
|
||||
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
||||
"kdf": user.client_kdf_type,
|
||||
"kdfIterations": user.client_kdf_iter,
|
||||
"kdfMemory": user.client_kdf_memory,
|
||||
"kdfParallelism": user.client_kdf_parallelism,
|
||||
"resetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
|
||||
"scope": scope,
|
||||
"unofficialServer": true,
|
||||
});
|
||||
|
@ -287,25 +287,25 @@ async fn _password_login(
|
|||
"expires_in": expires_in,
|
||||
"token_type": "Bearer",
|
||||
"refresh_token": device.refresh_token,
|
||||
"Key": user.akey,
|
||||
"PrivateKey": user.private_key,
|
||||
//"TwoFactorToken": "11122233333444555666777888999"
|
||||
"key": user.akey,
|
||||
"privateKey": user.private_key,
|
||||
//"twoFactorToken": "11122233333444555666777888999"
|
||||
|
||||
"Kdf": user.client_kdf_type,
|
||||
"KdfIterations": user.client_kdf_iter,
|
||||
"KdfMemory": user.client_kdf_memory,
|
||||
"KdfParallelism": user.client_kdf_parallelism,
|
||||
"ResetMasterPassword": false,// TODO: Same as above
|
||||
"kdf": user.client_kdf_type,
|
||||
"kdfIterations": user.client_kdf_iter,
|
||||
"kdfMemory": user.client_kdf_memory,
|
||||
"kdfParallelism": user.client_kdf_parallelism,
|
||||
"resetMasterPassword": false,// TODO: Same as above
|
||||
"scope": scope,
|
||||
"unofficialServer": true,
|
||||
"UserDecryptionOptions": {
|
||||
"HasMasterPassword": !user.password_hash.is_empty(),
|
||||
"Object": "userDecryptionOptions"
|
||||
"userDecryptionOptions": {
|
||||
"hasMasterPassword": !user.password_hash.is_empty(),
|
||||
"object": "userDecryptionOptions"
|
||||
},
|
||||
});
|
||||
|
||||
if let Some(token) = twofactor_token {
|
||||
result["TwoFactorToken"] = Value::String(token);
|
||||
result["twoFactorToken"] = Value::String(token);
|
||||
}
|
||||
|
||||
info!("User {} logged in successfully. IP: {}", username, ip.ip);
|
||||
|
@ -409,14 +409,14 @@ async fn _user_api_key_login(
|
|||
"access_token": access_token,
|
||||
"expires_in": expires_in,
|
||||
"token_type": "Bearer",
|
||||
"Key": user.akey,
|
||||
"PrivateKey": user.private_key,
|
||||
"key": user.akey,
|
||||
"privateKey": user.private_key,
|
||||
|
||||
"Kdf": user.client_kdf_type,
|
||||
"KdfIterations": user.client_kdf_iter,
|
||||
"KdfMemory": user.client_kdf_memory,
|
||||
"KdfParallelism": user.client_kdf_parallelism,
|
||||
"ResetMasterPassword": false, // TODO: Same as above
|
||||
"kdf": user.client_kdf_type,
|
||||
"kdfIterations": user.client_kdf_iter,
|
||||
"kdfMemory": user.client_kdf_memory,
|
||||
"kdfParallelism": user.client_kdf_parallelism,
|
||||
"resetMasterPassword": false, // TODO: Same as above
|
||||
"scope": "api",
|
||||
"unofficialServer": true,
|
||||
});
|
||||
|
@ -559,6 +559,7 @@ 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.",
|
||||
// These two are intentionally left with PascalCase, as of 2024-02-27 the clients still use them like this
|
||||
"TwoFactorProviders" : providers,
|
||||
"TwoFactorProviders2" : {} // { "0" : null }
|
||||
});
|
||||
|
@ -583,8 +584,8 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo
|
|||
let (signature, host) = duo::generate_duo_signature(&email, conn).await?;
|
||||
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Host": host,
|
||||
"Signature": signature,
|
||||
"host": host,
|
||||
"signature": signature,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -597,7 +598,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,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -614,7 +615,7 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo
|
|||
|
||||
let email_data = email::EmailTokenData::from_json(&twofactor.data)?;
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Email": email::obscure_email(&email_data.email),
|
||||
"email": email::obscure_email(&email_data.email),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -626,19 +627,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");
|
||||
|
|
|
@ -42,13 +42,13 @@ impl Attachment {
|
|||
|
||||
pub fn to_json(&self, host: &str) -> Value {
|
||||
json!({
|
||||
"Id": self.id,
|
||||
"Url": self.get_url(host),
|
||||
"FileName": self.file_name,
|
||||
"Size": self.file_size.to_string(),
|
||||
"SizeName": crate::util::get_display_size(self.file_size),
|
||||
"Key": self.akey,
|
||||
"Object": "attachment"
|
||||
"id": self.id,
|
||||
"url": self.get_url(host),
|
||||
"fileName": self.file_name,
|
||||
"size": self.file_size.to_string(),
|
||||
"sizeName": crate::util::get_display_size(self.file_size),
|
||||
"key": self.akey,
|
||||
"object": "attachment"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::util::LowerCase;
|
||||
use crate::CONFIG;
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use serde_json::Value;
|
||||
|
@ -81,7 +82,7 @@ impl Cipher {
|
|||
pub fn validate_notes(cipher_data: &[CipherData]) -> EmptyResult {
|
||||
let mut validation_errors = serde_json::Map::new();
|
||||
for (index, cipher) in cipher_data.iter().enumerate() {
|
||||
if let Some(note) = &cipher.Notes {
|
||||
if let Some(note) = &cipher.notes {
|
||||
if note.len() > 10_000 {
|
||||
validation_errors.insert(
|
||||
format!("Ciphers[{index}].Notes"),
|
||||
|
@ -135,10 +136,6 @@ impl Cipher {
|
|||
}
|
||||
}
|
||||
|
||||
let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
||||
let password_history_json =
|
||||
self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
||||
|
||||
// We don't need these values at all for Organizational syncs
|
||||
// Skip any other database calls if this is the case and just return false.
|
||||
let (read_only, hide_passwords) = if sync_type == CipherSyncType::User {
|
||||
|
@ -153,20 +150,34 @@ impl Cipher {
|
|||
(false, false)
|
||||
};
|
||||
|
||||
let fields_json = self
|
||||
.fields
|
||||
.as_ref()
|
||||
.and_then(|s| serde_json::from_str::<LowerCase<Value>>(s).ok())
|
||||
.unwrap_or_default()
|
||||
.data;
|
||||
let password_history_json = self
|
||||
.password_history
|
||||
.as_ref()
|
||||
.and_then(|s| serde_json::from_str::<LowerCase<Value>>(s).ok())
|
||||
.unwrap_or_default()
|
||||
.data;
|
||||
|
||||
// Get the type_data or a default to an empty json object '{}'.
|
||||
// If not passing an empty object, mobile clients will crash.
|
||||
let mut type_data_json: Value =
|
||||
serde_json::from_str(&self.data).unwrap_or_else(|_| Value::Object(serde_json::Map::new()));
|
||||
let mut type_data_json = serde_json::from_str::<LowerCase<Value>>(&self.data)
|
||||
.map(|d| d.data)
|
||||
.unwrap_or_else(|_| Value::Object(serde_json::Map::new()));
|
||||
|
||||
// NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream
|
||||
// Set the first element of the Uris array as Uri, this is needed several (mobile) clients.
|
||||
if self.atype == 1 {
|
||||
if type_data_json["Uris"].is_array() {
|
||||
let uri = type_data_json["Uris"][0]["Uri"].clone();
|
||||
type_data_json["Uri"] = uri;
|
||||
if type_data_json["uris"].is_array() {
|
||||
let uri = type_data_json["uris"][0]["uri"].clone();
|
||||
type_data_json["uri"] = uri;
|
||||
} else {
|
||||
// Upstream always has an Uri key/value
|
||||
type_data_json["Uri"] = Value::Null;
|
||||
type_data_json["uri"] = Value::Null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,10 +186,10 @@ impl Cipher {
|
|||
|
||||
// NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream
|
||||
// data_json should always contain the following keys with every atype
|
||||
data_json["Fields"] = fields_json.clone();
|
||||
data_json["Name"] = json!(self.name);
|
||||
data_json["Notes"] = json!(self.notes);
|
||||
data_json["PasswordHistory"] = password_history_json.clone();
|
||||
data_json["fields"] = fields_json.clone();
|
||||
data_json["name"] = json!(self.name);
|
||||
data_json["notes"] = json!(self.notes);
|
||||
data_json["passwordHistory"] = password_history_json.clone();
|
||||
|
||||
let collection_ids = if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
if let Some(cipher_collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {
|
||||
|
@ -198,48 +209,48 @@ impl Cipher {
|
|||
//
|
||||
// Ref: https://github.com/bitwarden/server/blob/master/src/Core/Models/Api/Response/CipherResponseModel.cs
|
||||
let mut json_object = json!({
|
||||
"Object": "cipherDetails",
|
||||
"Id": self.uuid,
|
||||
"Type": self.atype,
|
||||
"CreationDate": format_date(&self.created_at),
|
||||
"RevisionDate": format_date(&self.updated_at),
|
||||
"DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
|
||||
"Reprompt": self.reprompt.unwrap_or(RepromptType::None as i32),
|
||||
"OrganizationId": self.organization_uuid,
|
||||
"Key": self.key,
|
||||
"Attachments": attachments_json,
|
||||
"object": "cipherDetails",
|
||||
"id": self.uuid,
|
||||
"type": self.atype,
|
||||
"creationDate": format_date(&self.created_at),
|
||||
"revisionDate": format_date(&self.updated_at),
|
||||
"deletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
|
||||
"reprompt": self.reprompt.unwrap_or(RepromptType::None as i32),
|
||||
"organizationId": self.organization_uuid,
|
||||
"key": self.key,
|
||||
"attachments": attachments_json,
|
||||
// We have UseTotp set to true by default within the Organization model.
|
||||
// This variable together with UsersGetPremium is used to show or hide the TOTP counter.
|
||||
"OrganizationUseTotp": true,
|
||||
"organizationUseTotp": true,
|
||||
|
||||
// This field is specific to the cipherDetails type.
|
||||
"CollectionIds": collection_ids,
|
||||
"collectionIds": collection_ids,
|
||||
|
||||
"Name": self.name,
|
||||
"Notes": self.notes,
|
||||
"Fields": fields_json,
|
||||
"name": self.name,
|
||||
"notes": self.notes,
|
||||
"fields": fields_json,
|
||||
|
||||
"Data": data_json,
|
||||
"data": data_json,
|
||||
|
||||
"PasswordHistory": password_history_json,
|
||||
"passwordHistory": password_history_json,
|
||||
|
||||
// All Cipher types are included by default as null, but only the matching one will be populated
|
||||
"Login": null,
|
||||
"SecureNote": null,
|
||||
"Card": null,
|
||||
"Identity": null,
|
||||
"login": null,
|
||||
"secureNote": null,
|
||||
"card": null,
|
||||
"identity": null,
|
||||
});
|
||||
|
||||
// These values are only needed for user/default syncs
|
||||
// Not during an organizational sync like `get_org_details`
|
||||
// Skip adding these fields in that case
|
||||
if sync_type == CipherSyncType::User {
|
||||
json_object["FolderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
json_object["folderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string())
|
||||
} else {
|
||||
self.get_folder_uuid(user_uuid, conn).await
|
||||
});
|
||||
json_object["Favorite"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
json_object["favorite"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
|
||||
cipher_sync_data.cipher_favorites.contains(&self.uuid)
|
||||
} else {
|
||||
self.is_favorite(user_uuid, conn).await
|
||||
|
@ -247,15 +258,15 @@ impl Cipher {
|
|||
// These values are true by default, but can be false if the
|
||||
// cipher belongs to a collection or group where the org owner has enabled
|
||||
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
||||
json_object["Edit"] = json!(!read_only);
|
||||
json_object["ViewPassword"] = json!(!hide_passwords);
|
||||
json_object["edit"] = json!(!read_only);
|
||||
json_object["viewPassword"] = json!(!hide_passwords);
|
||||
}
|
||||
|
||||
let key = match self.atype {
|
||||
1 => "Login",
|
||||
2 => "SecureNote",
|
||||
3 => "Card",
|
||||
4 => "Identity",
|
||||
1 => "login",
|
||||
2 => "secureNote",
|
||||
3 => "card",
|
||||
4 => "identity",
|
||||
_ => panic!("Wrong type"),
|
||||
};
|
||||
|
||||
|
|
|
@ -49,11 +49,11 @@ impl Collection {
|
|||
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"ExternalId": self.external_id,
|
||||
"Id": self.uuid,
|
||||
"OrganizationId": self.org_uuid,
|
||||
"Name": self.name,
|
||||
"Object": "collection",
|
||||
"externalId": self.external_id,
|
||||
"id": self.uuid,
|
||||
"organizationId": self.org_uuid,
|
||||
"name": self.name,
|
||||
"object": "collection",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -97,9 +97,9 @@ impl Collection {
|
|||
};
|
||||
|
||||
let mut json_object = self.to_json();
|
||||
json_object["Object"] = json!("collectionDetails");
|
||||
json_object["ReadOnly"] = json!(read_only);
|
||||
json_object["HidePasswords"] = json!(hide_passwords);
|
||||
json_object["object"] = json!("collectionDetails");
|
||||
json_object["readOnly"] = json!(read_only);
|
||||
json_object["hidePasswords"] = json!(hide_passwords);
|
||||
json_object
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,11 +58,11 @@ impl EmergencyAccess {
|
|||
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Status": self.status,
|
||||
"Type": self.atype,
|
||||
"WaitTimeDays": self.wait_time_days,
|
||||
"Object": "emergencyAccess",
|
||||
"id": self.uuid,
|
||||
"status": self.status,
|
||||
"type": self.atype,
|
||||
"waitTimeDays": self.wait_time_days,
|
||||
"object": "emergencyAccess",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -70,14 +70,14 @@ impl EmergencyAccess {
|
|||
let grantor_user = User::find_by_uuid(&self.grantor_uuid, conn).await.expect("Grantor user not found.");
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Status": self.status,
|
||||
"Type": self.atype,
|
||||
"WaitTimeDays": self.wait_time_days,
|
||||
"GrantorId": grantor_user.uuid,
|
||||
"Email": grantor_user.email,
|
||||
"Name": grantor_user.name,
|
||||
"Object": "emergencyAccessGrantorDetails",
|
||||
"id": self.uuid,
|
||||
"status": self.status,
|
||||
"type": self.atype,
|
||||
"waitTimeDays": self.wait_time_days,
|
||||
"grantorId": grantor_user.uuid,
|
||||
"email": grantor_user.email,
|
||||
"name": grantor_user.name,
|
||||
"object": "emergencyAccessGrantorDetails",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -91,14 +91,14 @@ impl EmergencyAccess {
|
|||
};
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Status": self.status,
|
||||
"Type": self.atype,
|
||||
"WaitTimeDays": self.wait_time_days,
|
||||
"GranteeId": grantee_user.as_ref().map_or("", |u| &u.uuid),
|
||||
"Email": grantee_user.as_ref().map_or("", |u| &u.email),
|
||||
"Name": grantee_user.as_ref().map_or("", |u| &u.name),
|
||||
"Object": "emergencyAccessGranteeDetails",
|
||||
"id": self.uuid,
|
||||
"status": self.status,
|
||||
"type": self.atype,
|
||||
"waitTimeDays": self.wait_time_days,
|
||||
"granteeId": grantee_user.as_ref().map_or("", |u| &u.uuid),
|
||||
"email": grantee_user.as_ref().map_or("", |u| &u.email),
|
||||
"name": grantee_user.as_ref().map_or("", |u| &u.name),
|
||||
"object": "emergencyAccessGranteeDetails",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,10 +43,10 @@ impl Folder {
|
|||
use crate::util::format_date;
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"RevisionDate": format_date(&self.updated_at),
|
||||
"Name": self.name,
|
||||
"Object": "folder",
|
||||
"id": self.uuid,
|
||||
"revisionDate": format_date(&self.updated_at),
|
||||
"name": self.name,
|
||||
"object": "folder",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,14 +58,14 @@ impl Group {
|
|||
use crate::util::format_date;
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"OrganizationId": self.organizations_uuid,
|
||||
"Name": self.name,
|
||||
"AccessAll": self.access_all,
|
||||
"ExternalId": self.external_id,
|
||||
"CreationDate": format_date(&self.creation_date),
|
||||
"RevisionDate": format_date(&self.revision_date),
|
||||
"Object": "group"
|
||||
"id": self.uuid,
|
||||
"organizationId": self.organizations_uuid,
|
||||
"name": self.name,
|
||||
"accessAll": self.access_all,
|
||||
"externalId": self.external_id,
|
||||
"creationDate": format_date(&self.creation_date),
|
||||
"revisionDate": format_date(&self.revision_date),
|
||||
"object": "group"
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -75,21 +75,21 @@ impl Group {
|
|||
.iter()
|
||||
.map(|entry| {
|
||||
json!({
|
||||
"Id": entry.collections_uuid,
|
||||
"ReadOnly": entry.read_only,
|
||||
"HidePasswords": entry.hide_passwords
|
||||
"id": entry.collections_uuid,
|
||||
"readOnly": entry.read_only,
|
||||
"hidePasswords": entry.hide_passwords
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"OrganizationId": self.organizations_uuid,
|
||||
"Name": self.name,
|
||||
"AccessAll": self.access_all,
|
||||
"ExternalId": self.external_id,
|
||||
"Collections": collections_groups,
|
||||
"Object": "groupDetails"
|
||||
"id": self.uuid,
|
||||
"organizationId": self.organizations_uuid,
|
||||
"name": self.name,
|
||||
"accessAll": self.access_all,
|
||||
"externalId": self.external_id,
|
||||
"collections": collections_groups,
|
||||
"object": "groupDetails"
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ use serde_json::Value;
|
|||
use crate::api::EmptyResult;
|
||||
use crate::db::DbConn;
|
||||
use crate::error::MapResult;
|
||||
use crate::util::UpCase;
|
||||
|
||||
use super::{TwoFactor, UserOrgStatus, UserOrgType, UserOrganization};
|
||||
|
||||
|
@ -39,16 +38,18 @@ pub enum OrgPolicyType {
|
|||
|
||||
// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendOptionsPolicyData {
|
||||
pub DisableHideEmail: bool,
|
||||
#[serde(rename = "disableHideEmail", alias = "DisableHideEmail")]
|
||||
pub disable_hide_email: bool,
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResetPasswordDataModel {
|
||||
pub AutoEnrollEnabled: bool,
|
||||
#[serde(rename = "autoEnrollEnabled", alias = "AutoEnrollEnabled")]
|
||||
pub auto_enroll_enabled: bool,
|
||||
}
|
||||
|
||||
pub type OrgPolicyResult = Result<(), OrgPolicyErr>;
|
||||
|
@ -78,12 +79,12 @@ impl OrgPolicy {
|
|||
pub fn to_json(&self) -> Value {
|
||||
let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"OrganizationId": self.org_uuid,
|
||||
"Type": self.atype,
|
||||
"Data": data_json,
|
||||
"Enabled": self.enabled,
|
||||
"Object": "policy",
|
||||
"id": self.uuid,
|
||||
"organizationId": self.org_uuid,
|
||||
"type": self.atype,
|
||||
"data": data_json,
|
||||
"enabled": self.enabled,
|
||||
"object": "policy",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -307,9 +308,9 @@ impl OrgPolicy {
|
|||
|
||||
pub async fn org_is_reset_password_auto_enroll(org_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {
|
||||
Some(policy) => match serde_json::from_str::<UpCase<ResetPasswordDataModel>>(&policy.data) {
|
||||
Some(policy) => match serde_json::from_str::<ResetPasswordDataModel>(&policy.data) {
|
||||
Ok(opts) => {
|
||||
return policy.enabled && opts.data.AutoEnrollEnabled;
|
||||
return policy.enabled && opts.auto_enroll_enabled;
|
||||
}
|
||||
_ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data),
|
||||
},
|
||||
|
@ -327,9 +328,9 @@ impl OrgPolicy {
|
|||
{
|
||||
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
|
||||
if user.atype < UserOrgType::Admin {
|
||||
match serde_json::from_str::<UpCase<SendOptionsPolicyData>>(&policy.data) {
|
||||
match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
|
||||
Ok(opts) => {
|
||||
if opts.data.DisableHideEmail {
|
||||
if opts.disable_hide_email {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -153,39 +153,39 @@ impl Organization {
|
|||
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Identifier": null, // not supported by us
|
||||
"Name": self.name,
|
||||
"Seats": 10, // The value doesn't matter, we don't check server-side
|
||||
// "MaxAutoscaleSeats": null, // The value doesn't matter, we don't check server-side
|
||||
"MaxCollections": 10, // The value doesn't matter, we don't check server-side
|
||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
"Use2fa": true,
|
||||
"UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
||||
"UseEvents": CONFIG.org_events_enabled(),
|
||||
"UseGroups": CONFIG.org_groups_enabled(),
|
||||
"UseTotp": true,
|
||||
"UsePolicies": true,
|
||||
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||
"UseSso": false, // Not supported
|
||||
// "UseKeyConnector": false, // Not supported
|
||||
"SelfHost": true,
|
||||
"UseApi": true,
|
||||
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
|
||||
"UseResetPassword": CONFIG.mail_enabled(),
|
||||
"id": self.uuid,
|
||||
"identifier": null, // not supported by us
|
||||
"name": self.name,
|
||||
"seats": 10, // The value doesn't matter, we don't check server-side
|
||||
// "maxAutoscaleSeats": null, // The value doesn't matter, we don't check server-side
|
||||
"maxCollections": 10, // The value doesn't matter, we don't check server-side
|
||||
"maxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
"use2fa": true,
|
||||
"useDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
||||
"useEvents": CONFIG.org_events_enabled(),
|
||||
"useGroups": CONFIG.org_groups_enabled(),
|
||||
"useTotp": true,
|
||||
"usePolicies": true,
|
||||
// "useScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||
"useSso": false, // Not supported
|
||||
// "useKeyConnector": false, // Not supported
|
||||
"selfHost": true,
|
||||
"useApi": true,
|
||||
"hasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
|
||||
"useResetPassword": CONFIG.mail_enabled(),
|
||||
|
||||
"BusinessName": null,
|
||||
"BusinessAddress1": null,
|
||||
"BusinessAddress2": null,
|
||||
"BusinessAddress3": null,
|
||||
"BusinessCountry": null,
|
||||
"BusinessTaxNumber": null,
|
||||
"businessName": null,
|
||||
"businessAddress1": null,
|
||||
"businessAddress2": null,
|
||||
"businessAddress3": null,
|
||||
"businessCountry": null,
|
||||
"businessTaxNumber": null,
|
||||
|
||||
"BillingEmail": self.billing_email,
|
||||
"Plan": "TeamsAnnually",
|
||||
"PlanType": 5, // TeamsAnnually plan
|
||||
"UsersGetPremium": true,
|
||||
"Object": "organization",
|
||||
"billingEmail": self.billing_email,
|
||||
"plan": "TeamsAnnually",
|
||||
"planType": 5, // TeamsAnnually plan
|
||||
"usersGetPremium": true,
|
||||
"object": "organization",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -346,63 +346,63 @@ impl UserOrganization {
|
|||
|
||||
// https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
|
||||
json!({
|
||||
"Id": self.org_uuid,
|
||||
"Identifier": null, // Not supported
|
||||
"Name": org.name,
|
||||
"Seats": 10, // The value doesn't matter, we don't check server-side
|
||||
"MaxCollections": 10, // The value doesn't matter, we don't check server-side
|
||||
"UsersGetPremium": true,
|
||||
"Use2fa": true,
|
||||
"UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
||||
"UseEvents": CONFIG.org_events_enabled(),
|
||||
"UseGroups": CONFIG.org_groups_enabled(),
|
||||
"UseTotp": true,
|
||||
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||
"UsePolicies": true,
|
||||
"UseApi": true,
|
||||
"SelfHost": true,
|
||||
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
|
||||
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||
"UseResetPassword": CONFIG.mail_enabled(),
|
||||
"SsoBound": false, // Not supported
|
||||
"UseSso": false, // Not supported
|
||||
"ProviderId": null,
|
||||
"ProviderName": null,
|
||||
// "KeyConnectorEnabled": false,
|
||||
// "KeyConnectorUrl": null,
|
||||
"id": self.org_uuid,
|
||||
"identifier": null, // Not supported
|
||||
"name": org.name,
|
||||
"seats": 10, // The value doesn't matter, we don't check server-side
|
||||
"maxCollections": 10, // The value doesn't matter, we don't check server-side
|
||||
"usersGetPremium": true,
|
||||
"use2fa": true,
|
||||
"useDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
||||
"useEvents": CONFIG.org_events_enabled(),
|
||||
"useGroups": CONFIG.org_groups_enabled(),
|
||||
"useTotp": true,
|
||||
// "useScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||
"usePolicies": true,
|
||||
"useApi": true,
|
||||
"selfHost": true,
|
||||
"hasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
|
||||
"resetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||
"useResetPassword": CONFIG.mail_enabled(),
|
||||
"ssoBound": false, // Not supported
|
||||
"useSso": false, // Not supported
|
||||
"providerId": null,
|
||||
"providerName": null,
|
||||
// "keyConnectorEnabled": false,
|
||||
// "keyConnectorUrl": null,
|
||||
|
||||
// TODO: Add support for Custom User Roles
|
||||
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
|
||||
// "Permissions": {
|
||||
// "AccessEventLogs": false,
|
||||
// "AccessImportExport": false,
|
||||
// "AccessReports": false,
|
||||
// "ManageAllCollections": false,
|
||||
// "CreateNewCollections": false,
|
||||
// "EditAnyCollection": false,
|
||||
// "DeleteAnyCollection": false,
|
||||
// "ManageAssignedCollections": false,
|
||||
// "permissions": {
|
||||
// "accessEventLogs": false,
|
||||
// "accessImportExport": false,
|
||||
// "accessReports": false,
|
||||
// "manageAllCollections": false,
|
||||
// "createNewCollections": false,
|
||||
// "editAnyCollection": false,
|
||||
// "deleteAnyCollection": false,
|
||||
// "manageAssignedCollections": false,
|
||||
// "editAssignedCollections": false,
|
||||
// "deleteAssignedCollections": false,
|
||||
// "ManageCiphers": false,
|
||||
// "ManageGroups": false,
|
||||
// "ManagePolicies": false,
|
||||
// "ManageResetPassword": false,
|
||||
// "ManageSso": false, // Not supported
|
||||
// "ManageUsers": false,
|
||||
// "ManageScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||
// "manageCiphers": false,
|
||||
// "manageGroups": false,
|
||||
// "managePolicies": false,
|
||||
// "manageResetPassword": false,
|
||||
// "manageSso": false, // Not supported
|
||||
// "manageUsers": false,
|
||||
// "manageScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||
// },
|
||||
|
||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
"maxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
|
||||
// These are per user
|
||||
"UserId": self.user_uuid,
|
||||
"Key": self.akey,
|
||||
"Status": self.status,
|
||||
"Type": self.atype,
|
||||
"Enabled": true,
|
||||
"userId": self.user_uuid,
|
||||
"key": self.akey,
|
||||
"status": self.status,
|
||||
"type": self.atype,
|
||||
"enabled": true,
|
||||
|
||||
"Object": "profileOrganization",
|
||||
"object": "profileOrganization",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -438,9 +438,9 @@ impl UserOrganization {
|
|||
.iter()
|
||||
.map(|cu| {
|
||||
json!({
|
||||
"Id": cu.collection_uuid,
|
||||
"ReadOnly": cu.read_only,
|
||||
"HidePasswords": cu.hide_passwords,
|
||||
"id": cu.collection_uuid,
|
||||
"readOnly": cu.read_only,
|
||||
"hidePasswords": cu.hide_passwords,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
|
@ -449,29 +449,29 @@ impl UserOrganization {
|
|||
};
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"UserId": self.user_uuid,
|
||||
"Name": user.name,
|
||||
"Email": user.email,
|
||||
"ExternalId": self.external_id,
|
||||
"Groups": groups,
|
||||
"Collections": collections,
|
||||
"id": self.uuid,
|
||||
"userId": self.user_uuid,
|
||||
"name": user.name,
|
||||
"email": user.email,
|
||||
"externalId": self.external_id,
|
||||
"groups": groups,
|
||||
"collections": collections,
|
||||
|
||||
"Status": status,
|
||||
"Type": self.atype,
|
||||
"AccessAll": self.access_all,
|
||||
"TwoFactorEnabled": twofactor_enabled,
|
||||
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||
"status": status,
|
||||
"type": self.atype,
|
||||
"accessAll": self.access_all,
|
||||
"twoFactorEnabled": twofactor_enabled,
|
||||
"resetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||
|
||||
"Object": "organizationUserUserDetails",
|
||||
"object": "organizationUserUserDetails",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_user_access_restrictions(&self, col_user: &CollectionUser) -> Value {
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"ReadOnly": col_user.read_only,
|
||||
"HidePasswords": col_user.hide_passwords,
|
||||
"id": self.uuid,
|
||||
"readOnly": col_user.read_only,
|
||||
"hidePasswords": col_user.hide_passwords,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -485,9 +485,9 @@ impl UserOrganization {
|
|||
.iter()
|
||||
.map(|c| {
|
||||
json!({
|
||||
"Id": c.collection_uuid,
|
||||
"ReadOnly": c.read_only,
|
||||
"HidePasswords": c.hide_passwords,
|
||||
"id": c.collection_uuid,
|
||||
"readOnly": c.read_only,
|
||||
"hidePasswords": c.hide_passwords,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
|
@ -502,15 +502,15 @@ impl UserOrganization {
|
|||
};
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"UserId": self.user_uuid,
|
||||
"id": self.uuid,
|
||||
"userId": self.user_uuid,
|
||||
|
||||
"Status": status,
|
||||
"Type": self.atype,
|
||||
"AccessAll": self.access_all,
|
||||
"Collections": coll_uuids,
|
||||
"status": status,
|
||||
"type": self.atype,
|
||||
"accessAll": self.access_all,
|
||||
"collections": coll_uuids,
|
||||
|
||||
"Object": "organizationUserDetails",
|
||||
"object": "organizationUserDetails",
|
||||
})
|
||||
}
|
||||
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
|
||||
|
|
|
@ -125,26 +125,26 @@ impl Send {
|
|||
let data: Value = serde_json::from_str(&self.data).unwrap_or_default();
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"AccessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()),
|
||||
"Type": self.atype,
|
||||
"id": self.uuid,
|
||||
"accessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()),
|
||||
"type": self.atype,
|
||||
|
||||
"Name": self.name,
|
||||
"Notes": self.notes,
|
||||
"Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
||||
"File": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
||||
"name": self.name,
|
||||
"notes": self.notes,
|
||||
"text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
||||
"file": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
||||
|
||||
"Key": self.akey,
|
||||
"MaxAccessCount": self.max_access_count,
|
||||
"AccessCount": self.access_count,
|
||||
"Password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)),
|
||||
"Disabled": self.disabled,
|
||||
"HideEmail": self.hide_email,
|
||||
"key": self.akey,
|
||||
"maxAccessCount": self.max_access_count,
|
||||
"accessCount": self.access_count,
|
||||
"password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)),
|
||||
"disabled": self.disabled,
|
||||
"hideEmail": self.hide_email,
|
||||
|
||||
"RevisionDate": format_date(&self.revision_date),
|
||||
"ExpirationDate": self.expiration_date.as_ref().map(format_date),
|
||||
"DeletionDate": format_date(&self.deletion_date),
|
||||
"Object": "send",
|
||||
"revisionDate": format_date(&self.revision_date),
|
||||
"expirationDate": self.expiration_date.as_ref().map(format_date),
|
||||
"deletionDate": format_date(&self.deletion_date),
|
||||
"object": "send",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -154,16 +154,16 @@ impl Send {
|
|||
let data: Value = serde_json::from_str(&self.data).unwrap_or_default();
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Type": self.atype,
|
||||
"id": self.uuid,
|
||||
"type": self.atype,
|
||||
|
||||
"Name": self.name,
|
||||
"Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
||||
"File": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
||||
"name": self.name,
|
||||
"text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
||||
"file": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
||||
|
||||
"ExpirationDate": self.expiration_date.as_ref().map(format_date),
|
||||
"CreatorIdentifier": self.creator_identifier(conn).await,
|
||||
"Object": "send-access",
|
||||
"expirationDate": self.expiration_date.as_ref().map(format_date),
|
||||
"creatorIdentifier": self.creator_identifier(conn).await,
|
||||
"object": "send-access",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -290,25 +290,18 @@ impl Send {
|
|||
pub async fn size_by_user(user_uuid: &str, conn: &mut DbConn) -> Option<i64> {
|
||||
let sends = Self::find_by_user(user_uuid, conn).await;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(serde::Deserialize, Default)]
|
||||
#[derive(serde::Deserialize)]
|
||||
struct FileData {
|
||||
Size: Option<NumberOrString>,
|
||||
size: Option<NumberOrString>,
|
||||
#[serde(rename = "size", alias = "Size")]
|
||||
size: NumberOrString,
|
||||
}
|
||||
|
||||
let mut total: i64 = 0;
|
||||
for send in sends {
|
||||
if send.atype == SendType::File as i32 {
|
||||
let data: FileData = serde_json::from_str(&send.data).unwrap_or_default();
|
||||
|
||||
let size = match (data.size, data.Size) {
|
||||
(Some(s), _) => s.into_i64(),
|
||||
(_, Some(s)) => s.into_i64(),
|
||||
(None, None) => continue,
|
||||
};
|
||||
|
||||
if let Ok(size) = size {
|
||||
if let Ok(size) =
|
||||
serde_json::from_str::<FileData>(&send.data).map_err(Into::into).and_then(|d| d.size.into_i64())
|
||||
{
|
||||
total = total.checked_add(size)?;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -54,17 +54,17 @@ impl TwoFactor {
|
|||
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
"Key": "", // This key and value vary
|
||||
"Object": "twoFactorAuthenticator" // This value varies
|
||||
"enabled": self.enabled,
|
||||
"key": "", // This key and value vary
|
||||
"Oobject": "twoFactorAuthenticator" // This value varies
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_provider(&self) -> Value {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
"Type": self.atype,
|
||||
"Object": "twoFactorProvider"
|
||||
"enabled": self.enabled,
|
||||
"type": self.atype,
|
||||
"object": "twoFactorProvider"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -240,24 +240,24 @@ impl User {
|
|||
};
|
||||
|
||||
json!({
|
||||
"_Status": status as i32,
|
||||
"Id": self.uuid,
|
||||
"Name": self.name,
|
||||
"Email": self.email,
|
||||
"EmailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
|
||||
"Premium": true,
|
||||
"MasterPasswordHint": self.password_hint,
|
||||
"Culture": "en-US",
|
||||
"TwoFactorEnabled": twofactor_enabled,
|
||||
"Key": self.akey,
|
||||
"PrivateKey": self.private_key,
|
||||
"SecurityStamp": self.security_stamp,
|
||||
"Organizations": orgs_json,
|
||||
"Providers": [],
|
||||
"ProviderOrganizations": [],
|
||||
"ForcePasswordReset": false,
|
||||
"AvatarColor": self.avatar_color,
|
||||
"Object": "profile",
|
||||
"_status": status as i32,
|
||||
"id": self.uuid,
|
||||
"mame": self.name,
|
||||
"email": self.email,
|
||||
"emailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(),
|
||||
"premium": true,
|
||||
"masterPasswordHint": self.password_hint,
|
||||
"culture": "en-US",
|
||||
"twoFactorEnabled": twofactor_enabled,
|
||||
"key": self.akey,
|
||||
"privateKey": self.private_key,
|
||||
"securityStamp": self.security_stamp,
|
||||
"organizations": orgs_json,
|
||||
"providers": [],
|
||||
"providerOrganizations": [],
|
||||
"forcePasswordReset": false,
|
||||
"avatarColor": self.avatar_color,
|
||||
"object": "profile",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
18
src/error.rs
18
src/error.rs
|
@ -179,18 +179,18 @@ fn _serialize(e: &impl serde::Serialize, _msg: &str) -> String {
|
|||
|
||||
fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
|
||||
let json = json!({
|
||||
"Message": msg,
|
||||
"message": msg,
|
||||
"error": "",
|
||||
"error_description": "",
|
||||
"ValidationErrors": {"": [ msg ]},
|
||||
"ErrorModel": {
|
||||
"Message": msg,
|
||||
"Object": "error"
|
||||
"validationErrors": {"": [ msg ]},
|
||||
"errorModel": {
|
||||
"message": msg,
|
||||
"object": "error"
|
||||
},
|
||||
"ExceptionMessage": null,
|
||||
"ExceptionStackTrace": null,
|
||||
"InnerExceptionMessage": null,
|
||||
"Object": "error"
|
||||
"exceptionMessage": null,
|
||||
"exceptionStackTrace": null,
|
||||
"innerExceptionMessage": null,
|
||||
"object": "error"
|
||||
});
|
||||
_serialize(&json, "")
|
||||
}
|
||||
|
|
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
|
@ -29,7 +29,7 @@
|
|||
{{#if TwoFactorEnabled}}
|
||||
<span class="badge bg-success me-2" title="2FA is enabled">2FA</span>
|
||||
{{/if}}
|
||||
{{#case _Status 1}}
|
||||
{{#case _status 1}}
|
||||
<span class="badge bg-warning text-dark me-2" title="User is invited">Invited</span>
|
||||
{{/case}}
|
||||
{{#if EmailVerified}}
|
||||
|
@ -72,7 +72,7 @@
|
|||
{{else}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button><br>
|
||||
{{/if}}
|
||||
{{#case _Status 1}}
|
||||
{{#case _status 1}}
|
||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-resend-user-invite>Resend invite</button><br>
|
||||
{{/case}}
|
||||
</span>
|
||||
|
|
36
src/util.rs
36
src/util.rs
|
@ -525,25 +525,33 @@ use serde_json::{self, Value};
|
|||
pub type JsonMap = serde_json::Map<String, Value>;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct UpCase<T: DeserializeOwned> {
|
||||
#[serde(deserialize_with = "upcase_deserialize")]
|
||||
pub struct LowerCase<T: DeserializeOwned> {
|
||||
#[serde(deserialize_with = "lowercase_deserialize")]
|
||||
#[serde(flatten)]
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
impl Default for LowerCase<Value> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data: Value::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/serde-rs/serde/issues/586
|
||||
pub fn upcase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
pub fn lowercase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let d = deserializer.deserialize_any(UpCaseVisitor)?;
|
||||
let d = deserializer.deserialize_any(LowerCaseVisitor)?;
|
||||
T::deserialize(d).map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
struct UpCaseVisitor;
|
||||
struct LowerCaseVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for UpCaseVisitor {
|
||||
impl<'de> Visitor<'de> for LowerCaseVisitor {
|
||||
type Value = Value;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
@ -557,7 +565,7 @@ impl<'de> Visitor<'de> for UpCaseVisitor {
|
|||
let mut result_map = JsonMap::new();
|
||||
|
||||
while let Some((key, value)) = map.next_entry()? {
|
||||
result_map.insert(upcase_first(key), upcase_value(value));
|
||||
result_map.insert(lcase_first(key), lowercase_value(value));
|
||||
}
|
||||
|
||||
Ok(Value::Object(result_map))
|
||||
|
@ -570,20 +578,20 @@ impl<'de> Visitor<'de> for UpCaseVisitor {
|
|||
let mut result_seq = Vec::<Value>::new();
|
||||
|
||||
while let Some(value) = seq.next_element()? {
|
||||
result_seq.push(upcase_value(value));
|
||||
result_seq.push(lowercase_value(value));
|
||||
}
|
||||
|
||||
Ok(Value::Array(result_seq))
|
||||
}
|
||||
}
|
||||
|
||||
fn upcase_value(value: Value) -> Value {
|
||||
fn lowercase_value(value: Value) -> Value {
|
||||
if let Value::Object(map) = value {
|
||||
let mut new_value = Value::Object(serde_json::Map::new());
|
||||
|
||||
for (key, val) in map.into_iter() {
|
||||
let processed_key = _process_key(&key);
|
||||
new_value[processed_key] = upcase_value(val);
|
||||
new_value[processed_key] = lowercase_value(val);
|
||||
}
|
||||
new_value
|
||||
} else if let Value::Array(array) = value {
|
||||
|
@ -591,7 +599,7 @@ fn upcase_value(value: Value) -> Value {
|
|||
let mut new_value = Value::Array(vec![Value::Null; array.len()]);
|
||||
|
||||
for (index, val) in array.into_iter().enumerate() {
|
||||
new_value[index] = upcase_value(val);
|
||||
new_value[index] = lowercase_value(val);
|
||||
}
|
||||
new_value
|
||||
} else {
|
||||
|
@ -603,12 +611,12 @@ fn upcase_value(value: Value) -> Value {
|
|||
// This key is part of the Identity Cipher (Social Security Number)
|
||||
fn _process_key(key: &str) -> String {
|
||||
match key.to_lowercase().as_ref() {
|
||||
"ssn" => "SSN".into(),
|
||||
_ => self::upcase_first(key),
|
||||
"ssn" => "ssn".into(),
|
||||
_ => self::lcase_first(key),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum NumberOrString {
|
||||
Number(i64),
|
||||
|
|
|
@ -71,9 +71,9 @@ with urllib.request.urlopen(DOMAIN_LISTS_URL) as response:
|
|||
global_domains = []
|
||||
for name, domain_list in domain_lists.items():
|
||||
entry = OrderedDict()
|
||||
entry["Type"] = enums[name]
|
||||
entry["Domains"] = domain_list
|
||||
entry["Excluded"] = False
|
||||
entry["type"] = enums[name]
|
||||
entry["domains"] = domain_list
|
||||
entry["excluded"] = False
|
||||
global_domains.append(entry)
|
||||
|
||||
# Write out the global domains JSON file.
|
||||
|
|
Laden …
In neuem Issue referenzieren