Browse Source

Merge 72a83130ae into 07569a06da

pull/6495/merge
Stefan Melmuk 1 day ago
committed by GitHub
parent
commit
0ebc436116
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/api/core/accounts.rs
  2. 41
      src/api/core/two_factor/email.rs
  3. 24
      src/api/identity.rs
  4. 2
      src/api/push.rs
  5. 2
      src/auth.rs
  6. 56
      src/db/models/device.rs
  7. 18
      src/db/models/user.rs

2
src/api/core/accounts.rs

@ -1409,7 +1409,7 @@ async fn put_device_token(device_id: DeviceId, data: Json<PushToken>, headers: H
} }
device.push_token = Some(token); device.push_token = Some(token);
if let Err(e) = device.save(&conn).await { if let Err(e) = device.save(true, &conn).await {
err!(format!("An error occurred while trying to save the device push token: {e}")); err!(format!("An error occurred while trying to save the device push token: {e}"));
} }

41
src/api/core/two_factor/email.rs

@ -10,7 +10,7 @@ use crate::{
auth::Headers, auth::Headers,
crypto, crypto,
db::{ db::{
models::{EventType, TwoFactor, TwoFactorType, User, UserId}, models::{DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId},
DbConn, DbConn,
}, },
error::{Error, MapResult}, error::{Error, MapResult},
@ -24,10 +24,12 @@ pub fn routes() -> Vec<Route> {
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct SendEmailLoginData { struct SendEmailLoginData {
#[serde(alias = "DeviceIdentifier")]
device_identifier: DeviceId,
#[serde(alias = "Email")] #[serde(alias = "Email")]
email: String, email: Option<String>,
#[serde(alias = "MasterPasswordHash")] #[serde(alias = "MasterPasswordHash")]
master_password_hash: String, master_password_hash: Option<String>,
} }
/// User is trying to login and wants to use email 2FA. /// User is trying to login and wants to use email 2FA.
@ -36,25 +38,40 @@ struct SendEmailLoginData {
async fn send_email_login(data: Json<SendEmailLoginData>, conn: DbConn) -> EmptyResult { async fn send_email_login(data: Json<SendEmailLoginData>, conn: DbConn) -> EmptyResult {
let data: SendEmailLoginData = data.into_inner(); let data: SendEmailLoginData = data.into_inner();
use crate::db::models::User; if !CONFIG._enable_email_2fa() {
err!("Email 2FA is disabled")
}
// Get the user // Get the user
let Some(user) = User::find_by_mail(&data.email, &conn).await else { let email = match &data.email {
err!("Username or password is incorrect. Try again.") Some(email) if !email.is_empty() => Some(email),
_ => None,
};
let user = if let Some(email) = email {
let Some(master_password_hash) = &data.master_password_hash else {
err!("No password hash has been submitted.")
}; };
if !CONFIG._enable_email_2fa() { let Some(user) = User::find_by_mail(email, &conn).await else {
err!("Email 2FA is disabled") err!("Username or password is incorrect. Try again.")
} };
// Check password // Check password
if !user.check_valid_password(&data.master_password_hash) { if !user.check_valid_password(master_password_hash) {
err!("Username or password is incorrect. Try again.") err!("Username or password is incorrect. Try again.")
} }
send_token(&user.uuid, &conn).await?; user
} else {
// SSO login only sends device id, so we get the user by the most recently used device
let Some(user) = User::find_by_device_for_email2fa(&data.device_identifier, &conn).await else {
err!("Username or password is incorrect. Try again.")
};
Ok(()) user
};
send_token(&user.uuid, &conn).await
} }
/// Generate the token, save the data for later verification and send email to user /// Generate the token, save the data for later verification and send email to user

24
src/api/identity.rs

@ -1,4 +1,4 @@
use chrono::{NaiveDateTime, Utc}; use chrono::Utc;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use rocket::{ use rocket::{
form::{Form, FromForm}, form::{Form, FromForm},
@ -147,7 +147,7 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> Json
} }
Ok((mut device, auth_tokens)) => { Ok((mut device, auth_tokens)) => {
// Save to update `device.updated_at` to track usage and toggle new status // Save to update `device.updated_at` to track usage and toggle new status
device.save(conn).await?; device.save(true, conn).await?;
let result = json!({ let result = json!({
"refresh_token": auth_tokens.refresh_token(), "refresh_token": auth_tokens.refresh_token(),
@ -267,6 +267,7 @@ async fn _sso_login(
} }
Some((mut user, sso_user)) => { Some((mut user, sso_user)) => {
let mut device = get_device(&data, conn, &user).await?; let mut device = get_device(&data, conn, &user).await?;
let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?; let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?;
if user.private_key.is_none() { if user.private_key.is_none() {
@ -313,7 +314,7 @@ async fn _sso_login(
auth_user.expires_in, auth_user.expires_in,
)?; )?;
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await
} }
async fn _password_login( async fn _password_login(
@ -435,7 +436,7 @@ async fn _password_login(
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id); let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await
} }
async fn authenticated_response( async fn authenticated_response(
@ -443,12 +444,12 @@ async fn authenticated_response(
device: &mut Device, device: &mut Device,
auth_tokens: auth::AuthTokens, auth_tokens: auth::AuthTokens,
twofactor_token: Option<String>, twofactor_token: Option<String>,
now: &NaiveDateTime,
conn: &DbConn, conn: &DbConn,
ip: &ClientIp, ip: &ClientIp,
) -> JsonResult { ) -> JsonResult {
if CONFIG.mail_enabled() && device.is_new() { if CONFIG.mail_enabled() && device.is_new() {
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), now, device).await { let now = Utc::now().naive_utc();
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, device).await {
error!("Error sending new device email: {e:#?}"); error!("Error sending new device email: {e:#?}");
if CONFIG.require_device_email() { if CONFIG.require_device_email() {
@ -468,7 +469,7 @@ async fn authenticated_response(
} }
// Save to update `device.updated_at` to track usage and toggle new status // Save to update `device.updated_at` to track usage and toggle new status
device.save(conn).await?; device.save(true, conn).await?;
let master_password_policy = master_password_policy(user, conn).await; let master_password_policy = master_password_policy(user, conn).await;
@ -585,7 +586,7 @@ async fn _user_api_key_login(
let access_claims = auth::LoginJwtClaims::default(&device, &user, &AuthMethod::UserApiKey, data.client_id); let access_claims = auth::LoginJwtClaims::default(&device, &user, &AuthMethod::UserApiKey, data.client_id);
// Save to update `device.updated_at` to track usage and toggle new status // Save to update `device.updated_at` to track usage and toggle new status
device.save(conn).await?; device.save(true, conn).await?;
info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip); info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip);
@ -648,7 +649,12 @@ async fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> ApiResult
// Find device or create new // Find device or create new
match Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await { match Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await {
Some(device) => Ok(device), Some(device) => Ok(device),
None => Device::new(device_id, user.uuid.clone(), device_name, device_type, conn).await, 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)
}
} }
} }

2
src/api/push.rs

@ -128,7 +128,7 @@ pub async fn register_push_device(device: &mut Device, conn: &DbConn) -> EmptyRe
err!(format!("An error occurred while proceeding registration of a device: {e}")); err!(format!("An error occurred while proceeding registration of a device: {e}"));
} }
if let Err(e) = device.save(conn).await { if let Err(e) = device.save(true, conn).await {
err!(format!("An error occurred while trying to save the (registered) device push uuid: {e}")); err!(format!("An error occurred while trying to save the (registered) device push uuid: {e}"));
} }

2
src/auth.rs

@ -1223,7 +1223,7 @@ pub async fn refresh_tokens(
}; };
// Save to update `updated_at`. // Save to update `updated_at`.
device.save(conn).await?; device.save(true, conn).await?;
let user = match User::find_by_uuid(&device.user_uuid, conn).await { let user = match User::find_by_uuid(&device.user_uuid, conn).await {
None => err!("Impossible to find user"), None => err!("Impossible to find user"),

56
src/db/models/device.rs

@ -35,6 +35,25 @@ pub struct Device {
/// Local methods /// Local methods
impl Device { impl Device {
pub fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32) -> Self {
let now = Utc::now().naive_utc();
Self {
uuid,
created_at: now,
updated_at: now,
user_uuid,
name,
atype,
push_uuid: Some(PushId(get_uuid())),
push_token: None,
refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL),
twofactor_remember: None,
}
}
pub fn to_json(&self) -> Value { pub fn to_json(&self) -> Value {
json!({ json!({
"id": self.uuid, "id": self.uuid,
@ -110,38 +129,21 @@ impl DeviceWithAuthRequest {
} }
use crate::db::DbConn; use crate::db::DbConn;
use crate::api::{ApiResult, EmptyResult}; use crate::api::EmptyResult;
use crate::error::MapResult; use crate::error::MapResult;
/// Database methods /// Database methods
impl Device { impl Device {
pub async fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32, conn: &DbConn) -> ApiResult<Device> { pub async fn save(&mut self, update_time: bool, conn: &DbConn) -> EmptyResult {
let now = Utc::now().naive_utc(); if update_time {
self.updated_at = Utc::now().naive_utc();
let device = Self {
uuid,
created_at: now,
updated_at: now,
user_uuid,
name,
atype,
push_uuid: Some(PushId(get_uuid())),
push_token: None,
refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL),
twofactor_remember: None,
};
device.inner_save(conn).await.map(|()| device)
} }
async fn inner_save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn: db_run! { conn:
sqlite, mysql { sqlite, mysql {
crate::util::retry(|| crate::util::retry(||
diesel::replace_into(devices::table) diesel::replace_into(devices::table)
.values(self) .values(&*self)
.execute(conn), .execute(conn),
10, 10,
).map_res("Error saving device") ).map_res("Error saving device")
@ -149,10 +151,10 @@ impl Device {
postgresql { postgresql {
crate::util::retry(|| crate::util::retry(||
diesel::insert_into(devices::table) diesel::insert_into(devices::table)
.values(self) .values(&*self)
.on_conflict((devices::uuid, devices::user_uuid)) .on_conflict((devices::uuid, devices::user_uuid))
.do_update() .do_update()
.set(self) .set(&*self)
.execute(conn), .execute(conn),
10, 10,
).map_res("Error saving device") ).map_res("Error saving device")
@ -160,12 +162,6 @@ impl Device {
} }
} }
// Should only be called after user has passed authentication
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
self.inner_save(conn).await
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
db_run! { conn: { db_run! { conn: {
diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid))) diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid)))

18
src/db/models/user.rs

@ -1,4 +1,4 @@
use crate::db::schema::{invitations, sso_users, users}; use crate::db::schema::{invitations, sso_users, twofactor_incomplete, users};
use chrono::{NaiveDateTime, TimeDelta, Utc}; use chrono::{NaiveDateTime, TimeDelta, Utc};
use derive_more::{AsRef, Deref, Display, From}; use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*; use diesel::prelude::*;
@ -10,7 +10,7 @@ use super::{
use crate::{ use crate::{
api::EmptyResult, api::EmptyResult,
crypto, crypto,
db::DbConn, db::{models::DeviceId, DbConn},
error::MapResult, error::MapResult,
sso::OIDCIdentifier, sso::OIDCIdentifier,
util::{format_date, get_uuid, retry}, util::{format_date, get_uuid, retry},
@ -386,6 +386,20 @@ impl User {
}} }}
} }
pub async fn find_by_device_for_email2fa(device_uuid: &DeviceId, conn: &DbConn) -> Option<Self> {
if let Some(user_uuid) = db_run! ( conn: {
twofactor_incomplete::table
.filter(twofactor_incomplete::device_uuid.eq(device_uuid))
.order_by(twofactor_incomplete::login_time.desc())
.select(twofactor_incomplete::user_uuid)
.first::<UserId>(conn)
.ok()
}) {
return Self::find_by_uuid(&user_uuid, conn).await;
}
None
}
pub async fn get_all(conn: &DbConn) -> Vec<(Self, Option<SsoUser>)> { pub async fn get_all(conn: &DbConn) -> Vec<(Self, Option<SsoUser>)> {
db_run! { conn: { db_run! { conn: {
users::table users::table

Loading…
Cancel
Save