Switch to Edition 2024, more clippy lints, and less macro calls (#7200)

* Update to Rust 2024 Edition

Updated to the Rust 2024 Edition and added and fixed several lint checks.
This is a large change which, because of the extra lints, added some possible fixes for issues.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Reorder and merge imports

Signed-off-by: BlackDex <black.dex@gmail.com>

* Remove "db_run!" macro calls where possible

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:
Mathijs van Veluw
2026-05-17 19:38:49 +02:00
committed by GitHub
parent 22f5e0496c
commit 1ba2c6a26c
61 changed files with 3352 additions and 3087 deletions
+64 -69
View File
@@ -2,40 +2,40 @@ use std::{env, sync::LazyLock};
use reqwest::Method;
use rocket::{
Catcher, Route,
form::Form,
http::{Cookie, CookieJar, MediaType, SameSite, Status},
request::{FromRequest, Outcome, Request},
response::{content::RawHtml as Html, Redirect},
response::{Redirect, content::RawHtml as Html},
serde::json::Json,
Catcher, Route,
};
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::{
CONFIG, VERSION,
api::{
ApiResult, EmptyResult, JsonResult, Notify,
core::{log_event, two_factor},
unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify,
unregister_push_device,
},
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure},
auth::{ClientIp, Secure, decode_admin, encode_jwt, generate_admin_claims},
config::ConfigBuilder,
db::{
backup_sqlite, get_sql_server_version,
ACTIVE_DB_TYPE, DbConn, DbConnType, backup_sqlite, get_sql_server_version,
models::{
Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId,
MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId,
},
DbConn, DbConnType, ACTIVE_DB_TYPE,
},
error::{Error, MapResult},
http_client::make_http_request,
mail,
sso::FAKE_SSO_IDENTIFIER,
util::{
container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size,
is_running_in_container, parse_experimental_client_feature_flags, FeatureFlagFilter, NumberOrString,
FeatureFlagFilter, NumberOrString, container_base_image, format_naive_datetime_local, get_active_web_release,
get_display_size, is_running_in_container, parse_experimental_client_feature_flags,
},
CONFIG, VERSION,
};
pub fn routes() -> Vec<Route> {
@@ -93,8 +93,7 @@ static DB_TYPE: LazyLock<&str> = LazyLock::new(|| match ACTIVE_DB_TYPE.get() {
});
#[cfg(sqlite)]
static CAN_BACKUP: LazyLock<bool> =
LazyLock::new(|| ACTIVE_DB_TYPE.get().map(|t| *t == DbConnType::Sqlite).unwrap_or(false));
static CAN_BACKUP: LazyLock<bool> = LazyLock::new(|| ACTIVE_DB_TYPE.get().is_some_and(|t| *t == DbConnType::Sqlite));
#[cfg(not(sqlite))]
static CAN_BACKUP: LazyLock<bool> = LazyLock::new(|| false);
@@ -200,13 +199,7 @@ fn post_admin_login(
}
// If the token is invalid, redirect to login page
if !_validate_token(&data.token) {
error!("Invalid admin token. IP: {}", ip.ip);
Err(AdminResponse::Unauthorized(render_admin_login(
Some("Invalid admin token, please try again."),
redirect.as_deref(),
)))
} else {
if validate_token(&data.token) {
// If the token received is valid, generate JWT and save it as a cookie
let claims = generate_admin_claims();
let jwt = encode_jwt(&claims);
@@ -224,10 +217,16 @@ fn post_admin_login(
} else {
Err(AdminResponse::Ok(render_admin_page()))
}
} else {
error!("Invalid admin token. IP: {}", ip.ip);
Err(AdminResponse::Unauthorized(render_admin_login(
Some("Invalid admin token, please try again."),
redirect.as_deref(),
)))
}
}
fn _validate_token(token: &str) -> bool {
fn validate_token(token: &str) -> bool {
match CONFIG.admin_token().as_ref() {
None => false,
Some(t) if t.starts_with("$argon2") => {
@@ -307,21 +306,14 @@ async fn get_user_or_404(user_id: &UserId, conn: &DbConn) -> ApiResult<User> {
#[post("/invite", format = "application/json", data = "<data>")]
async fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {
let data: InviteData = data.into_inner();
if User::find_by_mail(&data.email, &conn).await.is_some() {
err_code!("User already exists", Status::Conflict.code)
}
let mut user = User::new(&data.email, None);
async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult {
async fn generate_invite(user: &User, conn: &DbConn) -> EmptyResult {
if CONFIG.mail_enabled() {
let org_id: OrganizationId = if CONFIG.sso_enabled() {
FAKE_SSO_IDENTIFIER.into()
} else {
FAKE_ADMIN_UUID.into()
};
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
let member_id: MembershipId = FAKE_ADMIN_UUID.to_owned().into();
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
} else {
let invitation = Invitation::new(&user.email);
@@ -329,7 +321,14 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -
}
}
_generate_invite(&user, &conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
let data: InviteData = data.into_inner();
if User::find_by_mail(&data.email, &conn).await.is_some() {
err_code!("User already exists", Status::Conflict.code)
}
let mut user = User::new(&data.email, None);
generate_invite(&user, &conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
user.save(&conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;
Ok(Json(user.to_json(&conn).await))
@@ -386,7 +385,7 @@ async fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<Stri
None => json!("Never"),
};
usr["sso_identifier"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new()));
usr["sso_identifier"] = json!(sso_u.map_or(String::new(), |u| u.identifier.to_string()));
users_json.push(usr);
}
@@ -472,7 +471,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti
match unregister_push_device(device.push_uuid.as_ref()).await {
Ok(r) => r,
Err(e) => error!("Unable to unregister devices from Bitwarden server: {e}"),
};
}
}
}
@@ -528,7 +527,7 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, conn: DbConn) -
} else {
FAKE_ADMIN_UUID.into()
};
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
let member_id: MembershipId = FAKE_ADMIN_UUID.to_owned().into();
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
} else {
Ok(())
@@ -554,9 +553,10 @@ async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToke
err!("The specified user isn't member of the organization")
};
let new_type = match MembershipType::from_str(&data.user_type.into_string()) {
Some(new_type) => new_type as i32,
None => err!("Invalid type"),
let new_type = if let Some(new_type) = MembershipType::from_str(&data.user_type.into_string()) {
new_type as i32
} else {
err!("Invalid type")
};
if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner {
@@ -656,42 +656,40 @@ async fn get_release_info(has_http_access: bool) -> (String, String, String) {
.await
{
Ok(r) => r.tag_name,
_ => "-".to_string(),
_ => "-".to_owned(),
},
match get_json_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await {
Ok(mut c) => {
c.sha.truncate(8);
c.sha
}
_ => "-".to_string(),
_ => "-".to_owned(),
},
// Do not fetch the web-vault version when running within a container
// The web-vault version is embedded within the container it self, and should not be updated manually
match get_json_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest")
.await
{
Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
_ => "-".to_string(),
Ok(r) => r.tag_name.trim_start_matches('v').to_owned(),
_ => "-".to_owned(),
},
)
} else {
("-".to_string(), "-".to_string(), "-".to_string())
("-".to_owned(), "-".to_owned(), "-".to_owned())
}
}
async fn get_ntp_time(has_http_access: bool) -> String {
if has_http_access {
if let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await {
for line in cf_trace.lines() {
if let Some((key, value)) = line.split_once('=') {
if key == "ts" {
let ts = value.split_once('.').map_or(value, |(s, _)| s);
if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") {
return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string();
}
break;
}
if has_http_access && let Ok(cf_trace) = get_text_api("https://cloudflare.com/cdn-cgi/trace").await {
for line in cf_trace.lines() {
if let Some((key, value)) = line.split_once('=')
&& key == "ts"
{
let ts = value.split_once('.').map_or(value, |(s, _)| s);
if let Ok(dt) = chrono::DateTime::parse_from_str(ts, "%s") {
return dt.format("%Y-%m-%d %H:%M:%S UTC").to_string();
}
break;
}
}
}
@@ -734,7 +732,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
// Check if we are able to resolve DNS entries
let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
Ok(Some(a)) => a.ip().to_string(),
_ => "Unable to resolve domain name.".to_string(),
_ => "Unable to resolve domain name.".to_owned(),
};
let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await;
@@ -745,7 +743,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A
let invalid_feature_flags: Vec<String> = parse_experimental_client_feature_flags(
&CONFIG.experimental_client_feature_flags(),
FeatureFlagFilter::InvalidOnly,
&FeatureFlagFilter::InvalidOnly,
)
.into_keys()
.collect();
@@ -834,33 +832,30 @@ impl<'r> FromRequest<'r> for AdminToken {
type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let ip = match ClientIp::from_request(request).await {
Outcome::Success(ip) => ip,
_ => err_handler!("Error getting Client IP"),
let Outcome::Success(ip) = ClientIp::from_request(request).await else {
err_handler!("Error getting Client IP")
};
if !CONFIG.disable_admin_token() {
let cookies = request.cookies();
let access_token = match cookies.get(COOKIE_NAME) {
Some(cookie) => cookie.value(),
None => {
let requested_page =
request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();
// When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page
// Else, return a 401 failure, which will be caught
if requested_page.is_empty() {
return Outcome::Forward(Status::Unauthorized);
} else {
return Outcome::Error((Status::Unauthorized, "Unauthorized"));
}
let access_token = if let Some(cookie) = cookies.get(COOKIE_NAME) {
cookie.value()
} else {
let requested_page =
request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();
// When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page
// Else, return a 401 failure, which will be caught
if requested_page.is_empty() {
return Outcome::Forward(Status::Unauthorized);
}
return Outcome::Error((Status::Unauthorized, "Unauthorized"));
};
if decode_admin(access_token).is_err() {
// Remove admin cookie
cookies.remove(Cookie::build(COOKIE_NAME).path(admin_path()));
error!("Invalid or expired admin JWT. IP: {}.", &ip.ip);
error!("Invalid or expired admin JWT. IP: {}.", ip.ip);
return Outcome::Error((Status::Unauthorized, "Session expired"));
}
}
+84 -88
View File
@@ -1,34 +1,37 @@
use std::collections::HashSet;
use crate::db::DbPool;
use chrono::Utc;
use rocket::serde::json::Json;
use serde_json::Value;
use crate::{
api::{
core::{accept_org_invite, log_user_event, two_factor::email},
master_password_policy, register_push_device, unregister_push_device, AnonymousNotify, ApiResult, EmptyResult,
JsonResult, Notify, PasswordOrOtpData, UpdateType,
},
auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},
crypto,
db::{
models::{
AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, EmergencyAccess,
EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, OrgPolicy,
OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType,
},
DbConn,
},
mail,
util::{deser_opt_nonempty_str, format_date, NumberOrString},
CONFIG,
};
use rocket::{
http::Status,
request::{FromRequest, Outcome, Request},
serde::json::Json,
};
use serde_json::Value;
use crate::{
CONFIG,
api::{
AnonymousNotify, ApiResult, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
core::{accept_org_invite, log_user_event, two_factor::email},
master_password_policy, register_push_device, unregister_push_device,
},
auth::{ClientHeaders, Headers, decode_delete, decode_invite, decode_verify_email},
crypto,
db::{
DbConn, DbPool,
models::{
AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, DeviceWithAuthRequest,
EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId,
OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType,
},
},
mail,
util::{NumberOrString, deser_opt_nonempty_str, format_date},
};
use super::{
ciphers::{CipherData, update_cipher_from_data},
sends::{SendData, update_send_from_data},
};
pub fn routes() -> Vec<rocket::Route> {
@@ -54,9 +57,9 @@ pub fn routes() -> Vec<rocket::Route> {
delete_account,
revision_date,
password_hint,
prelogin,
post_prelogin,
verify_password,
api_key,
post_api_key,
rotate_api_key,
get_known_device,
get_all_devices,
@@ -142,7 +145,7 @@ fn clean_password_hint(password_hint: Option<&String>) -> Option<String> {
None => None,
Some(h) => match h.trim() {
"" => None,
ht => Some(ht.to_string()),
ht => Some(ht.to_owned()),
},
}
}
@@ -166,7 +169,7 @@ async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &DbConn) -
false
}
pub async fn _register(data: Json<RegisterData>, email_verification: bool, conn: DbConn) -> JsonResult {
pub async fn register(data: Json<RegisterData>, email_verification: bool, conn: DbConn) -> JsonResult {
let mut data: RegisterData = data.into_inner();
let email = data.email.to_lowercase();
@@ -237,10 +240,10 @@ pub async fn _register(data: Json<RegisterData>, email_verification: bool, conn:
// 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 name.len() > 50 {
err!("The field Name must be a string with a maximum length of 50.");
}
if let Some(ref name) = data.name
&& name.len() > 50
{
err!("The field Name must be a string with a maximum length of 50.");
}
// Check against the password hint setting here so if it fails, the user
@@ -373,18 +376,19 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
user.public_key = Some(keys.public_key);
}
if let Some(identifier) = data.org_identifier {
if identifier != crate::sso::FAKE_SSO_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID {
let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else {
err!("Failed to retrieve the associated organization")
};
if let Some(identifier) = data.org_identifier
&& identifier != crate::sso::FAKE_SSO_IDENTIFIER
&& identifier != crate::api::admin::FAKE_ADMIN_UUID
{
let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else {
err!("Failed to retrieve the associated organization")
};
let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await else {
err!("Failed to retrieve the invitation")
};
let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await else {
err!("Failed to retrieve the invitation")
};
accept_org_invite(&user, membership, None, &conn).await?;
}
accept_org_invite(&user, membership, None, &conn).await?;
}
if CONFIG.mail_enabled() {
@@ -451,10 +455,10 @@ async fn put_avatar(data: Json<AvatarData>, headers: Headers, conn: DbConn) -> J
// 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.avatar_color {
if color.len() != 7 {
err!("The field AvatarColor must be a HTML/Hex color code with a length of 7 characters")
}
if let Some(color) = &data.avatar_color
&& 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;
@@ -668,9 +672,6 @@ struct UpdateResetPasswordData {
reset_password_key: String,
}
use super::ciphers::CipherData;
use super::sends::{update_send_from_data, SendData};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct KeyData {
@@ -840,7 +841,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
};
saved_folder.name = folder_data.name;
saved_folder.save(&conn).await?
saved_folder.save(&conn).await?;
}
}
@@ -853,7 +854,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
};
saved_emergency_access.key_encrypted = Some(emergency_access_data.key_encrypted);
saved_emergency_access.save(&conn).await?
saved_emergency_access.save(&conn).await?;
}
// Update reset password data
@@ -865,7 +866,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
};
membership.reset_password_key = Some(reset_password_data.reset_password_key);
membership.save(&conn).await?
membership.save(&conn).await?;
}
// Update send data
@@ -878,8 +879,6 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
}
// Update cipher data
use super::ciphers::update_cipher_from_data;
for cipher_data in data.account_data.ciphers {
if cipher_data.organization_id.is_none() {
let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap())
@@ -890,7 +889,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
// We force the users to logout after the user has been saved to try and prevent these issues.
update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &conn, &nt, UpdateType::None).await?
update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &conn, &nt, UpdateType::None).await?;
}
}
@@ -1020,24 +1019,22 @@ async fn post_email(data: Json<ChangeEmailData>, headers: Headers, conn: DbConn,
err!("Email already in use");
}
match user.email_new {
Some(ref val) => {
if val != &data.new_email {
err!("Email change mismatch");
}
if let Some(ref val) = user.email_new {
if val != &data.new_email {
err!("Email change mismatch");
}
None => err!("No email change pending"),
} else {
err!("No email change pending")
}
if CONFIG.mail_enabled() {
// Only check the token if we sent out an email...
match user.email_new_token {
Some(ref val) => {
if *val != data.token.into_string() {
err!("Token mismatch");
}
if let Some(ref val) = user.email_new_token {
if *val != data.token.into_string() {
err!("Token mismatch");
}
None => err!("No email change pending"),
} else {
err!("No email change pending")
}
user.verified_at = Some(Utc::now().naive_utc());
} else {
@@ -1114,10 +1111,10 @@ async fn post_delete_recover(data: Json<DeleteRecoverData>, conn: DbConn) -> Emp
let data: DeleteRecoverData = data.into_inner();
if CONFIG.mail_enabled() {
if let Some(user) = User::find_by_mail(&data.email, &conn).await {
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await {
error!("Error sending delete account email: {e:#?}");
}
if let Some(user) = User::find_by_mail(&data.email, &conn).await
&& let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await
{
error!("Error sending delete account email: {e:#?}");
}
Ok(())
} else {
@@ -1169,6 +1166,7 @@ async fn delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: D
user.delete(&conn).await
}
#[expect(clippy::needless_pass_by_value, reason = "Not beneficial for Headers")]
#[get("/accounts/revision-date")]
fn revision_date(headers: Headers) -> JsonResult {
let revision_date = headers.user.updated_at.and_utc().timestamp_millis();
@@ -1183,12 +1181,12 @@ struct PasswordHintData {
#[post("/accounts/password-hint", data = "<data>")]
async fn password_hint(data: Json<PasswordHintData>, conn: DbConn) -> EmptyResult {
const NO_HINT: &str = "Sorry, you have no password hint...";
if !CONFIG.password_hints_allowed() || (!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();
let email = &data.email;
@@ -1199,9 +1197,9 @@ async fn password_hint(data: Json<PasswordHintData>, conn: DbConn) -> EmptyResul
// There is still a timing side channel here in that the code
// paths that send mail take noticeably longer than ones that
// don't. Add a randomized sleep to mitigate this somewhat.
use rand::{rngs::SmallRng, RngExt};
use rand::{RngExt, rngs::SmallRng};
let mut rng: SmallRng = rand::make_rng();
let sleep_ms = rng.random_range(900..=1100) as u64;
let sleep_ms: u64 = rng.random_range(900..=1100);
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
Ok(())
} else {
@@ -1229,11 +1227,11 @@ pub struct PreloginData {
}
#[post("/accounts/prelogin", data = "<data>")]
async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
_prelogin(data, conn).await
async fn post_prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
prelogin(data, conn).await
}
pub async fn _prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
pub async fn prelogin(data: Json<PreloginData>, 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, &conn).await {
@@ -1283,9 +1281,7 @@ async fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers
Ok(Json(master_password_policy(&user, &conn).await))
}
async fn _api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers, conn: DbConn) -> JsonResult {
use crate::util::format_date;
async fn update_api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner();
let mut user = headers.user;
@@ -1304,13 +1300,13 @@ async fn _api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers,
}
#[post("/accounts/api-key", data = "<data>")]
async fn api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
_api_key(data, false, headers, conn).await
async fn post_api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
update_api_key(data, false, headers, conn).await
}
#[post("/accounts/rotate-api-key", data = "<data>")]
async fn rotate_api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
_api_key(data, true, headers, conn).await
update_api_key(data, true, headers, conn).await
}
#[get("/devices/knowndevice")]
@@ -1353,7 +1349,7 @@ impl<'r> FromRequest<'r> for KnownDevice {
};
let uuid = if let Some(uuid) = req.headers().get_one("X-Device-Identifier") {
uuid.to_string().into()
uuid.to_owned().into()
} else {
return Outcome::Error((Status::BadRequest, "X-Device-Identifier value is required"));
};
@@ -1368,7 +1364,7 @@ impl<'r> FromRequest<'r> for KnownDevice {
#[get("/devices")]
async fn get_all_devices(headers: Headers, conn: DbConn) -> JsonResult {
let devices = Device::find_with_auth_request_by_user(&headers.user.uuid, &conn).await;
let devices = devices.iter().map(|device| device.to_json()).collect::<Vec<Value>>();
let devices = devices.iter().map(DeviceWithAuthRequest::to_json).collect::<Vec<Value>>();
Ok(Json(json!({
"data": devices,
@@ -1708,6 +1704,6 @@ pub async fn purge_auth_requests(pool: DbPool) {
if let Ok(conn) = pool.get().await {
AuthRequest::purge_expired_auth_requests(&conn).await;
} else {
error!("Failed to get DB connection while purging auth requests")
error!("Failed to get DB connection while purging auth requests");
}
}
+108 -114
View File
@@ -2,30 +2,30 @@ use std::collections::{HashMap, HashSet};
use chrono::{NaiveDateTime, Utc};
use num_traits::ToPrimitive;
use rocket::fs::TempFile;
use rocket::serde::json::Json;
use rocket::{
form::{Form, FromForm},
Route,
form::{Form, FromForm},
fs::TempFile,
serde::json::Json,
};
use serde_json::Value;
use crate::auth::ClientVersion;
use crate::util::{deser_opt_nonempty_str, save_temp_file, NumberOrString};
use crate::{
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
CONFIG,
api::{self, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, core::log_event},
auth::ClientVersion,
auth::{Headers, OrgIdGuard, OwnerHeaders},
config::PathType,
crypto,
db::{
DbConn, DbPool,
models::{
Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup,
CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership,
MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,
},
DbConn, DbPool,
},
CONFIG,
util::{NumberOrString, deser_opt_nonempty_str, save_temp_file},
};
use super::folders::FolderData;
@@ -108,7 +108,7 @@ pub async fn purge_trashed_ciphers(pool: DbPool) {
if let Ok(conn) = pool.get().await {
Cipher::purge_trash(&conn).await;
} else {
error!("Failed to get DB connection while purging trashed ciphers")
error!("Failed to get DB connection while purging trashed ciphers");
}
}
@@ -164,7 +164,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
let domains_json = if data.exclude_domains {
Value::Null
} else {
api::core::_get_eq_domains(&headers, true).into_inner()
api::core::get_eq_domains(&headers, true).into_inner()
};
// This is very similar to the the userDecryptionOptions sent in connect/token,
@@ -401,20 +401,34 @@ pub async fn update_cipher_from_data(
nt: &Notify<'_>,
ut: UpdateType,
) -> EmptyResult {
// Cleanup cipher data, like removing the 'Response' key.
// This key is somewhere generated during Javascript so no way for us this fix this.
// Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key.
// We do not mind which data is in it, the keep our model more flexible when there are upstream changes.
// But, we at least know we do not need to store and return this specific key.
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");
});
}
json_data
}
enforce_personal_ownership_policy(Some(&data), headers, conn).await?;
// 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.last_known_revision_date {
match NaiveDateTime::parse_from_str(&dt, "%+") {
// ISO 8601 format
Err(err) => warn!("Error parsing LastKnownRevisionDate '{dt}': {err}"),
Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => {
err!("The client copy of this cipher is out of date. Resync the client and try again.")
}
Ok(_) => (),
if ut != UpdateType::None
&& 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}"),
Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => {
err!("The client copy of this cipher is out of date. Resync the client and try again.")
}
Ok(_) => (),
}
}
@@ -456,25 +470,22 @@ pub async fn update_cipher_from_data(
cipher.user_uuid = Some(headers.user.uuid.clone());
}
if let Some(ref folder_id) = data.folder_id {
if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, conn).await.is_none() {
err!("Invalid folder", "Folder does not exist or belongs to another user");
}
if let Some(ref folder_id) = data.folder_id
&& Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, conn).await.is_none()
{
err!("Invalid folder", "Folder does not exist or belongs to another user");
}
// Modify attachments name and keys when rotating
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,
None => {
// Warn and continue here.
// A missing attachment means it was removed via an other client.
// Also the Desktop Client supports removing attachments and save an update afterwards.
// Bitwarden it self ignores these mismatches server side.
warn!("Attachment {id} doesn't exist");
continue;
}
let Some(mut saved_att) = Attachment::find_by_id(&id, conn).await else {
// Warn and continue here.
// A missing attachment means it was removed via an other client.
// Also the Desktop Client supports removing attachments and save an update afterwards.
// Bitwarden it self ignores these mismatches server side.
warn!("Attachment {id} doesn't exist");
continue;
};
if saved_att.cipher_uuid != cipher.uuid {
@@ -491,20 +502,6 @@ pub async fn update_cipher_from_data(
}
}
// Cleanup cipher data, like removing the 'Response' key.
// This key is somewhere generated during Javascript so no way for us this fix this.
// Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key.
// We do not mind which data is in it, the keep our model more flexible when there are upstream changes.
// But, we at least know we do not need to store and return this specific key.
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");
});
};
json_data
}
let type_data_opt = match data.r#type {
1 => data.login,
2 => data.secure_note,
@@ -514,23 +511,22 @@ pub async fn update_cipher_from_data(
_ => 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");
// Remove the 'Response' key from every Uri.
if data["uris"].is_array() {
data["uris"] = _clean_cipher_data(data["uris"].clone());
}
data
let type_data = if let Some(mut data) = type_data_opt {
// Remove the 'Response' key from the base object.
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());
}
None => err!("Data missing"),
data
} else {
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.fields = data.fields.map(|f| clean_cipher_data(f).to_string());
cipher.data = type_data.to_string();
cipher.password_history = data.password_history.map(|f| f.to_string());
cipher.reprompt = data.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32);
@@ -612,7 +608,7 @@ async fn post_ciphers_import(data: Json<ImportData>, headers: Headers, conn: DbC
let existing_folders: HashSet<Option<FolderId>> =
Folder::find_by_user(&headers.user.uuid, &conn).await.into_iter().map(|f| Some(f.uuid)).collect();
let mut folders: Vec<FolderId> = Vec::with_capacity(data.folders.len());
for folder in data.folders.into_iter() {
for folder in data.folders {
let folder_id = if existing_folders.contains(&folder.id) {
folder.id.unwrap()
} else {
@@ -737,10 +733,10 @@ async fn put_cipher_partial(
err!("Cipher does not exist", "Cipher is not accessible for the current user")
}
if let Some(ref folder_id) = data.folder_id {
if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() {
err!("Invalid folder", "Folder does not exist or belongs to another user");
}
if let Some(ref folder_id) = data.folder_id
&& Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none()
{
err!("Invalid folder", "Folder does not exist or belongs to another user");
}
// Move cipher
@@ -1004,7 +1000,7 @@ async fn put_cipher_share_selected(
err!("You must select at least one collection.")
}
for cipher in data.ciphers.iter() {
for cipher in &data.ciphers {
if cipher.id.is_none() {
err!("Request missing ids field")
}
@@ -1016,11 +1012,10 @@ async fn put_cipher_share_selected(
collection_ids: data.collection_ids.clone(),
};
match shared_cipher_data.cipher.id.take() {
Some(id) => {
share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt, Some(UpdateType::None)).await?
}
None => err!("Request missing ids field"),
if let Some(id) = shared_cipher_data.cipher.id.take() {
share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt, Some(UpdateType::None)).await?
} else {
err!("Request missing ids field")
};
}
@@ -1038,15 +1033,14 @@ async fn share_cipher_by_uuid(
nt: &Notify<'_>,
override_ut: Option<UpdateType>,
) -> JsonResult {
let mut cipher = match Cipher::find_by_uuid(cipher_id, conn).await {
Some(cipher) => {
if cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await {
cipher
} else {
err!("Cipher is not write accessible")
}
let mut cipher = if let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await {
if cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await {
cipher
} else {
err!("Cipher is not write accessible")
}
None => err!("Cipher doesn't exist"),
} else {
err!("Cipher doesn't exist")
};
let mut shared_to_collections = vec![];
@@ -1065,7 +1059,7 @@ async fn share_cipher_by_uuid(
}
}
}
};
}
// When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate.
// If there is an override, like when handling multiple items, we want to prevent a push notification for every single item
@@ -1263,10 +1257,10 @@ async fn save_attachment(
err!("Cipher is neither owned by a user nor an organization");
};
if let Some(size_limit) = size_limit {
if size > size_limit {
err!("Attachment storage limit exceeded with this file");
}
if let Some(size_limit) = size_limit
&& size > size_limit
{
err!("Attachment storage limit exceeded with this file");
}
let file_id = match &attachment {
@@ -1408,7 +1402,7 @@ async fn post_attachment_share(
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await?;
delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await?;
post_attachment(cipher_id, data, headers, conn, nt).await
}
@@ -1442,7 +1436,7 @@ async fn delete_attachment(
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await
delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await
}
#[delete("/ciphers/<cipher_id>/attachment/<attachment_id>/admin")]
@@ -1453,42 +1447,42 @@ async fn delete_attachment_admin(
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await
delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await
}
#[post("/ciphers/<cipher_id>/delete")]
async fn delete_cipher_post(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
// permanent delete
}
#[post("/ciphers/<cipher_id>/delete-admin")]
async fn delete_cipher_post_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
// permanent delete
}
#[put("/ciphers/<cipher_id>/delete")]
async fn delete_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await
delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await
// soft delete
}
#[put("/ciphers/<cipher_id>/delete-admin")]
async fn delete_cipher_put_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await
delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await
// soft delete
}
#[delete("/ciphers/<cipher_id>")]
async fn delete_cipher(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
// permanent delete
}
#[delete("/ciphers/<cipher_id>/admin")]
async fn delete_cipher_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
_delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await
// permanent delete
}
@@ -1499,7 +1493,7 @@ async fn delete_cipher_selected(
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await
delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await
// permanent delete
}
@@ -1510,7 +1504,7 @@ async fn delete_cipher_selected_post(
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await
delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await
// permanent delete
}
@@ -1521,7 +1515,7 @@ async fn delete_cipher_selected_put(
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await
delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await
// soft delete
}
@@ -1532,7 +1526,7 @@ async fn delete_cipher_selected_admin(
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await
delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await
// permanent delete
}
@@ -1543,7 +1537,7 @@ async fn delete_cipher_selected_post_admin(
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await
delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await
// permanent delete
}
@@ -1554,18 +1548,18 @@ async fn delete_cipher_selected_put_admin(
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await
delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await
// soft delete
}
#[put("/ciphers/<cipher_id>/restore")]
async fn restore_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
_restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await
restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await
}
#[put("/ciphers/<cipher_id>/restore-admin")]
async fn restore_cipher_put_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
_restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await
restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await
}
#[put("/ciphers/restore-admin", data = "<data>")]
@@ -1575,7 +1569,7 @@ async fn restore_cipher_selected_admin(
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_restore_multiple_ciphers(data, &headers, &conn, &nt).await
restore_multiple_ciphers(data, &headers, &conn, &nt).await
}
#[put("/ciphers/restore", data = "<data>")]
@@ -1585,7 +1579,7 @@ async fn restore_cipher_selected(
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
_restore_multiple_ciphers(data, &headers, &conn, &nt).await
restore_multiple_ciphers(data, &headers, &conn, &nt).await
}
#[derive(Deserialize)]
@@ -1606,10 +1600,10 @@ async fn move_cipher_selected(
let data = data.into_inner();
let user_id = &headers.user.uuid;
if let Some(ref folder_id) = data.folder_id {
if Folder::find_by_uuid_and_user(folder_id, user_id, &conn).await.is_none() {
err!("Invalid folder", "Folder does not exist or belongs to another user");
}
if let Some(ref folder_id) = data.folder_id
&& Folder::find_by_uuid_and_user(folder_id, user_id, &conn).await.is_none()
{
err!("Invalid folder", "Folder does not exist or belongs to another user");
}
let cipher_count = data.ids.len();
@@ -1773,7 +1767,7 @@ pub enum CipherDeleteOptions {
HardMulti,
}
async fn _delete_cipher_by_uuid(
async fn delete_cipher_by_uuid(
cipher_id: &CipherId,
headers: &Headers,
conn: &DbConn,
@@ -1839,7 +1833,7 @@ struct CipherIdsData {
ids: Vec<CipherId>,
}
async fn _delete_multiple_ciphers(
async fn delete_multiple_ciphers(
data: Json<CipherIdsData>,
headers: Headers,
conn: DbConn,
@@ -1849,9 +1843,9 @@ async fn _delete_multiple_ciphers(
let data = data.into_inner();
for cipher_id in data.ids {
if let error @ Err(_) = _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &delete_options, &nt).await {
if let error @ Err(_) = delete_cipher_by_uuid(&cipher_id, &headers, &conn, &delete_options, &nt).await {
return error;
};
}
}
// Multi delete actions do not send out a push for each cipher, we need to send a general sync here
@@ -1860,7 +1854,7 @@ async fn _delete_multiple_ciphers(
Ok(())
}
async fn _restore_cipher_by_uuid(
async fn restore_cipher_by_uuid(
cipher_id: &CipherId,
headers: &Headers,
multi_restore: bool,
@@ -1906,7 +1900,7 @@ async fn _restore_cipher_by_uuid(
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))
}
async fn _restore_multiple_ciphers(
async fn restore_multiple_ciphers(
data: Json<CipherIdsData>,
headers: &Headers,
conn: &DbConn,
@@ -1916,7 +1910,7 @@ async fn _restore_multiple_ciphers(
let mut ciphers: Vec<Value> = Vec::new();
for cipher_id in data.ids {
match _restore_cipher_by_uuid(&cipher_id, headers, true, conn, nt).await {
match restore_cipher_by_uuid(&cipher_id, headers, true, conn, nt).await {
Ok(json) => ciphers.push(json.into_inner()),
err => return err,
}
@@ -1932,7 +1926,7 @@ async fn _restore_multiple_ciphers(
})))
}
async fn _delete_cipher_attachment_by_id(
async fn delete_cipher_attachment_by_id(
cipher_id: &CipherId,
attachment_id: &AttachmentId,
headers: &Headers,
@@ -2206,11 +2200,11 @@ impl CipherSyncData {
};
Self {
cipher_archives,
cipher_attachments,
cipher_folders,
cipher_favorites,
cipher_collections,
cipher_archives,
members,
user_collections,
user_collections_groups,
+28 -24
View File
@@ -1,23 +1,23 @@
use chrono::{TimeDelta, Utc};
use rocket::{serde::json::Json, Route};
use rocket::{Route, serde::json::Json};
use serde_json::Value;
use crate::{
CONFIG,
api::{
core::{CipherSyncData, CipherSyncType},
EmptyResult, JsonResult,
core::{CipherSyncData, CipherSyncType},
},
auth::{decode_emergency_access_invite, Headers},
auth::{Headers, decode_emergency_access_invite},
db::{
DbConn, DbPool,
models::{
Cipher, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation,
Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,
},
DbConn, DbPool,
},
mail,
util::NumberOrString,
CONFIG,
};
pub fn routes() -> Vec<Route> {
@@ -55,7 +55,7 @@ async fn get_contacts(headers: Headers, conn: DbConn) -> Json<Value> {
let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());
for ea in emergency_access_list {
if let Some(grantee) = ea.to_json_grantee_details(&conn).await {
emergency_access_list_json.push(grantee)
emergency_access_list_json.push(grantee);
}
}
@@ -89,11 +89,14 @@ async fn get_grantees(headers: Headers, conn: DbConn) -> Json<Value> {
async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await {
Some(emergency_access) => Ok(Json(
if let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await
{
Ok(Json(
emergency_access.to_json_grantee_details(&conn).await.expect("Grantee user should exist but does not!"),
)),
None => err!("Emergency access not valid."),
))
} else {
err!("Emergency access not valid.")
}
}
@@ -136,9 +139,10 @@ async fn post_emergency_access(
err!("Emergency access not valid.")
};
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."),
let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) {
new_type as i32
} else {
err!("Invalid emergency access type.")
};
emergency_access.atype = new_type;
@@ -205,9 +209,10 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, co
let emergency_access_status = EmergencyAccessStatus::Invited as i32;
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."),
let new_type = if let Some(new_type) = EmergencyAccessType::from_str(&data.r#type.into_string()) {
new_type as i32
} else {
err!("Invalid emergency access type.")
};
let grantor_user = headers.user;
@@ -342,12 +347,11 @@ async fn accept_invite(
err!("Claim email does not match current users email")
}
let grantee_user = match User::find_by_mail(&claims.email, &conn).await {
Some(user) => {
Invitation::take(&claims.email, &conn).await;
user
}
None => err!("Invited user not found"),
let grantee_user = if let Some(user) = User::find_by_mail(&claims.email, &conn).await {
Invitation::take(&claims.email, &conn).await;
user
} else {
err!("Invited user not found")
};
// We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database.
@@ -766,7 +770,7 @@ pub async fn emergency_request_timeout_job(pool: DbPool) {
}
}
} else {
error!("Failed to get DB connection while searching emergency request timed out")
error!("Failed to get DB connection while searching emergency request timed out");
}
}
@@ -825,6 +829,6 @@ pub async fn emergency_notification_reminder_job(pool: DbPool) {
}
}
} else {
error!("Failed to get DB connection while searching emergency notification reminder")
error!("Failed to get DB connection while searching emergency notification reminder");
}
}
+54 -60
View File
@@ -1,18 +1,18 @@
use std::net::IpAddr;
use chrono::NaiveDateTime;
use rocket::{form::FromForm, serde::json::Json, Route};
use rocket::{Route, form::FromForm, serde::json::Json};
use serde_json::Value;
use crate::{
CONFIG,
api::{EmptyResult, JsonResult},
auth::{AdminHeaders, Headers},
db::{
models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId},
DbConn, DbPool,
models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId},
},
util::parse_date,
CONFIG,
};
/// ###############################################################################################################
@@ -38,9 +38,7 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0)
} else {
let events_json: Vec<Value> = if CONFIG.org_events_enabled() {
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
@@ -51,8 +49,10 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &conn)
.await
.iter()
.map(|e| e.to_json())
.map(Event::to_json)
.collect()
} else {
Vec::with_capacity(0)
};
Ok(Json(json!({
@@ -64,27 +64,21 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin
#[get("/ciphers/<cipher_id>/events?<data..>")]
async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, conn: DbConn) -> JsonResult {
// Return an empty vec when we org events are disabled.
// Return an empty vec when org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0)
} else {
let mut events_json = Vec::with_capacity(0);
if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await {
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
} else {
parse_date(&data.end)
};
let events_json: Vec<Value> = if CONFIG.org_events_enabled()
&& Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await
{
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
} else {
parse_date(&data.end)
};
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn)
.await
.iter()
.map(|e| e.to_json())
.collect()
}
events_json
Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn).await.iter().map(Event::to_json).collect()
} else {
Vec::with_capacity(0)
};
Ok(Json(json!({
@@ -107,9 +101,7 @@ async fn get_user_events(
}
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0)
} else {
let events_json: Vec<Value> = if CONFIG.org_events_enabled() {
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
@@ -120,8 +112,10 @@ async fn get_user_events(
Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &conn)
.await
.iter()
.map(|e| e.to_json())
.map(Event::to_json)
.collect()
} else {
Vec::with_capacity(0)
};
Ok(Json(json!({
@@ -134,7 +128,8 @@ async fn get_user_events(
fn get_continuation_token(events_json: &[Value]) -> Option<&str> {
// When the length of the vec equals the max page_size there probably is more data
// When it is less, then all events are loaded.
if events_json.len() as i64 == Event::PAGE_SIZE {
#[expect(clippy::cast_possible_truncation, reason = "PAGE_SIZE fits within usize")]
if events_json.len() == Event::PAGE_SIZE as usize {
if let Some(last_event) = events_json.last() {
last_event["date"].as_str()
} else {
@@ -176,7 +171,7 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
let event_date = parse_date(&event.date);
match event.r#type {
1000..=1099 => {
_log_user_event(
log_user_event_impl(
event.r#type,
&headers.user.uuid,
headers.device.atype,
@@ -188,7 +183,7 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
}
1600..=1699 => {
if let Some(org_id) = &event.organization_id {
_log_event(
log_event_impl(
event.r#type,
org_id,
org_id,
@@ -202,22 +197,21 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
}
}
_ => {
if let Some(cipher_uuid) = &event.cipher_id {
if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &conn).await {
if let Some(org_id) = cipher.organization_uuid {
_log_event(
event.r#type,
cipher_uuid,
&org_id,
&headers.user.uuid,
headers.device.atype,
Some(event_date),
&headers.ip.ip,
&conn,
)
.await;
}
}
if let Some(cipher_uuid) = &event.cipher_id
&& let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &conn).await
&& let Some(org_id) = cipher.organization_uuid
{
log_event_impl(
event.r#type,
cipher_uuid,
&org_id,
&headers.user.uuid,
headers.device.atype,
Some(event_date),
&headers.ip.ip,
&conn,
)
.await;
}
}
}
@@ -229,10 +223,10 @@ pub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32,
if !CONFIG.org_events_enabled() {
return;
}
_log_user_event(event_type, user_id, device_type, None, ip, conn).await;
log_user_event_impl(event_type, user_id, device_type, None, ip, conn).await;
}
async fn _log_user_event(
async fn log_user_event_impl(
event_type: i32,
user_id: &UserId,
device_type: i32,
@@ -278,11 +272,11 @@ pub async fn log_event(
if !CONFIG.org_events_enabled() {
return;
}
_log_event(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await;
log_event_impl(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await;
}
#[allow(clippy::too_many_arguments)]
async fn _log_event(
#[expect(clippy::too_many_arguments)]
async fn log_event_impl(
event_type: i32,
source_uuid: &str,
org_id: &OrganizationId,
@@ -298,24 +292,24 @@ async fn _log_event(
// 1000..=1099 Are user events, they need to be logged via log_user_event()
// Cipher Events
1100..=1199 => {
event.cipher_uuid = Some(source_uuid.to_string().into());
event.cipher_uuid = Some(source_uuid.to_owned().into());
}
// Collection Events
1300..=1399 => {
event.collection_uuid = Some(source_uuid.to_string().into());
event.collection_uuid = Some(source_uuid.to_owned().into());
}
// Group Events
1400..=1499 => {
event.group_uuid = Some(source_uuid.to_string().into());
event.group_uuid = Some(source_uuid.to_owned().into());
}
// Org User Events
1500..=1599 => {
event.org_user_uuid = Some(source_uuid.to_string().into());
event.org_user_uuid = Some(source_uuid.to_owned().into());
}
// 1600..=1699 Are organizational events, and they do not need the source_uuid
// Policy Events
1700..=1799 => {
event.policy_uuid = Some(source_uuid.to_string().into());
event.policy_uuid = Some(source_uuid.to_owned().into());
}
// Ignore others
_ => {}
@@ -338,6 +332,6 @@ pub async fn event_cleanup_job(pool: DbPool) {
if let Ok(conn) = pool.get().await {
Event::clean_events(&conn).await.ok();
} else {
error!("Failed to get DB connection while trying to cleanup the events table")
error!("Failed to get DB connection while trying to cleanup the events table");
}
}
+5 -4
View File
@@ -5,8 +5,8 @@ use crate::{
api::{EmptyResult, JsonResult, Notify, UpdateType},
auth::Headers,
db::{
models::{Folder, FolderId},
DbConn,
models::{Folder, FolderId},
},
util::deser_opt_nonempty_str,
};
@@ -29,9 +29,10 @@ async fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> {
#[get("/folders/<folder_id>")]
async fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> JsonResult {
match Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await {
Some(folder) => Ok(Json(folder.to_json())),
_ => err!("Invalid folder", "Folder does not exist or belongs to another user"),
if let Some(folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await {
Ok(Json(folder.to_json()))
} else {
err!("Invalid folder", "Folder does not exist or belongs to another user")
}
}
+48 -38
View File
@@ -1,4 +1,6 @@
pub mod accounts;
pub mod two_factor;
mod ciphers;
mod emergency_access;
mod events;
@@ -6,17 +8,32 @@ mod folders;
mod organizations;
mod public;
mod sends;
pub mod two_factor;
pub use accounts::purge_auth_requests;
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
pub use ciphers::{CipherData, CipherSyncData, CipherSyncType, purge_trashed_ciphers};
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
pub use events::{event_cleanup_job, log_event, log_user_event};
use reqwest::Method;
pub use sends::purge_sends;
use reqwest::Method;
use rocket::{Catcher, Route, serde::json::Json, serde::json::Value};
use crate::{
CONFIG,
api::{EmptyResult, JsonResult, Notify, UpdateType},
auth::Headers,
db::{
DbConn,
models::{Membership, MembershipStatus, OrgPolicy, Organization, User},
},
error::Error,
http_client::make_http_request,
mail,
util::{FeatureFlagFilter, parse_experimental_client_feature_flags},
};
pub fn routes() -> Vec<Route> {
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
let mut eq_domains_routes = routes![get_settings_domains, post_settings_domains, put_settings_domains];
let mut hibp_routes = routes![hibp_breach];
let mut meta_routes = routes![alive, now, version, config, get_api_webauthn];
@@ -44,25 +61,6 @@ pub fn events_routes() -> Vec<Route> {
routes
}
//
// Move this somewhere else
//
use rocket::{serde::json::Json, serde::json::Value, Catcher, Route};
use crate::{
api::{EmptyResult, JsonResult, Notify, UpdateType},
auth::Headers,
db::{
models::{Membership, MembershipStatus, OrgPolicy, Organization, User},
DbConn,
},
error::Error,
http_client::make_http_request,
mail,
util::{parse_experimental_client_feature_flags, FeatureFlagFilter},
CONFIG,
};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GlobalDomain {
@@ -73,15 +71,17 @@ struct GlobalDomain {
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
#[expect(clippy::needless_pass_by_value, reason = "Not beneficial for Headers")]
#[get("/settings/domains")]
fn get_eq_domains(headers: Headers) -> Json<Value> {
_get_eq_domains(&headers, false)
fn get_settings_domains(headers: Headers) -> Json<Value> {
get_eq_domains(&headers, false)
}
fn _get_eq_domains(headers: &Headers, no_excluded: bool) -> Json<Value> {
let user = &headers.user;
fn get_eq_domains(headers: &Headers, no_excluded: bool) -> Json<Value> {
use serde_json::from_str;
let user = &headers.user;
let equivalent_domains: Vec<Vec<String>> = from_str(&user.equivalent_domains).unwrap();
let excluded_globals: Vec<i32> = from_str(&user.excluded_globals).unwrap();
@@ -110,17 +110,23 @@ struct EquivDomainData {
}
#[post("/settings/domains", data = "<data>")]
async fn post_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
async fn post_settings_domains(
data: Json<EquivDomainData>,
headers: Headers,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
use serde_json::to_string;
let data: EquivDomainData = data.into_inner();
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;
user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string());
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string());
user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_owned());
user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_owned());
user.save(&conn).await?;
@@ -130,8 +136,13 @@ async fn post_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: Db
}
#[put("/settings/domains", data = "<data>")]
async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
post_eq_domains(data, headers, conn, nt).await
async fn put_settings_domains(
data: Json<EquivDomainData>,
headers: Headers,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
post_settings_domains(data, headers, conn, nt).await
}
#[get("/hibp/breach?<username>")]
@@ -206,9 +217,9 @@ fn config() -> Json<Value> {
// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
let mut feature_states = parse_experimental_client_feature_flags(
&CONFIG.experimental_client_feature_flags(),
FeatureFlagFilter::ValidOnly,
&FeatureFlagFilter::ValidOnly,
);
feature_states.insert("pm-19148-innovation-archive".to_string(), true);
feature_states.insert("pm-19148-innovation-archive".to_owned(), true);
Json(json!({
// Note: The clients use this version to handle backwards compatibility concerns
@@ -278,9 +289,8 @@ async fn accept_org_invite(
member.save(conn).await?;
if CONFIG.mail_enabled() {
let org = match Organization::find_by_uuid(&member.org_uuid, conn).await {
Some(org) => org,
None => err!("Organization not found."),
let Some(org) = Organization::find_by_uuid(&member.org_uuid, conn).await else {
err!("Organization not found.")
};
// User was invited to an organization, so they must be confirmed manually after acceptance
mail::send_invite_accepted(&user.email, &member.invited_by_email.unwrap_or(org.billing_email), &org.name)
+135 -133
View File
@@ -1,28 +1,28 @@
use num_traits::FromPrimitive;
use rocket::serde::json::Json;
use rocket::Route;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use crate::api::admin::FAKE_ADMIN_UUID;
use num_traits::FromPrimitive;
use rocket::{Route, serde::json::Json};
use serde_json::Value;
use crate::{
CONFIG,
api::admin::FAKE_ADMIN_UUID,
api::{
core::{accept_org_invite, log_event, two_factor, CipherSyncData, CipherSyncType},
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
core::{CipherSyncData, CipherSyncType, accept_org_invite, log_event, two_factor},
},
auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders},
auth::{AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders, decode_invite},
db::{
DbConn,
models::{
Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType,
Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType,
OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, User, UserId,
},
DbConn,
},
mail,
sso::FAKE_SSO_IDENTIFIER,
util::{convert_json_key_lcase_first, NumberOrString},
CONFIG,
util::{NumberOrString, convert_json_key_lcase_first},
};
pub fn routes() -> Vec<Route> {
@@ -97,7 +97,7 @@ pub fn routes() -> Vec<Route> {
get_reset_password_details,
put_reset_password,
get_org_export,
api_key,
post_api_key,
rotate_api_key,
get_billing_metadata,
get_billing_warnings,
@@ -286,9 +286,10 @@ async fn get_organization(org_id: OrganizationId, headers: OwnerHeaders, conn: D
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
match Organization::find_by_uuid(&org_id, &conn).await {
Some(organization) => Ok(Json(organization.to_json())),
None => err!("Can't find organization details"),
if let Some(organization) = Organization::find_by_uuid(&org_id, &conn).await {
Ok(Json(organization.to_json()))
} else {
err!("Can't find organization details")
}
}
@@ -367,7 +368,7 @@ async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn
};
let (id, identifier, rp_auto_enroll) = match org {
None => (identifier.to_string(), identifier.to_string(), false),
None => (identifier.to_owned(), identifier.to_owned(), false),
Some(org) => (
org.uuid.to_string(),
org.uuid.to_string(),
@@ -393,7 +394,7 @@ async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoos
}
Ok(Json(json!({
"data": _get_org_collections(&org_id, &conn).await,
"data": get_org_collections_impl(&org_id, &conn).await,
"object": "list",
"continuationToken": null,
})))
@@ -465,7 +466,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea
CollectionGroup::find_by_collection(&col.uuid, &conn)
.await
.iter()
.map(|collection_group| collection_group.to_json_details_for_group())
.map(CollectionGroup::to_json_details_for_group)
.collect()
} else {
Vec::with_capacity(0)
@@ -477,7 +478,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea
json_object["groups"] = json!(groups);
json_object["object"] = json!("collectionAccessDetails");
json_object["unmanaged"] = json!(false);
data.push(json_object)
data.push(json_object);
}
Ok(Json(json!({
@@ -487,7 +488,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea
})))
}
async fn _get_org_collections(org_id: &OrganizationId, conn: &DbConn) -> Value {
async fn get_org_collections_impl(org_id: &OrganizationId, conn: &DbConn) -> Value {
Collection::find_by_organization(org_id, conn).await.iter().map(Collection::to_json).collect::<Value>()
}
@@ -573,7 +574,7 @@ async fn post_bulk_access_collections(
if Organization::find_by_uuid(&org_id, &conn).await.is_none() {
err!("Can't find organization details")
};
}
for col_id in data.collection_ids {
let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else {
@@ -650,7 +651,7 @@ async fn post_organization_collection_update(
if Organization::find_by_uuid(&org_id, &conn).await.is_none() {
err!("Can't find organization details")
};
}
let Some(mut collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else {
err!("Collection not found")
@@ -701,7 +702,7 @@ async fn post_organization_collection_update(
Ok(Json(collection.to_json_details(&headers.user.uuid, None, &conn).await))
}
async fn _delete_organization_collection(
async fn delete_organization_collection_impl(
org_id: &OrganizationId,
col_id: &CollectionId,
headers: &ManagerHeaders,
@@ -733,7 +734,7 @@ async fn delete_organization_collection(
headers: ManagerHeaders,
conn: DbConn,
) -> EmptyResult {
_delete_organization_collection(&org_id, &col_id, &headers, &conn).await
delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await
}
#[post("/organizations/<org_id>/collections/<col_id>/delete")]
@@ -743,7 +744,7 @@ async fn post_organization_collection_delete(
headers: ManagerHeaders,
conn: DbConn,
) -> EmptyResult {
_delete_organization_collection(&org_id, &col_id, &headers, &conn).await
delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await
}
#[derive(Deserialize, Debug)]
@@ -769,7 +770,7 @@ async fn bulk_delete_organization_collections(
let headers = ManagerHeaders::from_loose(headers, &collections, &conn).await?;
for col_id in collections {
_delete_organization_collection(&org_id, &col_id, &headers, &conn).await?
delete_organization_collection_impl(&org_id, &col_id, &headers, &conn).await?;
}
Ok(())
}
@@ -799,7 +800,7 @@ async fn get_org_collection_detail(
CollectionGroup::find_by_collection(&collection.uuid, &conn)
.await
.iter()
.map(|collection_group| collection_group.to_json_details_for_group())
.map(CollectionGroup::to_json_details_for_group)
.collect()
} else {
// The Bitwarden clients seem to call this API regardless of whether groups are enabled,
@@ -886,13 +887,13 @@ async fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: Db
}
Ok(Json(json!({
"data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?,
"data": get_org_details_impl(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?,
"object": "list",
"continuationToken": null,
})))
}
async fn _get_org_details(
async fn get_org_details_impl(
org_id: &OrganizationId,
host: &str,
user_id: &UserId,
@@ -975,14 +976,13 @@ async fn post_org_keys(
}
let data: OrgKeyData = data.into_inner();
let mut org = match Organization::find_by_uuid(&org_id, &conn).await {
Some(organization) => {
if organization.private_key.is_some() && organization.public_key.is_some() {
err!("Organization Keys already exist")
}
organization
let mut org = if let Some(organization) = Organization::find_by_uuid(&org_id, &conn).await {
if organization.private_key.is_some() && organization.public_key.is_some() {
err!("Organization Keys already exist")
}
None => err!("Can't find organization details"),
organization
} else {
err!("Can't find organization details")
};
org.private_key = Some(data.encrypted_private_key);
@@ -1043,9 +1043,10 @@ async fn send_invite(
// The from_str() will convert the custom role type into a manager role type
let raw_type = &data.r#type.into_string();
// Membership::from_str will convert custom (4) to manager (3)
let new_type = match MembershipType::from_str(raw_type) {
Some(new_type) => new_type as i32,
None => err!("Invalid type"),
let new_type = if let Some(new_type) = MembershipType::from_str(raw_type) {
new_type as i32
} else {
err!("Invalid type")
};
if new_type != MembershipType::User && headers.membership_type != MembershipType::Owner {
@@ -1062,7 +1063,7 @@ async fn send_invite(
&& data.permissions.get("createNewCollections") == Some(&json!(true)));
let mut user_created: bool = false;
for email in data.emails.iter() {
for email in &data.emails {
let mut member_status = MembershipStatus::Invited as i32;
let user = match User::find_by_mail(email, &conn).await {
None => {
@@ -1086,13 +1087,13 @@ async fn send_invite(
Some(user) => {
if Membership::find_by_user_and_org(&user.uuid, &org_id, &conn).await.is_some() {
err!(format!("User already in organization: {email}"))
} else {
// automatically accept existing users if mail is disabled
if !CONFIG.mail_enabled() && !user.password_hash.is_empty() {
member_status = MembershipStatus::Accepted as i32;
}
user
}
// automatically accept existing users if mail is disabled
if !CONFIG.mail_enabled() && !user.password_hash.is_empty() {
member_status = MembershipStatus::Accepted as i32;
}
user
}
};
@@ -1103,9 +1104,10 @@ async fn send_invite(
new_member.save(&conn).await?;
if CONFIG.mail_enabled() {
let org_name = match Organization::find_by_uuid(&org_id, &conn).await {
Some(org) => org.name,
None => err!("Error looking up organization"),
let org_name = if let Some(org) = Organization::find_by_uuid(&org_id, &conn).await {
org.name
} else {
err!("Error looking up organization")
};
if let Err(e) = mail::send_invite(
@@ -1159,7 +1161,7 @@ async fn send_invite(
}
}
for group_id in data.groups.iter() {
for group_id in &data.groups {
let mut group_entry = GroupUser::new(group_id.clone(), new_member.uuid.clone());
group_entry.save(&conn).await?;
}
@@ -1182,8 +1184,8 @@ async fn bulk_reinvite_members(
let mut bulk_response = Vec::new();
for member_id in data.ids {
let err_msg = match _reinvite_member(&org_id, &member_id, &headers.user.email, &conn).await {
Ok(_) => String::new(),
let err_msg = match reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn).await {
Ok(()) => String::new(),
Err(e) => format!("{e:?}"),
};
@@ -1193,7 +1195,7 @@ async fn bulk_reinvite_members(
"id": member_id,
"error": err_msg
}
))
));
}
Ok(Json(json!({
@@ -1213,10 +1215,10 @@ async fn reinvite_member(
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
_reinvite_member(&org_id, &member_id, &headers.user.email, &conn).await
reinvite_member_impl(&org_id, &member_id, &headers.user.email, &conn).await
}
async fn _reinvite_member(
async fn reinvite_member_impl(
org_id: &OrganizationId,
member_id: &MembershipId,
invited_by_email: &str,
@@ -1238,13 +1240,14 @@ async fn _reinvite_member(
err!("Invitations are not allowed.")
}
let org_name = match Organization::find_by_uuid(org_id, conn).await {
Some(org) => org.name,
None => err!("Error looking up organization."),
let org_name = if let Some(org) = Organization::find_by_uuid(org_id, conn).await {
org.name
} else {
err!("Error looking up organization.")
};
if CONFIG.mail_enabled() {
mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_string())).await?;
mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_owned())).await?;
} else if user.password_hash.is_empty() {
let invitation = Invitation::new(&user.email);
invitation.save(conn).await?;
@@ -1352,8 +1355,8 @@ async fn bulk_confirm_invite(
for invite in keys {
let member_id = invite.id.unwrap();
let user_key = invite.key.unwrap_or_default();
let err_msg = match _confirm_invite(&org_id, &member_id, &user_key, &headers, &conn, &nt).await {
Ok(_) => String::new(),
let err_msg = match confirm_invite_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await {
Ok(()) => String::new(),
Err(e) => format!("{e:?}"),
};
@@ -1387,10 +1390,10 @@ async fn confirm_invite(
) -> EmptyResult {
let data = data.into_inner();
let user_key = data.key.unwrap_or_default();
_confirm_invite(&org_id, &member_id, &user_key, &headers, &conn, &nt).await
confirm_invite_impl(&org_id, &member_id, &user_key, &headers, &conn, &nt).await
}
async fn _confirm_invite(
async fn confirm_invite_impl(
org_id: &OrganizationId,
member_id: &MembershipId,
key: &str,
@@ -1418,7 +1421,7 @@ async fn _confirm_invite(
}
member_to_confirm.status = MembershipStatus::Confirmed as i32;
member_to_confirm.akey = key.to_string();
member_to_confirm.akey = key.to_owned();
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
OrgPolicy::check_user_allowed(&member_to_confirm, "confirm", conn).await?;
@@ -1435,13 +1438,15 @@ async fn _confirm_invite(
.await;
if CONFIG.mail_enabled() {
let org_name = match Organization::find_by_uuid(org_id, conn).await {
Some(org) => org.name,
None => err!("Error looking up organization."),
let org_name = if let Some(org) = Organization::find_by_uuid(org_id, conn).await {
org.name
} else {
err!("Error looking up organization.")
};
let address = match User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {
Some(user) => user.email,
None => err!("Error looking up user."),
let address = if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {
user.email
} else {
err!("Error looking up user.")
};
mail::send_invite_confirmed(&address, &org_name).await?;
}
@@ -1637,8 +1642,8 @@ async fn bulk_delete_member(
let mut bulk_response = Vec::new();
for member_id in data.ids {
let err_msg = match _delete_member(&org_id, &member_id, &headers, &conn, &nt).await {
Ok(_) => String::new(),
let err_msg = match delete_member_impl(&org_id, &member_id, &headers, &conn, &nt).await {
Ok(()) => String::new(),
Err(e) => format!("{e:?}"),
};
@@ -1648,7 +1653,7 @@ async fn bulk_delete_member(
"id": member_id,
"error": err_msg
}
))
));
}
Ok(Json(json!({
@@ -1666,10 +1671,10 @@ async fn delete_member(
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
_delete_member(&org_id, &member_id, &headers, &conn, &nt).await
delete_member_impl(&org_id, &member_id, &headers, &conn, &nt).await
}
async fn _delete_member(
async fn delete_member_impl(
org_id: &OrganizationId,
member_id: &MembershipId,
headers: &AdminHeaders,
@@ -1753,8 +1758,8 @@ async fn bulk_public_keys(
})))
}
use super::ciphers::update_cipher_from_data;
use super::ciphers::CipherData;
use super::ciphers::update_cipher_from_data;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -1902,24 +1907,24 @@ async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers
}
}
for cipher_id in data.cipher_ids.iter() {
for cipher_id in &data.cipher_ids {
// Only act on existing cipher uuid's
// Do not abort the operation just ignore it, it could be a cipher was just deleted for example
if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &conn).await {
if cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {
// When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection
// In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting.
if data.remove_collections {
for collection in &data.collection_ids {
CollectionCipher::delete(&cipher.uuid, collection, &conn).await?;
}
} else {
for collection in &data.collection_ids {
CollectionCipher::save(&cipher.uuid, collection, &conn).await?;
}
if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &conn).await
&& cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await
{
// When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection
// In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting.
if data.remove_collections {
for collection in &data.collection_ids {
CollectionCipher::delete(&cipher.uuid, collection, &conn).await?;
}
} else {
for collection in &data.collection_ids {
CollectionCipher::save(&cipher.uuid, collection, &conn).await?;
}
}
};
}
}
Ok(())
@@ -1969,7 +1974,7 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn)
fn get_dummy_master_password_policy() -> JsonResult {
let (enabled, data) = match CONFIG.sso_master_password_policy_value() {
Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()),
_ => (false, "null".to_string()),
_ => (false, "null".to_owned()),
};
let policy = OrgPolicy::new(FAKE_SSO_IDENTIFIER.into(), OrgPolicyType::MasterPassword, enabled, data);
Ok(Json(policy.to_json()))
@@ -1982,7 +1987,7 @@ async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberH
OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| {
let (enabled, data) = match CONFIG.sso_master_password_policy_value() {
Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()),
_ => (false, "null".to_string()),
_ => (false, "null".to_owned()),
};
OrgPolicy::new(org_id, OrgPolicyType::MasterPassword, enabled, data)
@@ -2003,7 +2008,7 @@ async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders
let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {
Some(p) => p,
None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "null".to_string()),
None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "null".to_owned()),
};
Ok(Json(policy.to_json()))
@@ -2078,7 +2083,7 @@ async fn put_policy(
// When enabling the SingleOrg policy, remove this org's members that are members of other orgs
if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled {
for mut member in Membership::find_by_org(&org_id, &conn).await.into_iter() {
for mut member in Membership::find_by_org(&org_id, &conn).await {
// Policy only applies to non-Owner/non-Admin members who have accepted joining the org
// Exclude invited and revoked users when checking for this policy.
// Those users will not be allowed to accept or be activated because of the policy checks done there.
@@ -2113,7 +2118,7 @@ async fn put_policy(
let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {
Some(p) => p,
None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_string()),
None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_owned()),
};
policy.enabled = data.enabled;
@@ -2187,7 +2192,7 @@ fn get_plans() -> Json<Value> {
#[get("/organizations/<_org_id>/billing/metadata")]
fn get_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json<Value> {
// Prevent a 404 error, which also causes Javascript errors.
Json(_empty_data_json())
Json(empty_data_json())
}
#[get("/organizations/<_org_id>/billing/vnext/warnings")]
@@ -2209,7 +2214,7 @@ fn get_self_host_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHe
}))
}
fn _empty_data_json() -> Value {
fn empty_data_json() -> Value {
json!({
"object": "list",
"data": [],
@@ -2230,7 +2235,7 @@ async fn revoke_member(
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
_revoke_member(&org_id, &member_id, &headers, &conn).await
revoke_member_impl(&org_id, &member_id, &headers, &conn).await
}
#[put("/organizations/<org_id>/users/revoke", data = "<data>")]
@@ -2249,8 +2254,8 @@ async fn bulk_revoke_members(
match data.ids {
Some(members) => {
for member_id in members {
let err_msg = match _revoke_member(&org_id, &member_id, &headers, &conn).await {
Ok(_) => String::new(),
let err_msg = match revoke_member_impl(&org_id, &member_id, &headers, &conn).await {
Ok(()) => String::new(),
Err(e) => format!("{e:?}"),
};
@@ -2273,7 +2278,7 @@ async fn bulk_revoke_members(
})))
}
async fn _revoke_member(
async fn revoke_member_impl(
org_id: &OrganizationId,
member_id: &MembershipId,
headers: &AdminHeaders,
@@ -2325,7 +2330,7 @@ async fn restore_member_vnext(
) -> EmptyResult {
// Vaultwarden does not (yet) support the per User Collection linked to the `Enforce organization data ownership` policy.
// Therefor we ignore the `defaultUserCollectionName` data sent and just call restore_member
_restore_member(&org_id, &member_id, &headers, &conn).await
restore_member_impl(&org_id, &member_id, &headers, &conn).await
}
#[put("/organizations/<org_id>/users/<member_id>/restore")]
@@ -2335,7 +2340,7 @@ async fn restore_member(
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
_restore_member(&org_id, &member_id, &headers, &conn).await
restore_member_impl(&org_id, &member_id, &headers, &conn).await
}
#[put("/organizations/<org_id>/users/restore", data = "<data>")]
@@ -2352,8 +2357,8 @@ async fn bulk_restore_members(
let mut bulk_response = Vec::new();
for member_id in data.ids {
let err_msg = match _restore_member(&org_id, &member_id, &headers, &conn).await {
Ok(_) => String::new(),
let err_msg = match restore_member_impl(&org_id, &member_id, &headers, &conn).await {
Ok(()) => String::new(),
Err(e) => format!("{e:?}"),
};
@@ -2373,7 +2378,7 @@ async fn bulk_restore_members(
})))
}
async fn _restore_member(
async fn restore_member_impl(
org_id: &OrganizationId,
member_id: &MembershipId,
headers: &AdminHeaders,
@@ -2429,11 +2434,11 @@ async fn get_groups_data(
if details {
for g in groups {
groups_json.push(g.to_json_details(&conn).await)
groups_json.push(g.to_json_details(&conn).await);
}
} else {
for g in groups {
groups_json.push(g.to_json())
groups_json.push(g.to_json());
}
}
groups_json
@@ -2672,15 +2677,15 @@ async fn post_delete_group(
headers: AdminHeaders,
conn: DbConn,
) -> EmptyResult {
_delete_group(&org_id, &group_id, &headers, &conn).await
delete_group_impl(&org_id, &group_id, &headers, &conn).await
}
#[delete("/organizations/<org_id>/groups/<group_id>")]
async fn delete_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
_delete_group(&org_id, &group_id, &headers, &conn).await
delete_group_impl(&org_id, &group_id, &headers, &conn).await
}
async fn _delete_group(
async fn delete_group_impl(
org_id: &OrganizationId,
group_id: &GroupId,
headers: &AdminHeaders,
@@ -2728,7 +2733,7 @@ async fn bulk_delete_groups(
let data: BulkGroupIds = data.into_inner();
for group_id in data.ids {
_delete_group(&org_id, &group_id, &headers, &conn).await?
delete_group_impl(&org_id, &group_id, &headers, &conn).await?;
}
Ok(())
}
@@ -2765,7 +2770,7 @@ async fn get_group_members(
if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() {
err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization")
};
}
let group_members: Vec<MembershipId> = GroupUser::find_by_group(&group_id, &org_id, &conn)
.await
@@ -2793,7 +2798,7 @@ async fn put_group_members(
if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() {
err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization")
};
}
let assigned_members = data.into_inner();
@@ -3100,12 +3105,12 @@ async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, conn: DbC
}
Ok(Json(json!({
"collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &conn).await),
"ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &conn).await?),
"collections": convert_json_key_lcase_first(get_org_collections_impl(&org_id, &conn).await),
"ciphers": convert_json_key_lcase_first(get_org_details_impl(&org_id, &headers.host, &headers.user.uuid, &conn).await?),
})))
}
async fn _api_key(
async fn api_key(
org_id: &OrganizationId,
data: Json<PasswordOrOtpData>,
rotate: bool,
@@ -3121,21 +3126,18 @@ async fn _api_key(
// Validate the admin users password/otp
data.validate(&user, true, &conn).await?;
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_id, &conn).await {
Some(mut org_api_key) => {
if rotate {
org_api_key.api_key = crate::crypto::generate_api_key();
org_api_key.revision_date = chrono::Utc::now().naive_utc();
org_api_key.save(&conn).await.expect("Error rotating organization API Key");
}
org_api_key
}
None => {
let api_key = crate::crypto::generate_api_key();
let new_org_api_key = OrganizationApiKey::new(org_id.clone(), api_key);
new_org_api_key.save(&conn).await.expect("Error creating organization API Key");
new_org_api_key
let org_api_key = if let Some(mut org_api_key) = OrganizationApiKey::find_by_org_uuid(org_id, &conn).await {
if rotate {
org_api_key.api_key = crate::crypto::generate_api_key();
org_api_key.revision_date = chrono::Utc::now().naive_utc();
org_api_key.save(&conn).await.expect("Error rotating organization API Key");
}
org_api_key
} else {
let api_key = crate::crypto::generate_api_key();
let new_org_api_key = OrganizationApiKey::new(org_id.clone(), api_key);
new_org_api_key.save(&conn).await.expect("Error creating organization API Key");
new_org_api_key
};
Ok(Json(json!({
@@ -3146,13 +3148,13 @@ async fn _api_key(
}
#[post("/organizations/<org_id>/api-key", data = "<data>")]
async fn api_key(
async fn post_api_key(
org_id: OrganizationId,
data: Json<PasswordOrOtpData>,
headers: AdminHeaders,
conn: DbConn,
) -> JsonResult {
_api_key(&org_id, data, false, headers, conn).await
api_key(&org_id, data, false, headers, conn).await
}
#[post("/organizations/<org_id>/rotate-api-key", data = "<data>")]
@@ -3162,5 +3164,5 @@ async fn rotate_api_key(
headers: AdminHeaders,
conn: DbConn,
) -> JsonResult {
_api_key(&org_id, data, true, headers, conn).await
api_key(&org_id, data, true, headers, conn).await
}
+63 -66
View File
@@ -1,23 +1,24 @@
use chrono::Utc;
use rocket::{
request::{FromRequest, Outcome},
serde::json::Json,
Request, Route,
};
use std::collections::HashSet;
use chrono::Utc;
use rocket::{
Request, Route,
request::{FromRequest, Outcome},
serde::json::Json,
};
use crate::{
CONFIG,
api::EmptyResult,
auth,
db::{
DbConn,
models::{
Group, GroupUser, Invitation, Membership, MembershipStatus, MembershipType, Organization,
OrganizationApiKey, OrganizationId, User,
},
DbConn,
},
mail, CONFIG,
mail,
};
pub fn routes() -> Vec<Route> {
@@ -90,19 +91,18 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, conn: DbConn
}
} else {
// If user is not part of the organization
let user = match User::find_by_mail(&user_data.email, &conn).await {
Some(user) => user, // exists in vaultwarden
None => {
// User does not exist yet
let mut new_user = User::new(&user_data.email, None);
new_user.save(&conn).await?;
let user = if let Some(user) = User::find_by_mail(&user_data.email, &conn).await {
user
} else {
// User does not exist yet
let mut new_user = User::new(&user_data.email, None);
new_user.save(&conn).await?;
if !CONFIG.mail_enabled() {
Invitation::new(&new_user.email).save(&conn).await?;
}
user_created = true;
new_user
if !CONFIG.mail_enabled() {
Invitation::new(&new_user.email).save(&conn).await?;
}
user_created = true;
new_user
};
let member_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() {
MembershipStatus::Invited as i32
@@ -110,9 +110,10 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, conn: DbConn
MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
};
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &conn).await {
Some(org) => (org.name, org.billing_email),
None => err!("Error looking up organization"),
let (org_name, org_email) = if let Some(org) = Organization::find_by_uuid(&org_id, &conn).await {
(org.name, org.billing_email)
} else {
err!("Error looking up organization")
};
let mut new_member = Membership::new(user.uuid.clone(), org_id.clone(), Some(org_email.clone()));
@@ -123,37 +124,33 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, conn: DbConn
new_member.save(&conn).await?;
if CONFIG.mail_enabled() {
if let Err(e) =
if CONFIG.mail_enabled()
&& let Err(e) =
mail::send_invite(&user, org_id.clone(), new_member.uuid.clone(), &org_name, Some(org_email)).await
{
// Upon error delete the user, invite and org member records when needed
if user_created {
user.delete(&conn).await?;
} else {
new_member.delete(&conn).await?;
}
err!(format!("Error sending invite: {e:?} "));
{
// Upon error delete the user, invite and org member records when needed
if user_created {
user.delete(&conn).await?;
} else {
new_member.delete(&conn).await?;
}
err!(format!("Error sending invite: {e:?} "));
}
}
}
if CONFIG.org_groups_enabled() {
for group_data in &data.groups {
let group_uuid = match Group::find_by_external_id_and_org(&group_data.external_id, &org_id, &conn).await {
Some(group) => group.uuid,
None => {
let mut group = Group::new(
org_id.clone(),
group_data.name.clone(),
false,
Some(group_data.external_id.clone()),
);
group.save(&conn).await?;
group.uuid
}
let group_uuid = if let Some(group) =
Group::find_by_external_id_and_org(&group_data.external_id, &org_id, &conn).await
{
group.uuid
} else {
let mut group =
Group::new(org_id.clone(), group_data.name.clone(), false, Some(group_data.external_id.clone()));
group.save(&conn).await?;
group.uuid
};
GroupUser::delete_all_by_group(&group_uuid, &org_id, &conn).await?;
@@ -174,18 +171,17 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, conn: DbConn
// 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.external_id).collect();
for member in Membership::find_by_org(&org_id, &conn).await {
if let Some(ref user_external_id) = member.external_id {
if !sync_members.contains(user_external_id) {
if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 {
// Removing owner, check that there is at least one other confirmed owner
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1
{
warn!("Can't delete the last owner");
continue;
}
if let Some(ref user_external_id) = member.external_id
&& !sync_members.contains(user_external_id)
{
if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 {
// Removing owner, check that there is at least one other confirmed owner
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 {
warn!("Can't delete the last owner");
continue;
}
member.delete(&conn).await?;
}
member.delete(&conn).await?;
}
}
}
@@ -202,12 +198,14 @@ impl<'r> FromRequest<'r> for PublicToken {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = request.headers();
// Get access_token
let access_token: &str = match headers.get_one("Authorization") {
Some(a) => match a.rsplit("Bearer ").next() {
Some(split) => split,
None => err_handler!("No access token provided"),
},
None => err_handler!("No access token provided"),
let access_token: &str = if let Some(a) = headers.get_one("Authorization") {
if let Some(split) = a.rsplit("Bearer ").next() {
split
} else {
err_handler!("No access token provided")
}
} else {
err_handler!("No access token provided")
};
// Check JWT token is valid and get device and user from it
let Ok(claims) = auth::decode_api_org(access_token) else {
@@ -229,14 +227,13 @@ impl<'r> FromRequest<'r> for PublicToken {
// Check if claims.sub is org_api_key.uuid
// Check if claims.client_sub is org_api_key.org_uuid
let conn = match DbConn::from_request(request).await {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
let Outcome::Success(conn) = DbConn::from_request(request).await else {
err_handler!("Error getting DB")
};
let Some(org_id) = claims.client_id.strip_prefix("organization.") else {
err_handler!("Malformed client_id")
};
let org_id: OrganizationId = org_id.to_string().into();
let org_id: OrganizationId = org_id.to_owned().into();
let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, &conn).await else {
err_handler!("Invalid client_id")
};
+34 -32
View File
@@ -10,15 +10,15 @@ use rocket::{
use serde_json::Value;
use crate::{
CONFIG,
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
auth::{ClientIp, Headers, Host},
config::PathType,
db::{
models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId},
DbConn, DbPool,
models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId},
},
util::{save_temp_file, NumberOrString},
CONFIG,
util::{NumberOrString, save_temp_file},
};
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
@@ -63,7 +63,7 @@ pub async fn purge_sends(pool: DbPool) {
if let Ok(conn) = pool.get().await {
Send::purge(&conn).await;
} else {
error!("Failed to get DB connection while purging sends")
error!("Failed to get DB connection while purging sends");
}
}
@@ -168,7 +168,7 @@ fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {
#[get("/sends")]
async fn get_sends(headers: Headers, conn: DbConn) -> Json<Value> {
let sends = Send::find_by_user(&headers.user.uuid, &conn);
let sends_json: Vec<Value> = sends.await.iter().map(|s| s.to_json()).collect();
let sends_json: Vec<Value> = sends.await.iter().map(Send::to_json).collect();
Json(json!({
"data": sends_json,
@@ -179,9 +179,10 @@ async fn get_sends(headers: Headers, conn: DbConn) -> Json<Value> {
#[get("/sends/<send_id>")]
async fn get_send(send_id: SendId, headers: Headers, conn: DbConn) -> JsonResult {
match Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await {
Some(send) => Ok(Json(send.to_json())),
None => err!("Send not found", "Invalid send uuid or does not belong to user"),
if let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await {
Ok(Json(send.to_json()))
} else {
err!("Send not found", "Invalid send uuid or does not belong to user")
}
}
@@ -310,9 +311,10 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, conn: DbConn)
enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
let file_length = match &data.file_length {
Some(m) => m.into_i64()?,
_ => err!("Invalid send length"),
let file_length = if let Some(m) = &data.file_length {
m.into_i64()?
} else {
err!("Invalid send length")
};
if file_length < 0 {
err!("Send size can't be negative")
@@ -457,16 +459,16 @@ async fn post_access(
err_code!(SEND_INACCESSIBLE_MSG, 404)
};
if let Some(max_access_count) = send.max_access_count {
if send.access_count >= max_access_count {
err_code!(SEND_INACCESSIBLE_MSG, 404);
}
if let Some(max_access_count) = send.max_access_count
&& send.access_count >= max_access_count
{
err_code!(SEND_INACCESSIBLE_MSG, 404);
}
if let Some(expiration) = send.expiration_date {
if Utc::now().naive_utc() >= expiration {
err_code!(SEND_INACCESSIBLE_MSG, 404)
}
if let Some(expiration) = send.expiration_date
&& Utc::now().naive_utc() >= expiration
{
err_code!(SEND_INACCESSIBLE_MSG, 404)
}
if Utc::now().naive_utc() >= send.deletion_date {
@@ -517,16 +519,16 @@ async fn post_access_file(
err_code!(SEND_INACCESSIBLE_MSG, 404)
};
if let Some(max_access_count) = send.max_access_count {
if send.access_count >= max_access_count {
err_code!(SEND_INACCESSIBLE_MSG, 404)
}
if let Some(max_access_count) = send.max_access_count
&& send.access_count >= max_access_count
{
err_code!(SEND_INACCESSIBLE_MSG, 404)
}
if let Some(expiration) = send.expiration_date {
if Utc::now().naive_utc() >= expiration {
err_code!(SEND_INACCESSIBLE_MSG, 404)
}
if let Some(expiration) = send.expiration_date
&& Utc::now().naive_utc() >= expiration
{
err_code!(SEND_INACCESSIBLE_MSG, 404)
}
if Utc::now().naive_utc() >= send.deletion_date {
@@ -572,7 +574,7 @@ async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Re
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
let token = crate::auth::encode_jwt(&token_claims);
Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host))
Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", host.host))
} else {
Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_mins(5)).await?.uri().to_string())
}
@@ -580,10 +582,10 @@ async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Re
#[get("/sends/<send_id>/<file_id>?<t>")]
async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option<NamedFile> {
if let Ok(claims) = crate::auth::decode_send(t) {
if claims.sub == format!("{send_id}/{file_id}") {
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
}
if let Ok(claims) = crate::auth::decode_send(t)
&& claims.sub == format!("{send_id}/{file_id}")
{
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
}
None
}
+11 -11
View File
@@ -1,14 +1,13 @@
use data_encoding::BASE32;
use rocket::serde::json::Json;
use rocket::Route;
use rocket::{Route, serde::json::Json};
use crate::{
api::{core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, PasswordOrOtpData},
api::{EmptyResult, JsonResult, PasswordOrOtpData, core::log_user_event, core::two_factor::generate_recover_code},
auth::{ClientIp, Headers},
crypto,
db::{
models::{EventType, TwoFactor, TwoFactorType, UserId},
DbConn,
models::{EventType, TwoFactor, TwoFactorType, UserId},
},
util::NumberOrString,
};
@@ -70,9 +69,10 @@ async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: He
.await?;
// Validate key as base32 and 20 bytes length
let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
Ok(decoded) => decoded,
_ => err!("Invalid totp secret"),
let decoded_key: Vec<u8> = if let Ok(decoded) = BASE32.decode(key.as_bytes()) {
decoded
} else {
err!("Invalid totp secret")
};
if decoded_key.len() != 20 {
@@ -82,7 +82,7 @@ async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: He
// Validate the token provided with the key, and save new twofactor
validate_totp_code(&user.uuid, &token, &key.to_uppercase(), &headers.ip, &conn).await?;
_generate_recover_code(&mut user, &conn).await;
generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
@@ -119,7 +119,7 @@ pub async fn validate_totp_code(
ip: &ClientIp,
conn: &DbConn,
) -> EmptyResult {
use totp_lite::{totp_custom, Sha1};
use totp_lite::{Sha1, totp_custom};
let Ok(decoded_secret) = BASE32.decode(secret.as_bytes()) else {
err!("Invalid TOTP secret")
@@ -128,7 +128,7 @@ pub async fn validate_totp_code(
let mut twofactor = match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Authenticator as i32, conn).await
{
Some(tf) => tf,
_ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_string()),
_ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_owned()),
};
// The amount of steps back and forward in time
@@ -145,7 +145,7 @@ pub async fn validate_totp_code(
// We need to calculate the time offsite and cast it as an u64.
// Since we only have times into the future and the totp generator needs an u64 instead of the default i64.
let time = (current_timestamp + step * 30i64) as u64;
let time: u64 = (current_timestamp + step * 30i64).cast_unsigned();
let generated = totp_custom::<Sha1>(30, 6, &decoded_secret, time);
// Check the given code equals the generated and if the time_step is larger then the one last used.
+16 -17
View File
@@ -1,22 +1,21 @@
use chrono::Utc;
use data_encoding::BASE64;
use rocket::serde::json::Json;
use rocket::Route;
use rocket::{Route, serde::json::Json};
use crate::{
CONFIG,
api::{
core::log_user_event, core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult,
PasswordOrOtpData,
ApiResult, EmptyResult, JsonResult, PasswordOrOtpData, core::log_user_event,
core::two_factor::generate_recover_code,
},
auth::Headers,
crypto,
db::{
models::{EventType, TwoFactor, TwoFactorType, User, UserId},
DbConn,
models::{EventType, TwoFactor, TwoFactorType, User, UserId},
},
error::MapResult,
http_client::make_http_request,
CONFIG,
};
pub fn routes() -> Vec<Route> {
@@ -82,8 +81,7 @@ enum DuoStatus {
impl DuoStatus {
fn data(self) -> Option<DuoData> {
match self {
DuoStatus::Global(data) => Some(data),
DuoStatus::User(data) => Some(data),
DuoStatus::Global(data) | DuoStatus::User(data) => Some(data),
DuoStatus::Disabled(_) => None,
}
}
@@ -182,7 +180,7 @@ async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, conn: DbConn)
let twofactor = TwoFactor::new(user.uuid.clone(), type_, data_str);
twofactor.save(&conn).await?;
_generate_recover_code(&mut user, &conn).await;
generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
@@ -201,14 +199,14 @@ async fn activate_duo_put(data: Json<EnableDuoData>, headers: Headers, conn: DbC
}
async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
use reqwest::{header, Method};
use reqwest::{Method, header};
use std::str::FromStr;
// https://duo.com/docs/authapi#api-details
let url = format!("https://{}{path}", &data.host);
let date = Utc::now().to_rfc2822();
let url = format!("https://{}{path}", data.host);
let dt = Utc::now().to_rfc2822();
let username = &data.ik;
let fields = [&date, method, &data.host, path, params];
let fields = [&dt, method, &data.host, path, params];
let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
let m = Method::from_str(method).unwrap_or_default();
@@ -216,7 +214,7 @@ async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData)
make_http_request(m, &url)?
.basic_auth(username, Some(password))
.header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)")
.header(header::DATE, date)
.header(header::DATE, dt)
.send()
.await?
.error_for_status()?;
@@ -356,9 +354,10 @@ fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -
err!("Invalid ikey")
}
let expire: i64 = match expire.parse() {
Ok(e) => e,
Err(_) => err!("Invalid expire time"),
let expire: i64 = if let Ok(e) = expire.parse() {
e
} else {
err!("Invalid expire time")
};
if time >= expire {
+21 -23
View File
@@ -1,23 +1,24 @@
use std::collections::HashMap;
use chrono::Utc;
use data_encoding::HEXLOWER;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
use reqwest::{header, StatusCode};
use ring::digest::{digest, Digest, SHA512_256};
use reqwest::{StatusCode, header};
use ring::digest::{Digest, SHA512_256, digest};
use serde::Serialize;
use std::collections::HashMap;
use url::Url;
use crate::{
api::{core::two_factor::duo::get_duo_keys_email, EmptyResult},
CONFIG,
api::{EmptyResult, core::two_factor::duo::get_duo_keys_email},
crypto,
db::{
models::{DeviceId, EventType, TwoFactorDuoContext},
DbConn, DbPool,
models::{DeviceId, EventType, TwoFactorDuoContext},
},
error::Error,
http_client::make_http_request,
CONFIG,
};
use url::Url;
// The location on this service that Duo should redirect users to. For us, this is a bridge
// built in to the Bitwarden clients.
@@ -124,7 +125,7 @@ impl DuoClient {
ClientAssertion {
iss: self.client_id.clone(),
sub: self.client_id.clone(),
aud: url.to_string(),
aud: url.to_owned(),
exp: now + JWT_VALIDITY_SECS,
jti: jwt_id,
iat: now,
@@ -302,7 +303,7 @@ impl DuoClient {
if !(matching_nonces && matching_usernames) {
err!("Error validating Duo authorization, nonce or username mismatch.")
};
}
Ok(())
}
@@ -347,7 +348,7 @@ pub async fn purge_duo_contexts(pool: DbPool) {
if let Ok(conn) = pool.get().await {
TwoFactorDuoContext::purge_expired_duo_contexts(&conn).await;
} else {
error!("Failed to get DB connection while purging expired Duo authentications")
error!("Failed to get DB connection while purging expired Duo authentications");
}
}
@@ -394,7 +395,7 @@ pub async fn get_duo_auth_url(
match client.health_check().await {
Ok(()) => {}
Err(e) => return Err(e),
};
}
// Generate random OAuth2 state and OIDC Nonce
let state: String = crypto::get_random_string_alphanum(STATE_LENGTH);
@@ -438,16 +439,13 @@ pub async fn validate_duo_login(
// Get the context by the state reported by the client. If we don't have one,
// it means the context is either missing or expired.
let ctx = match extract_context(state, conn).await {
Some(c) => c,
None => {
err!(
"Error validating duo authentication",
ErrorEvent {
event: EventType::UserFailedLogIn2fa
}
)
}
let Some(ctx) = extract_context(state, conn).await else {
err!(
"Error validating duo authentication",
ErrorEvent {
event: EventType::UserFailedLogIn2fa
}
)
};
// Context validation steps
@@ -476,13 +474,13 @@ pub async fn validate_duo_login(
match client.health_check().await {
Ok(()) => {}
Err(e) => return Err(e),
};
}
let d: Digest = digest(&SHA512_256, format!("{}{device_identifier}", ctx.nonce).as_bytes());
let hash: String = HEXLOWER.encode(d.as_ref());
match client.exchange_authz_code_for_result(code, email, hash.as_str()).await {
Ok(_) => Ok(()),
Ok(()) => Ok(()),
Err(_) => {
err!(
"Error validating duo authentication",
+20 -20
View File
@@ -1,20 +1,20 @@
use chrono::{DateTime, TimeDelta, Utc};
use rocket::serde::json::Json;
use rocket::Route;
use rocket::{Route, serde::json::Json};
use crate::{
CONFIG,
api::{
core::{log_user_event, two_factor::_generate_recover_code},
EmptyResult, JsonResult, PasswordOrOtpData,
core::{log_user_event, two_factor::generate_recover_code},
},
auth::{ClientHeaders, Headers},
crypto,
db::{
models::{AuthRequest, AuthRequestId, DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId},
DbConn,
models::{AuthRequest, AuthRequestId, DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId},
},
error::{Error, MapResult},
mail, CONFIG,
mail,
};
pub fn routes() -> Vec<Route> {
@@ -232,7 +232,7 @@ async fn email(data: Json<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
twofactor.data = email_data.to_json();
twofactor.save(&conn).await?;
_generate_recover_code(&mut user, &conn).await;
generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
@@ -284,9 +284,9 @@ pub async fn validate_email_code_str(
twofactor.data = email_data.to_json();
twofactor.save(conn).await?;
let date = DateTime::from_timestamp(email_data.token_sent, 0).expect("Email token timestamp invalid.").naive_utc();
let max_time = CONFIG.email_expiration_time() as i64;
if date + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {
let dt = DateTime::from_timestamp(email_data.token_sent, 0).expect("Email token timestamp invalid.").naive_utc();
let max_time = CONFIG.email_expiration_time().cast_signed();
if dt + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {
err!(
"Token has expired",
ErrorEvent {
@@ -342,9 +342,10 @@ impl EmailTokenData {
pub fn from_json(string: &str) -> Result<EmailTokenData, Error> {
let res: Result<EmailTokenData, serde_json::Error> = serde_json::from_str(string);
match res {
Ok(x) => Ok(x),
Err(_) => err!("Could not decode EmailTokenData from string"),
if let Ok(x) = res {
Ok(x)
} else {
err!("Could not decode EmailTokenData from string")
}
}
}
@@ -362,18 +363,17 @@ pub async fn activate_email_2fa(user: &User, conn: &DbConn) -> EmptyResult {
pub fn obscure_email(email: &str) -> String {
let split: Vec<&str> = email.rsplitn(2, '@').collect();
let mut name = split[1].to_string();
let mut name = split[1].to_owned();
let domain = &split[0];
let name_size = name.chars().count();
let new_name = match name_size {
1..=3 => "*".repeat(name_size),
_ => {
let stars = "*".repeat(name_size - 2);
name.truncate(2);
format!("{name}{stars}")
}
let new_name = if let 1..=3 = name_size {
"*".repeat(name_size)
} else {
let stars = "*".repeat(name_size - 2);
name.truncate(2);
format!("{name}{stars}")
};
format!("{new_name}@{domain}")
+14 -21
View File
@@ -1,28 +1,27 @@
use chrono::{TimeDelta, Utc};
use data_encoding::BASE32;
use num_traits::FromPrimitive;
use rocket::serde::json::Json;
use rocket::Route;
use rocket::{Route, serde::json::Json};
use serde::Deserialize;
use serde_json::Value;
use crate::{
CONFIG,
api::{
core::{log_event, log_user_event},
EmptyResult, JsonResult, PasswordOrOtpData,
core::{log_event, log_user_event},
},
auth::Headers,
crypto,
db::{
DbConn, DbPool,
models::{
DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor,
TwoFactorIncomplete, TwoFactorType, User, UserId,
},
DbConn, DbPool,
},
mail,
util::NumberOrString,
CONFIG,
};
pub mod authenticator;
@@ -37,7 +36,7 @@ fn has_global_duo_credentials() -> bool {
CONFIG._enable_duo() && CONFIG.duo_host().is_some() && CONFIG.duo_ikey().is_some() && CONFIG.duo_skey().is_some()
}
pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data: Option<&str>) -> bool {
pub fn is_twofactor_provider_usable(provider_type: &TwoFactorType, provider_data: Option<&str>) -> bool {
#[derive(Deserialize)]
struct DuoProviderData {
host: String,
@@ -46,7 +45,7 @@ pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data:
}
match provider_type {
TwoFactorType::Authenticator => true,
TwoFactorType::Authenticator | TwoFactorType::RecoveryCode => true,
TwoFactorType::Email => CONFIG._enable_email_2fa(),
TwoFactorType::Duo | TwoFactorType::OrganizationDuo => {
provider_data
@@ -59,7 +58,6 @@ pub fn is_twofactor_provider_usable(provider_type: TwoFactorType, provider_data:
}
TwoFactorType::Webauthn => CONFIG.is_webauthn_2fa_supported(),
TwoFactorType::Remember => !CONFIG.disable_2fa_remember(),
TwoFactorType::RecoveryCode => true,
TwoFactorType::U2f
| TwoFactorType::U2fRegisterChallenge
| TwoFactorType::U2fLoginChallenge
@@ -96,7 +94,7 @@ async fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {
.iter()
.filter_map(|tf| {
let provider_type = TwoFactorType::from_i32(tf.atype)?;
is_twofactor_provider_usable(provider_type, Some(&tf.data)).then(|| TwoFactor::to_json_provider(tf))
is_twofactor_provider_usable(&provider_type, Some(&tf.data)).then(|| TwoFactor::to_json_provider(tf))
})
.collect();
@@ -120,7 +118,7 @@ async fn get_recover(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbCo
})))
}
async fn _generate_recover_code(user: &mut User, conn: &DbConn) {
async fn generate_recover_code(user: &mut User, conn: &DbConn) {
if user.totp_recover.is_none() {
let totp_recover = crypto::encode_random_bytes::<20>(&BASE32);
user.totp_recover = Some(totp_recover);
@@ -180,9 +178,7 @@ pub async fn enforce_2fa_policy(
ip: &std::net::IpAddr,
conn: &DbConn,
) -> EmptyResult {
for member in
Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await.into_iter()
{
for member in Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await {
// Policy only applies to non-Owner/non-Admin members who have accepted joining the org
if member.atype < MembershipType::Admin {
if CONFIG.mail_enabled() {
@@ -217,7 +213,7 @@ pub async fn enforce_2fa_policy_for_org(
conn: &DbConn,
) -> EmptyResult {
let org = Organization::find_by_uuid(org_id, conn).await.unwrap();
for member in Membership::find_confirmed_by_org(org_id, conn).await.into_iter() {
for member in Membership::find_confirmed_by_org(org_id, conn).await {
// Don't enforce the policy for Admins and Owners.
if member.atype < MembershipType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() {
if CONFIG.mail_enabled() {
@@ -251,12 +247,9 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
return;
}
let conn = match pool.get().await {
Ok(conn) => conn,
_ => {
error!("Failed to get DB connection in send_incomplete_2fa_notifications()");
return;
}
let Ok(conn) = pool.get().await else {
error!("Failed to get DB connection in send_incomplete_2fa_notifications()");
return;
};
let now = Utc::now().naive_utc();
@@ -278,7 +271,7 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
)
.await
{
Ok(_) => {
Ok(()) => {
if let Err(e) = login.delete(&conn).await {
error!("Error deleting incomplete 2FA record: {e:#?}");
}
+16 -10
View File
@@ -1,16 +1,17 @@
use chrono::{naive::serde::ts_seconds, NaiveDateTime, TimeDelta, Utc};
use rocket::{serde::json::Json, Route};
use chrono::{NaiveDateTime, TimeDelta, Utc, naive::serde::ts_seconds};
use rocket::{Route, serde::json::Json};
use crate::{
CONFIG,
api::EmptyResult,
auth::Headers,
crypto,
db::{
models::{TwoFactor, TwoFactorType, UserId},
DbConn,
models::{TwoFactor, TwoFactorType, UserId},
},
error::{Error, MapResult},
mail, CONFIG,
mail,
};
pub fn routes() -> Vec<Route> {
@@ -44,9 +45,10 @@ impl ProtectedActionData {
pub fn from_json(string: &str) -> Result<Self, Error> {
let res: Result<Self, serde_json::Error> = serde_json::from_str(string);
match res {
Ok(x) => Ok(x),
Err(_) => err!("Could not decode ProtectedActionData from string"),
if let Ok(x) = res {
Ok(x)
} else {
err!("Could not decode ProtectedActionData from string")
}
}
@@ -62,7 +64,9 @@ impl ProtectedActionData {
#[post("/accounts/request-otp")]
async fn request_otp(headers: Headers, 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.");
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;
@@ -102,7 +106,9 @@ struct ProtectedActionVerify {
#[post("/accounts/verify-otp", data = "<data>")]
async fn verify_otp(data: Json<ProtectedActionVerify>, headers: Headers, 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.");
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;
@@ -133,7 +139,7 @@ pub async fn validate_protected_action_otp(
}
// Check if the token has expired (Using the email 2fa expiration time)
let max_time = CONFIG.email_expiration_time() as i64;
let max_time = CONFIG.email_expiration_time().cast_signed();
if pa_data.time_since_sent().num_seconds() > max_time {
pa.delete(conn).await?;
err!("Token has expired")
+44 -44
View File
@@ -1,34 +1,35 @@
use crate::{
api::{
core::{log_user_event, two_factor::_generate_recover_code},
EmptyResult, JsonResult, PasswordOrOtpData,
},
auth::Headers,
crypto::ct_eq,
db::{
models::{EventType, TwoFactor, TwoFactorType, UserId},
DbConn,
},
error::Error,
util::NumberOrString,
CONFIG,
};
use rocket::serde::json::Json;
use rocket::Route;
use std::{str::FromStr, sync::LazyLock, time::Duration};
use rocket::{Route, serde::json::Json};
use serde_json::Value;
use std::str::FromStr;
use std::sync::LazyLock;
use std::time::Duration;
use url::Url;
use uuid::Uuid;
use webauthn_rs::prelude::{Base64UrlSafeData, Credential, Passkey, PasskeyAuthentication, PasskeyRegistration};
use webauthn_rs::{Webauthn, WebauthnBuilder};
use webauthn_rs::{
Webauthn, WebauthnBuilder,
prelude::{Base64UrlSafeData, Credential, Passkey, PasskeyAuthentication, PasskeyRegistration},
};
use webauthn_rs_proto::{
AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw,
PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,
RequestAuthenticationExtensions, UserVerificationPolicy,
};
use crate::{
CONFIG,
api::{
EmptyResult, JsonResult, PasswordOrOtpData,
core::{log_user_event, two_factor::generate_recover_code},
},
auth::Headers,
crypto::ct_eq,
db::{
DbConn,
models::{EventType, TwoFactor, TwoFactorType, UserId},
},
error::Error,
util::NumberOrString,
};
static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
let domain = CONFIG.domain();
let domain_origin = CONFIG.domain_origin();
@@ -149,7 +150,7 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
)?;
let mut state = serde_json::to_value(&state)?;
state["rs"]["policy"] = Value::String("discouraged".to_string());
state["rs"]["policy"] = Value::String("discouraged".to_owned());
state["rs"]["extensions"].as_object_mut().unwrap().clear();
let type_ = TwoFactorType::WebauthnRegisterChallenge;
@@ -265,13 +266,12 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, con
// Retrieve and delete the saved challenge state
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
Some(tf) => {
let state: PasskeyRegistration = serde_json::from_str(&tf.data)?;
tf.delete(&conn).await?;
state
}
None => err!("Can't recover challenge"),
let state = if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
let state: PasskeyRegistration = serde_json::from_str(&tf.data)?;
tf.delete(&conn).await?;
state
} else {
err!("Can't recover challenge")
};
// Verify the credentials with the saved state
@@ -291,7 +291,7 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, con
TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)
.save(&conn)
.await?;
_generate_recover_code(&mut user, &conn).await;
generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
@@ -342,9 +342,10 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, conn: DbCo
// If entry is migrated from u2f, delete the u2f entry as well
if let Some(mut u2f) = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &conn).await
{
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&u2f.data) {
Ok(d) => d,
Err(_) => err!("Error parsing U2F data"),
let mut data: Vec<U2FRegistration> = if let Ok(d) = serde_json::from_str(&u2f.data) {
d
} else {
err!("Error parsing U2F data")
};
data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id().as_slice());
@@ -388,10 +389,10 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonRes
// Modify to discourage user verification
let mut state = serde_json::to_value(&state)?;
state["ast"]["policy"] = Value::String("discouraged".to_string());
state["ast"]["policy"] = Value::String("discouraged".to_owned());
// Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well
let app_id = format!("{}/app-id.json", &CONFIG.domain());
let app_id = format!("{}/app-id.json", CONFIG.domain());
state["ast"]["appid"] = Value::String(app_id.clone());
response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
@@ -416,18 +417,17 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonRes
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &DbConn) -> EmptyResult {
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
Some(tf) => {
let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?;
tf.delete(conn).await?;
state
}
None => err!(
let mut state = if let Some(tf) = TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?;
tf.delete(conn).await?;
state
} else {
err!(
"Can't recover login challenge",
ErrorEvent {
event: EventType::UserFailedLogIn2fa
}
),
)
};
let rsp: PublicKeyCredentialCopy = serde_json::from_str(response)?;
+10 -10
View File
@@ -1,20 +1,19 @@
use rocket::serde::json::Json;
use rocket::Route;
use rocket::{Route, serde::json::Json};
use serde_json::Value;
use yubico::{config::Config, verify_async};
use crate::{
CONFIG,
api::{
core::{log_user_event, two_factor::_generate_recover_code},
EmptyResult, JsonResult, PasswordOrOtpData,
core::{log_user_event, two_factor::generate_recover_code},
},
auth::Headers,
db::{
models::{EventType, TwoFactor, TwoFactorType},
DbConn,
models::{EventType, TwoFactor, TwoFactorType},
},
error::{Error, MapResult},
CONFIG,
};
pub fn routes() -> Vec<Route> {
@@ -46,7 +45,7 @@ pub struct YubikeyMetadata {
fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
let data_keys = [&data.key1, &data.key2, &data.key3, &data.key4, &data.key5];
data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()
data_keys.into_iter().flatten().cloned().collect()
}
fn jsonify_yubikeys(yubikeys: Vec<String>) -> Value {
@@ -64,9 +63,10 @@ fn get_yubico_credentials() -> Result<(String, String), Error> {
err!("Yubico support is disabled");
}
match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
(Some(id), Some(secret)) => Ok((id, secret)),
_ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
if let (Some(id), Some(secret)) = (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
Ok((id, secret))
} else {
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
}
}
@@ -162,7 +162,7 @@ async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, conn:
yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
yubikey_data.save(&conn).await?;
_generate_recover_code(&mut user, &conn).await;
generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
+37 -38
View File
@@ -6,28 +6,29 @@ use std::{
};
use bytes::{Bytes, BytesMut};
use futures::{stream::StreamExt, TryFutureExt};
use futures::{TryFutureExt, stream::StreamExt};
use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer};
use regex::Regex;
use reqwest::{
header::{self, HeaderMap, HeaderValue},
Client, Response,
header::{self, HeaderMap, HeaderValue},
};
use rocket::{http::ContentType, response::Redirect, Route};
use svg_hush::{data_url_filter, Filter};
use rocket::{Route, http::ContentType, response::Redirect};
use svg_hush::{Filter, data_url_filter};
use crate::{
CONFIG,
config::PathType,
error::Error,
http_client::{get_reqwest_client_builder, get_valid_host, should_block_host, CustomHttpClientError},
http_client::{CustomHttpClientError, get_reqwest_client_builder, get_valid_host, should_block_host},
util::Cached,
CONFIG,
};
pub fn routes() -> Vec<Route> {
match CONFIG.icon_service().as_str() {
"internal" => routes![icon_internal],
_ => routes![icon_external],
if CONFIG.icon_service().as_str() == "internal" {
routes![icon_internal]
} else {
routes![icon_external]
}
}
@@ -147,7 +148,7 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
if let Some(icon) = get_cached_icon(&path).await {
let icon_type = get_icon_type(&icon).unwrap_or("x-icon");
return Some((icon, icon_type.to_string()));
return Some((icon, icon_type.to_owned()));
}
if CONFIG.disable_icon_download() {
@@ -158,7 +159,7 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
match download_icon(domain).await {
Ok((icon, icon_type)) => {
save_icon(&path, icon.to_vec()).await;
Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string()))
Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_owned()))
}
Err(e) => {
// If this error comes from the custom resolver, this means this is a blocked domain
@@ -183,10 +184,10 @@ async fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
}
// Try to read the cached icon, and return it if it exists
if let Ok(operator) = CONFIG.opendal_operator_for_path_type(&PathType::IconCache) {
if let Ok(buf) = operator.read(path).await {
return Some(buf.to_vec());
}
if let Ok(operator) = CONFIG.opendal_operator_for_path_type(&PathType::IconCache)
&& let Ok(buf) = operator.read(path).await
{
return Some(buf.to_vec());
}
None
@@ -280,17 +281,17 @@ fn get_favicons_node(dom: Tokenizer<StringReader<'_>, FaviconEmitter>, icons: &m
}
for icon_tag in icon_tags {
if let Some(icon_href) = icon_tag.attributes.get(ATTR_HREF) {
if let Ok(full_href) = base_url.join(std::str::from_utf8(icon_href).unwrap_or_default()) {
let sizes = if let Some(v) = icon_tag.attributes.get(ATTR_SIZES) {
std::str::from_utf8(v).unwrap_or_default()
} else {
""
};
let priority = get_icon_priority(full_href.as_str(), sizes);
icons.push(Icon::new(priority, full_href.to_string()));
}
};
if let Some(icon_href) = icon_tag.attributes.get(ATTR_HREF)
&& let Ok(full_href) = base_url.join(std::str::from_utf8(icon_href).unwrap_or_default())
{
let sizes = if let Some(v) = icon_tag.attributes.get(ATTR_SIZES) {
std::str::from_utf8(v).unwrap_or_default()
} else {
""
};
let priority = get_icon_priority(full_href.as_str(), sizes);
icons.push(Icon::new(priority, full_href.to_string()));
}
}
}
@@ -406,7 +407,7 @@ async fn get_page(url: &str) -> Result<Response, Error> {
async fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Error> {
let mut client = CLIENT.get(url);
if !referer.is_empty() {
client = client.header("Referer", referer)
client = client.header("Referer", referer);
}
Ok(client.send().await?.error_for_status()?)
@@ -494,12 +495,10 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
let mut buffer = Bytes::new();
let mut icon_type: Option<&str> = None;
use data_url::DataUrl;
let mut icons = icon_result.iconlist.iter().take(5).peekable();
while let Some(icon) = icons.next() {
if icon.href.starts_with("data:image") {
let Ok(datauri) = DataUrl::process(&icon.href) else {
let Ok(datauri) = data_url::DataUrl::process(&icon.href) else {
continue;
};
// Check if we are able to decode the data uri
@@ -523,7 +522,7 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
}
}
_ => debug!("Extracted icon from data:image uri is invalid"),
};
}
} else {
debug!("Trying {}", icon.href);
// Make sure all icons are checked before returning error
@@ -587,10 +586,10 @@ async fn save_icon(path: &str, icon: Vec<u8>) {
fn get_icon_type(bytes: &[u8]) -> Option<&'static str> {
fn check_svg_after_xml_declaration(bytes: &[u8]) -> Option<&'static str> {
// Look for SVG tag within the first 1KB
if let Ok(content) = std::str::from_utf8(&bytes[..bytes.len().min(1024)]) {
if content.contains("<svg") || content.contains("<SVG") {
return Some("svg+xml");
}
if let Ok(content) = std::str::from_utf8(&bytes[..bytes.len().min(1024)])
&& (content.contains("<svg") || content.contains("<SVG"))
{
return Some("svg+xml");
}
None
}
@@ -733,7 +732,7 @@ impl FaviconEmitter {
let rel_value =
std::str::from_utf8(token.tag.attributes.get(ATTR_REL).unwrap()).unwrap_or_default();
if rel_value.contains("icon") && !rel_value.contains("mask-icon") {
self.emit_token = true
self.emit_token = true;
}
}
_ => (),
@@ -806,13 +805,13 @@ impl Emitter for FaviconEmitter {
fn push_attribute_name(&mut self, s: &[u8]) {
if let Some(attr) = &mut self.current_attribute {
attr.0.extend(s)
attr.0.extend(s);
}
}
fn push_attribute_value(&mut self, s: &[u8]) {
if let Some(attr) = &mut self.current_attribute {
attr.1.extend(s)
attr.1.extend(s);
}
}
+136 -134
View File
@@ -1,18 +1,20 @@
use chrono::Utc;
use num_traits::FromPrimitive;
use rocket::{
Route,
form::{Form, FromForm},
http::{Cookie, CookieJar, SameSite},
response::Redirect,
serde::json::Json,
Route,
};
use serde_json::Value;
use crate::{
CONFIG,
api::{
ApiResult, EmptyResult, JsonResult,
core::{
accounts::{_prelogin, _register, kdf_upgrade, PreloginData, RegisterData},
accounts::{PreloginData, RegisterData, kdf_upgrade, prelogin, register},
log_user_event,
two_factor::{
authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn,
@@ -21,29 +23,28 @@ use crate::{
},
master_password_policy,
push::register_push_device,
ApiResult, EmptyResult, JsonResult,
},
auth,
auth::{generate_organization_api_key_login_claims, AuthMethod, ClientHeaders, ClientIp, ClientVersion, Secure},
auth::{AuthMethod, ClientHeaders, ClientIp, ClientVersion, Secure, generate_organization_api_key_login_claims},
crypto,
db::{
DbConn,
models::{
AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeResponseError,
OrganizationApiKey, OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User,
UserId,
},
DbConn,
},
error::MapResult,
mail, sso,
sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState},
util, CONFIG,
util,
};
pub fn routes() -> Vec<Route> {
routes![
login,
prelogin,
post_prelogin,
prelogin_password,
identity_register,
register_verification_email,
@@ -68,43 +69,43 @@ async fn login(
let login_result = match data.grant_type.as_ref() {
"refresh_token" => {
_check_is_some(data.refresh_token.as_ref(), "refresh_token cannot be blank")?;
_refresh_login(data, &conn, &client_header.ip).await
check_is_some(data.refresh_token.as_ref(), "refresh_token cannot be blank")?;
refresh_login(data, &conn, &client_header.ip).await
}
"password" if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO sign-in is required"),
"password" => {
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
_check_is_some(data.password.as_ref(), "password cannot be blank")?;
_check_is_some(data.scope.as_ref(), "scope cannot be blank")?;
_check_is_some(data.username.as_ref(), "username cannot be blank")?;
check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
check_is_some(data.password.as_ref(), "password cannot be blank")?;
check_is_some(data.scope.as_ref(), "scope cannot be blank")?;
check_is_some(data.username.as_ref(), "username cannot be blank")?;
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
_password_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
password_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
}
"client_credentials" => {
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
_check_is_some(data.client_secret.as_ref(), "client_secret cannot be blank")?;
_check_is_some(data.scope.as_ref(), "scope cannot be blank")?;
check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
check_is_some(data.client_secret.as_ref(), "client_secret cannot be blank")?;
check_is_some(data.scope.as_ref(), "scope cannot be blank")?;
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
_api_key_login(data, &mut user_id, &conn, &client_header.ip).await
api_key_login(data, &mut user_id, &conn, &client_header.ip).await
}
"authorization_code" if CONFIG.sso_enabled() => {
_check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
_check_is_some(data.code.as_ref(), "code cannot be blank")?;
_check_is_some(data.code_verifier.as_ref(), "code verifier cannot be blank")?;
check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
check_is_some(data.code.as_ref(), "code cannot be blank")?;
check_is_some(data.code_verifier.as_ref(), "code verifier cannot be blank")?;
_check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
_check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
_check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
check_is_some(data.device_identifier.as_ref(), "device_identifier cannot be blank")?;
check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
_sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await
}
"authorization_code" => err!("SSO sign-in is not available"),
t => err!("Invalid type", t),
@@ -125,7 +126,7 @@ async fn login(
Err(e) => {
if let Some(ev) = e.get_event() {
log_user_event(ev.event as i32, &user_id, client_header.device_type, &client_header.ip.ip, &conn)
.await
.await;
}
}
}
@@ -134,7 +135,7 @@ async fn login(
login_result
}
async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
async fn refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
// When a refresh token is invalid or missing we need to respond with an HTTP BadRequest (400)
// It also needs to return a json which holds at least a key `error` with the value `invalid_grant`
// See the link below for details
@@ -175,7 +176,7 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> Json
}
// After exchanging the code we need to check first if 2FA is needed before continuing
async fn _sso_login(
async fn sso_login(
data: ConnectData,
user_id: &mut Option<UserId>,
conn: &DbConn,
@@ -344,7 +345,7 @@ async fn _sso_login(
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await
}
async fn _password_login(
async fn password_login(
data: ConnectData,
user_id: &mut Option<UserId>,
conn: &DbConn,
@@ -428,9 +429,9 @@ async fn _password_login(
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
if user.last_verifying_at.is_none()
|| now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()
> CONFIG.signups_verify_resend_time() as i64
> CONFIG.signups_verify_resend_time().cast_signed()
{
let resend_limit = CONFIG.signups_verify_resend_limit() as i32;
let resend_limit = CONFIG.signups_verify_resend_limit().cast_signed();
if resend_limit == 0 || user.login_verify_count < resend_limit {
// We want to send another email verification if we require signups to verify
// their email address, and we haven't sent them a reminder in a while...
@@ -566,19 +567,19 @@ async fn authenticated_response(
Ok(Json(result))
}
async fn _api_key_login(data: ConnectData, user_id: &mut Option<UserId>, conn: &DbConn, ip: &ClientIp) -> JsonResult {
async fn api_key_login(data: ConnectData, user_id: &mut Option<UserId>, conn: &DbConn, ip: &ClientIp) -> JsonResult {
// Ratelimit the login
crate::ratelimit::check_limit_login(&ip.ip)?;
// Validate scope
match data.scope.as_ref() {
Some(scope) if scope == &AuthMethod::UserApiKey.scope() => _user_api_key_login(data, user_id, conn, ip).await,
Some(scope) if scope == &AuthMethod::OrgApiKey.scope() => _organization_api_key_login(data, conn, ip).await,
Some(scope) if scope == &AuthMethod::UserApiKey.scope() => user_api_key_login(data, user_id, conn, ip).await,
Some(scope) if scope == &AuthMethod::OrgApiKey.scope() => organization_api_key_login(data, conn, ip).await,
_ => err!("Scope not supported"),
}
}
async fn _user_api_key_login(
async fn user_api_key_login(
data: ConnectData,
user_id: &mut Option<UserId>,
conn: &DbConn,
@@ -710,13 +711,13 @@ async fn _user_api_key_login(
Ok(Json(result))
}
async fn _organization_api_key_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
async fn organization_api_key_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {
// Get the org via the client_id
let client_id = data.client_id.as_ref().unwrap();
let Some(org_id) = client_id.strip_prefix("organization.") else {
err!("Malformed client_id", format!("IP: {}.", ip.ip))
};
let org_id: OrganizationId = org_id.to_string().into();
let org_id: OrganizationId = org_id.to_owned().into();
let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, conn).await else {
err!("Invalid client_id", format!("IP: {}.", ip.ip))
};
@@ -747,14 +748,13 @@ async fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> ApiResult
let device_name = data.device_name.clone().expect("No device name provided");
// Find device or create new
match Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await {
Some(device) => Ok(device),
None => {
let mut device = Device::new(device_id, user.uuid.clone(), device_name, device_type);
// save device without updating `device.updated_at`
device.save(false, conn).await?;
Ok(device)
}
if let Some(device) = Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await {
Ok(device)
} else {
let mut device = Device::new(device_id, user.uuid.clone(), device_name, device_type);
// save device without updating `device.updated_at`
device.save(false, conn).await?;
Ok(device)
}
}
@@ -780,7 +780,7 @@ async fn twofactor_auth(
.iter()
.filter_map(|tf| {
let provider_type = TwoFactorType::from_i32(tf.atype)?;
(tf.enabled && is_twofactor_provider_usable(provider_type, Some(&tf.data))).then_some(tf.atype)
(tf.enabled && is_twofactor_provider_usable(&provider_type, Some(&tf.data))).then_some(tf.atype)
})
.collect();
if twofactor_ids.is_empty() {
@@ -788,59 +788,51 @@ async fn twofactor_auth(
}
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one
// Ignore Remember and RecoveryCode Types during this check, these are special
// Ignore Remember and RecoveryCode Types during this check, these are special
if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id)
&& !twofactor_ids.contains(&selected_id)
{
err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"Invalid two factor provider"
)
}
let twofactor_code = match data.two_factor_token {
Some(ref code) => code,
None => {
err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"2FA token not provided"
)
}
let Some(ref twofactor_code) = data.two_factor_token else {
err_json!(
json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"2FA token not provided"
)
};
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
use crate::crypto::ct_eq;
let selected_data = _selected_data(selected_twofactor);
let selected_data = selected_data(selected_twofactor);
match TwoFactorType::from_i32(selected_id) {
Some(TwoFactorType::Authenticator) => {
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?;
}
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
Some(TwoFactorType::Duo) => {
match CONFIG.duo_use_iframe() {
true => {
// Legacy iframe prompt flow
duo::validate_duo_login(&user.email, twofactor_code, conn).await?
}
false => {
// OIDC based flow
duo_oidc::validate_duo_login(
&user.email,
twofactor_code,
data.client_id.as_ref().unwrap(),
data.device_identifier.as_ref().unwrap(),
conn,
)
.await?
}
if CONFIG.duo_use_iframe() {
// Legacy iframe prompt flow
duo::validate_duo_login(&user.email, twofactor_code, conn).await?;
} else {
// OIDC based flow
duo_oidc::validate_duo_login(
&user.email,
twofactor_code,
data.client_id.as_ref().unwrap(),
data.device_identifier.as_ref().unwrap(),
conn,
)
.await?;
}
}
Some(TwoFactorType::Email) => {
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?;
}
Some(TwoFactorType::Remember) => {
match device.twofactor_remember {
@@ -848,7 +840,7 @@ async fn twofactor_auth(
// If it is invalid we need to trigger the 2FA Login prompt
Some(ref token)
if !CONFIG.disable_2fa_remember()
&& (ct_eq(token, twofactor_code)
&& (crypto::ct_eq(token, twofactor_code)
&& auth::decode_2fa_remember(twofactor_code)
.is_ok_and(|t| t.sub == device.uuid && t.user_uuid == user.uuid)) => {}
_ => {
@@ -859,7 +851,7 @@ async fn twofactor_auth(
device.save(true, conn).await?;
}
err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"2FA Remember token not provided or expired"
)
}
@@ -900,11 +892,11 @@ async fn twofactor_auth(
Ok(two_factor)
}
fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
fn selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
tf.map(|t| t.data).map_res("Two factor doesn't exist")
}
async fn _json_err_twofactor(
async fn json_err_twofactor(
providers: &[i32],
user_id: &UserId,
data: &ConnectData,
@@ -925,42 +917,38 @@ async fn _json_err_twofactor(
result["TwoFactorProviders2"][provider.to_string()] = Value::Null;
match TwoFactorType::from_i32(*provider) {
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
Some(TwoFactorType::Webauthn) if CONFIG.is_webauthn_2fa_supported() => {
let request = webauthn::generate_webauthn_login(user_id, conn).await?;
result["TwoFactorProviders2"][provider.to_string()] = request.0;
}
Some(TwoFactorType::Duo) => {
let email = match User::find_by_uuid(user_id, conn).await {
Some(u) => u.email,
None => err!("User does not exist"),
let email = if let Some(u) = User::find_by_uuid(user_id, conn).await {
u.email
} else {
err!("User does not exist")
};
match CONFIG.duo_use_iframe() {
true => {
// Legacy iframe prompt flow
let (signature, host) = duo::generate_duo_signature(&email, conn).await?;
result["TwoFactorProviders2"][provider.to_string()] = json!({
"Host": host,
"Signature": signature,
})
}
false => {
// OIDC based flow
let auth_url = duo_oidc::get_duo_auth_url(
&email,
data.client_id.as_ref().unwrap(),
data.device_identifier.as_ref().unwrap(),
conn,
)
.await?;
if CONFIG.duo_use_iframe() {
// Legacy iframe prompt flow
let (signature, host) = duo::generate_duo_signature(&email, conn).await?;
result["TwoFactorProviders2"][provider.to_string()] = json!({
"Host": host,
"Signature": signature,
});
} else {
// OIDC based flow
let auth_url = duo_oidc::get_duo_auth_url(
&email,
data.client_id.as_ref().unwrap(),
data.device_identifier.as_ref().unwrap(),
conn,
)
.await?;
result["TwoFactorProviders2"][provider.to_string()] = json!({
"AuthUrl": auth_url,
})
}
result["TwoFactorProviders2"][provider.to_string()] = json!({
"AuthUrl": auth_url,
});
}
}
@@ -973,7 +961,7 @@ async fn _json_err_twofactor(
result["TwoFactorProviders2"][provider.to_string()] = json!({
"Nfc": yubikey_metadata.nfc,
})
});
}
Some(tf_type @ TwoFactorType::Email) => {
@@ -991,16 +979,30 @@ async fn _json_err_twofactor(
// Send email immediately if email is the only 2FA option.
if providers.len() == 1 && !disabled_send {
email::send_token(user_id, conn).await?
email::send_token(user_id, conn).await?;
}
let email_data = email::EmailTokenData::from_json(&twofactor.data)?;
result["TwoFactorProviders2"][provider.to_string()] = json!({
"Email": email::obscure_email(&email_data.email),
})
});
}
_ => {}
None
| Some(
TwoFactorType::Authenticator
| TwoFactorType::EmailVerificationChallenge
| TwoFactorType::OrganizationDuo
| TwoFactorType::ProtectedActions
| TwoFactorType::RecoveryCode
| TwoFactorType::Remember
| TwoFactorType::U2f
| TwoFactorType::U2fLoginChallenge
| TwoFactorType::U2fRegisterChallenge
| TwoFactorType::Webauthn
| TwoFactorType::WebauthnLoginChallenge
| TwoFactorType::WebauthnRegisterChallenge,
) => { /* Nothing special to do for these providers */ }
}
}
@@ -1008,18 +1010,18 @@ async fn _json_err_twofactor(
}
#[post("/accounts/prelogin", data = "<data>")]
async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
_prelogin(data, conn).await
async fn post_prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
prelogin(data, conn).await
}
#[post("/accounts/prelogin/password", data = "<data>")]
async fn prelogin_password(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
_prelogin(data, conn).await
prelogin(data, conn).await
}
#[post("/accounts/register", data = "<data>")]
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
_register(data, false, conn).await
register(data, false, conn).await
}
#[derive(Debug, Deserialize)]
@@ -1058,13 +1060,13 @@ async fn register_verification_email(
if should_send_mail {
let user = User::find_by_mail(&data.email, &conn).await;
if user.filter(|u| u.private_key.is_some()).is_some() {
if user.as_ref().is_some_and(|u| u.private_key.is_some()) {
// There is still a timing side channel here in that the code
// paths that send mail take noticeably longer than ones that don't.
// Add a randomized sleep to mitigate this somewhat.
use rand::{rngs::SmallRng, RngExt};
use rand::{RngExt, rngs::SmallRng};
let mut rng: SmallRng = rand::make_rng();
let sleep_ms = rng.random_range(900..=1100) as u64;
let sleep_ms: u64 = rng.random_range(900..=1100);
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
} else {
mail::send_register_verify_email(&data.email, &token).await?;
@@ -1080,7 +1082,7 @@ async fn register_verification_email(
#[post("/accounts/register/finish", data = "<data>")]
async fn register_finish(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
_register(data, true, conn).await
register(data, true, conn).await
}
// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts
@@ -1143,7 +1145,7 @@ struct ConnectData {
#[field(name = uncased("code_verifier"))]
code_verifier: Option<OIDCCodeVerifier>,
}
fn _check_is_some<T>(value: Option<&T>, msg: &str) -> EmptyResult {
fn check_is_some<T>(value: Option<&T>, msg: &str) -> EmptyResult {
if value.is_none() {
err!(msg)
}
@@ -1166,7 +1168,7 @@ const SSO_BINDING_COOKIE: &str = "VW_SSO_BINDING";
#[get("/connect/oidc-signin?<code>&<state>", rank = 1)]
async fn oidcsignin(code: OIDCCode, state: String, cookies: &CookieJar<'_>, mut conn: DbConn) -> ApiResult<Redirect> {
_oidcsignin_redirect(state, code, None, cookies, &mut conn).await
oidcsignin_redirect(state, code, None, cookies, &mut conn).await
}
// Bitwarden client appear to only care for code and state
@@ -1180,7 +1182,7 @@ async fn oidcsignin_error(
cookies: &CookieJar<'_>,
mut conn: DbConn,
) -> ApiResult<Redirect> {
_oidcsignin_redirect(
oidcsignin_redirect(
state.clone(),
state.into(),
Some(OIDCCodeResponseError {
@@ -1195,7 +1197,8 @@ async fn oidcsignin_error(
// The state was encoded using Base64 to ensure no issue with providers.
// iss and scope parameters are needed for redirection to work on IOS.
async fn _oidcsignin_redirect(
// We pass the state as the code to get it back later on.
async fn oidcsignin_redirect(
base64_state: String,
code: OIDCCode,
error: Option<OIDCCodeResponseError>,
@@ -1204,14 +1207,13 @@ async fn _oidcsignin_redirect(
) -> ApiResult<Redirect> {
let state = sso::decode_state(&base64_state)?;
let mut sso_auth = match SsoAuth::find(&state, conn).await {
None => err!(format!("Cannot retrieve sso_auth for {state}")),
Some(sso_auth) => sso_auth,
let Some(mut sso_auth) = SsoAuth::find(&state, conn).await else {
err!(format!("Cannot retrieve sso_auth for {state}"))
};
// Browser-binding check
// The cookie was set on /connect/authorize and must come from the same browser that initiated the flow.
let cookie_value = cookies.get(SSO_BINDING_COOKIE).map(|c| c.value().to_string());
let cookie_value = cookies.get(SSO_BINDING_COOKIE).map(|c| c.value().to_owned());
let provided_hash = cookie_value.as_deref().map(|v| crypto::sha256_hex(v.as_bytes()));
match (sso_auth.binding_hash.as_deref(), provided_hash.as_deref()) {
(Some(expected), Some(actual)) if crypto::ct_eq(expected, actual) => {}
+7 -4
View File
@@ -32,11 +32,13 @@ pub use crate::api::{
web::routes as web_routes,
web::static_files,
};
use crate::db::{
models::{OrgPolicy, OrgPolicyType, User},
DbConn,
use crate::{
CONFIG,
db::{
DbConn,
models::{OrgPolicy, OrgPolicyType, User},
},
};
use crate::CONFIG;
// Type aliases for API methods results
pub type ApiResult<T> = Result<T, crate::error::Error>;
@@ -74,6 +76,7 @@ impl PasswordOrOtpData {
}
}
#[expect(clippy::struct_excessive_bools, reason = "Bitwarden clients expect the data in this specific format")]
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MasterPasswordPolicy {
+19 -18
View File
@@ -6,17 +6,22 @@ use std::{
use chrono::{NaiveDateTime, Utc};
use rmpv::Value;
use rocket::{futures::StreamExt, Route};
use rocket::{Route, futures::StreamExt};
use rocket_ws::{Message, WebSocket};
use tokio::sync::mpsc::Sender;
use crate::{
CONFIG, Error,
auth::{ClientIp, WsAccessTokenHeader},
db::{
models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId},
DbConn,
models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId},
},
Error, CONFIG,
};
use super::{
push::push_auth_request, push::push_auth_response, push_cipher_update, push_folder_update, push_logout,
push_send_update, push_user_update,
};
pub static WS_USERS: LazyLock<Arc<WebSocketUsers>> = LazyLock::new(|| {
@@ -31,11 +36,6 @@ pub static WS_ANONYMOUS_SUBSCRIPTIONS: LazyLock<Arc<AnonymousWebSocketSubscripti
})
});
use super::{
push::push_auth_request, push::push_auth_response, push_cipher_update, push_folder_update, push_logout,
push_send_update, push_user_update,
};
static NOTIFICATIONS_DISABLED: LazyLock<bool> = LazyLock::new(|| !CONFIG.enable_websocket() && !CONFIG.push_enabled());
pub fn routes() -> Vec<Route> {
@@ -102,7 +102,7 @@ impl Drop for WSAnonymousEntryMapGuard {
}
}
#[allow(tail_expr_drop_order)]
#[expect(tail_expr_drop_order)]
#[get("/hub?<data..>")]
fn websockets_hub<'r>(
ws: WebSocket,
@@ -186,7 +186,7 @@ fn websockets_hub<'r>(
})
}
#[allow(tail_expr_drop_order)]
#[expect(tail_expr_drop_order)]
#[get("/anonymous-hub?<token..>")]
fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> Result<rocket_ws::Stream!['r], Error> {
info!("Accepting Anonymous Rocket WS connection from {}", ip.ip);
@@ -268,14 +268,15 @@ fn serialize(val: &Value) -> Vec<u8> {
let mut len_buf: Vec<u8> = Vec::new();
loop {
let mut size_part = size & 0x7f;
#[expect(clippy::cast_possible_truncation, reason = "masked to 7 bits, fits u8")]
let mut size_part = (size & 0x7f) as u8;
size >>= 7;
if size > 0 {
size_part |= 0x80;
}
len_buf.push(size_part as u8);
len_buf.push(size_part);
if size == 0 {
break;
@@ -329,7 +330,7 @@ pub struct WebSocketUsers {
impl WebSocketUsers {
async fn send_update(&self, user_id: &UserId, data: &[u8]) {
if let Some(user) = self.map.get(user_id.as_ref()).map(|v| v.clone()) {
for (_, sender) in user.iter() {
for (_, sender) in &user {
if let Err(e) = sender.send(Message::binary(data)).await {
error!("Error sending WS update {e}");
}
@@ -538,10 +539,10 @@ pub struct AnonymousWebSocketSubscriptions {
impl AnonymousWebSocketSubscriptions {
async fn send_update(&self, token: &str, data: &[u8]) {
if let Some(sender) = self.map.get(token).map(|v| v.clone()) {
if let Err(e) = sender.send(Message::binary(data)).await {
error!("Error sending WS update {e}");
}
if let Some(sender) = self.map.get(token).map(|v| v.clone())
&& let Err(e) = sender.send(Message::binary(data)).await
{
error!("Error sending WS update {e}");
}
}
@@ -582,7 +583,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_id:
V::Nil,
"ReceiveMessage".into(),
V::Array(vec![V::Map(vec![
("ContextId".into(), acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| V::Nil)),
("ContextId".into(), acting_device_id.map_or(V::Nil, |v| v.to_string().into())),
("Type".into(), (ut as i32).into()),
("Payload".into(), payload.into()),
])]),
+24 -24
View File
@@ -4,21 +4,21 @@ use std::{
};
use reqwest::{
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
Method,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
};
use serde_json::Value;
use tokio::sync::RwLock;
use crate::{
CONFIG,
api::{ApiResult, EmptyResult, UpdateType},
db::{
models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId},
DbConn,
models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId},
},
http_client::make_http_request,
util::{format_date, get_uuid},
CONFIG,
};
#[derive(Deserialize)]
@@ -74,9 +74,9 @@ async fn get_auth_api_token() -> ApiResult<String> {
};
let mut api_token = API_TOKEN.write().await;
api_token.valid_until = Instant::now()
.checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time
.unwrap();
// Token valid for half the specified time
let half_expires_in = u64::from((json_pushtoken.expires_in / 2).max(0).cast_unsigned());
api_token.valid_until = Instant::now().checked_add(Duration::from_secs(half_expires_in)).unwrap();
api_token.access_token = json_pushtoken.access_token;
@@ -161,7 +161,7 @@ pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device
// We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
if cipher.organization_uuid.is_some() {
return;
};
}
let Some(user_id) = &cipher.user_uuid else {
debug!("Cipher has no uuid");
return;
@@ -244,23 +244,23 @@ pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device
}
pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &DbConn) {
if let Some(s) = &send.user_uuid {
if Device::check_user_has_push_device(s, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
if let Some(s) = &send.user_uuid
&& Device::check_user_has_push_device(s, conn).await
{
tokio::task::spawn(send_to_push_relay(json!({
"userId": send.user_uuid,
"organizationId": null,
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
"type": ut as i32,
"payload": {
"id": send.uuid,
"userId": send.user_uuid,
"organizationId": null,
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
"type": ut as i32,
"payload": {
"id": send.uuid,
"userId": send.user_uuid,
"revisionDate": format_date(&send.revision_date)
},
"clientType": null,
"installationId": null
})));
}
"revisionDate": format_date(&send.revision_date)
},
"clientType": null,
"installationId": null
})));
}
}
@@ -296,7 +296,7 @@ async fn send_to_push_relay(notification_data: Value) {
.await
{
error!("An error occurred while sending a send update to the push relay: {e}");
};
}
}
pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &DbConn) {
+11 -9
View File
@@ -1,21 +1,24 @@
use std::path::{Path, PathBuf};
use rocket::{
Catcher, Route,
fs::NamedFile,
http::ContentType,
response::{content::RawCss as Css, content::RawHtml as Html, Redirect},
response::{Redirect, content::RawCss as Css, content::RawHtml as Html},
serde::json::Json,
Catcher, Route,
};
use serde_json::Value;
use crate::{
api::{core::now, ApiResult, EmptyResult},
CONFIG,
api::{ApiResult, EmptyResult, core::now},
auth::decode_file_download,
db::models::{AttachmentId, CipherId},
db::{
DbConn,
models::{AttachmentId, CipherId},
},
error::Error,
util::Cached,
CONFIG,
};
pub fn routes() -> Vec<Route> {
@@ -28,7 +31,7 @@ pub fn routes() -> Vec<Route> {
#[cfg(debug_assertions)]
if CONFIG.reload_templates() {
routes.append(&mut routes![_static_files_dev]);
routes.append(&mut routes![static_files_dev]);
}
routes
@@ -178,7 +181,6 @@ async fn attachments(cipher_id: CipherId, file_id: AttachmentId, token: String)
}
// We use DbConn here to let the alive healthcheck also verify the database connection.
use crate::db::DbConn;
#[get("/alive")]
fn alive(_conn: DbConn) -> Json<String> {
now()
@@ -197,7 +199,7 @@ fn alive_head(_conn: DbConn) -> EmptyResult {
// NOTE: Do not forget to add any new files added to the `static_files` function below!
#[cfg(debug_assertions)]
#[get("/vw_static/<filename>", rank = 1)]
pub async fn _static_files_dev(filename: PathBuf) -> Option<NamedFile> {
pub async fn static_files_dev(filename: PathBuf) -> Option<NamedFile> {
warn!("LOADING STATIC FILES FROM DISK");
let file = filename.to_str().unwrap_or_default();
let ext = filename.extension().unwrap_or_default();
@@ -210,7 +212,7 @@ pub async fn _static_files_dev(filename: PathBuf) -> Option<NamedFile> {
if let Ok(path) = path {
return NamedFile::open(path).await.ok();
};
}
None
}