You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

486 lines
19 KiB

use chrono::{NaiveDateTime, TimeDelta, Utc};
use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*;
use serde_json::Value;
use crate::{
api::EmptyResult,
crypto::ct_eq,
db::{DbConn, schema::auth_requests},
error::MapResult,
util::format_date,
};
use macros::UuidFromParam;
use super::{DeviceId, DeviceType, MembershipId, OrganizationId, UserId};
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
#[diesel(table_name = auth_requests)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct AuthRequest {
pub uuid: AuthRequestId,
pub user_uuid: UserId,
pub organization_uuid: Option<OrganizationId>,
/// See `AuthRequestType`. Decides who may answer the request and how long it stays open.
pub atype: i32,
pub request_device_identifier: DeviceId,
pub device_type: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub request_ip: String,
pub response_device_id: Option<DeviceId>,
pub access_code: String,
pub public_key: String,
pub enc_key: Option<String>,
pub master_password_hash: Option<String>,
pub approved: Option<bool>,
pub creation_date: NaiveDateTime,
pub response_date: Option<NaiveDateTime>,
pub authentication_date: Option<NaiveDateTime>,
}
/// https://github.com/bitwarden/server/blob/main/src/Core/Auth/Enums/AuthRequestType.cs
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AuthRequestType {
/// A new session asking one of the user's own devices to let it in.
AuthenticateAndUnlock = 0,
/// An existing session asking one of the user's own devices to unlock it.
Unlock = 1,
/// The user asking an administrator of their organization to let a device in, for when no
/// device of their own is around to ask.
AdminApproval = 2,
}
impl AuthRequestType {
pub fn from_i32(value: i32) -> Option<Self> {
match value {
0 => Some(AuthRequestType::AuthenticateAndUnlock),
1 => Some(AuthRequestType::Unlock),
2 => Some(AuthRequestType::AdminApproval),
_ => None,
}
}
}
impl AuthRequest {
/// A request between the user's own devices is short lived, an administrator gets a week to
/// answer, and their answer stays usable for half a day. Same windows as upstream.
/// https://github.com/bitwarden/server/blob/main/src/Core/Settings/GlobalSettings.cs
pub fn user_request_expiration() -> TimeDelta {
TimeDelta::try_minutes(15).unwrap()
}
pub fn admin_request_expiration() -> TimeDelta {
TimeDelta::try_days(7).unwrap()
}
pub fn after_admin_approval_expiration() -> TimeDelta {
TimeDelta::try_hours(12).unwrap()
}
#[expect(clippy::too_many_arguments, reason = "Every field of the request is supplied by the caller")]
pub fn new(
user_uuid: UserId,
organization_uuid: Option<OrganizationId>,
atype: AuthRequestType,
request_device_identifier: DeviceId,
device_type: i32,
request_ip: String,
access_code: String,
public_key: String,
) -> Self {
let now = Utc::now().naive_utc();
Self {
uuid: AuthRequestId(crate::util::get_uuid()),
user_uuid,
organization_uuid,
atype: atype as i32,
request_device_identifier,
device_type,
request_ip,
response_device_id: None,
access_code,
public_key,
enc_key: None,
master_password_hash: None,
approved: None,
creation_date: now,
response_date: None,
authentication_date: None,
}
}
pub fn is_admin_approval(&self) -> bool {
self.atype == AuthRequestType::AdminApproval as i32
}
pub fn is_expired(&self) -> bool {
let now = Utc::now().naive_utc();
if self.is_admin_approval() {
// Once approved the clock restarts, so the user has time to come back and use it.
if let (Some(true), Some(response_date)) = (self.approved, self.response_date) {
return now > response_date + Self::after_admin_approval_expiration();
}
return now > self.creation_date + Self::admin_request_expiration();
}
now > self.creation_date + Self::user_request_expiration()
}
pub fn to_json_for_pending_device(&self) -> Value {
json!({
"id": self.uuid,
"creationDate": format_date(&self.creation_date),
})
}
/// What an administrator gets to see about a request that is waiting for them, which is the
/// public key of the asking device and enough about it to recognise it. Same shape as
/// `PendingOrganizationAuthRequestResponseModel` upstream.
///
/// Deliberately no access code, which is the asking device's own proof, and no wrapped key: a
/// request that is still waiting has none, and handing one out here would be crypto material
/// the answering side has no use for.
pub fn to_json_for_organization(&self, email: &str, member_id: &MembershipId) -> Value {
json!({
"id": self.uuid,
"userId": self.user_uuid,
"organizationUserId": member_id,
"email": email,
"publicKey": self.public_key,
"requestDeviceIdentifier": self.request_device_identifier,
"requestDeviceType": DeviceType::from_i32(self.device_type).to_string(),
"requestIpAddress": self.request_ip,
// Not recorded here, but the clients read it, so it is answered rather than missing.
"requestCountryName": null,
"creationDate": format_date(&self.creation_date),
"object": "pending-org-auth-request",
})
}
}
impl AuthRequest {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(auth_requests::table)
.values(&*self)
.execute(conn)
{
Ok(_) => Ok(()),
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(auth_requests::table)
.filter(auth_requests::uuid.eq(&self.uuid))
.set(&*self)
.execute(conn)
.map_res("Error auth_request")
}
Err(e) => Err(e.into()),
}.map_res("Error auth_request")
}
postgresql {
diesel::insert_into(auth_requests::table)
.values(&*self)
.on_conflict(auth_requests::uuid)
.do_update()
.set(&*self)
.execute(conn)
.map_res("Error saving auth_request")
}
}
}
pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &DbConn) -> Option<Self> {
conn.run(move |conn| auth_requests::table.filter(auth_requests::uuid.eq(uuid)).first::<Self>(conn).ok()).await
}
pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
conn.run(move |conn| {
auth_requests::table
.filter(auth_requests::uuid.eq(uuid))
.filter(auth_requests::user_uuid.eq(user_uuid))
.first::<Self>(conn)
.ok()
})
.await
}
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
conn.run(move |conn| {
auth_requests::table
.filter(auth_requests::user_uuid.eq(user_uuid))
.load::<Self>(conn)
.expect("Error loading auth_requests")
})
.await
}
/// The request a device is currently waiting on, if it is still open and still within its
/// window.
///
/// Only the types a device answers for itself. A request addressed to an administrator is
/// answered through the organization and stays open for a week, so counting it here would let
/// it shadow the short lived request the user is actually being shown.
/// https://github.com/bitwarden/server/blob/main/src/Infrastructure.EntityFramework/Auth/Repositories/Queries/DeviceWithPendingAuthByUserIdQuery.cs
pub async fn find_by_user_and_requested_device(
user_uuid: &UserId,
device_uuid: &DeviceId,
conn: &DbConn,
) -> Option<Self> {
let oldest = Utc::now().naive_utc() - Self::user_request_expiration();
conn.run(move |conn| {
auth_requests::table
.filter(auth_requests::user_uuid.eq(user_uuid))
.filter(auth_requests::request_device_identifier.eq(device_uuid))
.filter(auth_requests::atype.ne(AuthRequestType::AdminApproval as i32))
.filter(auth_requests::approved.is_null())
.filter(auth_requests::creation_date.gt(oldest))
.order_by(auth_requests::creation_date.desc())
.first::<Self>(conn)
.ok()
})
.await
}
/// The open request a device already has waiting at this organization, if any.
///
/// Asking again from the same device updates that one instead of adding another, so a client
/// that retries cannot fill the table or mail the administrators over and over.
///
/// A request past its window does not count: it is one nobody can answer any more, and reviving
/// it by moving its date forward would leave the user waiting on a request the administrators
/// were never told about. Asking again after it ran out is a new request, and is announced.
pub async fn find_pending_admin_approval(
user_uuid: &UserId,
device_uuid: &DeviceId,
org_uuid: &OrganizationId,
conn: &DbConn,
) -> Option<Self> {
let oldest = Utc::now().naive_utc() - Self::admin_request_expiration();
conn.run(move |conn| {
auth_requests::table
.filter(auth_requests::user_uuid.eq(user_uuid))
.filter(auth_requests::request_device_identifier.eq(device_uuid))
.filter(auth_requests::organization_uuid.eq(org_uuid))
.filter(auth_requests::atype.eq(AuthRequestType::AdminApproval as i32))
.filter(auth_requests::approved.is_null())
.filter(auth_requests::creation_date.gt(oldest))
.order_by(auth_requests::creation_date.desc())
.first::<Self>(conn)
.ok()
})
.await
}
/// Everything an administrator of this organization still has to answer.
pub async fn find_pending_admin_approval_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
conn.run(move |conn| {
auth_requests::table
.filter(auth_requests::organization_uuid.eq(org_uuid))
.filter(auth_requests::atype.eq(AuthRequestType::AdminApproval as i32))
.filter(auth_requests::approved.is_null())
.order_by(auth_requests::creation_date.desc())
.load::<Self>(conn)
.expect("Error loading auth_requests")
})
.await
}
/// Bound to the organization on purpose: an administrator may only ever reach a request that
/// was addressed to their own organization.
pub async fn find_admin_approval_by_org_and_uuid(
uuid: &AuthRequestId,
org_uuid: &OrganizationId,
conn: &DbConn,
) -> Option<Self> {
conn.run(move |conn| {
auth_requests::table
.filter(auth_requests::uuid.eq(uuid))
.filter(auth_requests::organization_uuid.eq(org_uuid))
.filter(auth_requests::atype.eq(AuthRequestType::AdminApproval as i32))
.first::<Self>(conn)
.ok()
})
.await
}
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
conn.run(move |conn| {
diesel::delete(auth_requests::table.filter(auth_requests::uuid.eq(&self.uuid)))
.execute(conn)
.map_res("Error deleting auth request")
})
.await
}
pub fn check_access_code(&self, access_code: &str) -> bool {
ct_eq(&self.access_code, access_code)
}
/// Drops everything past its window, which is a different one per type.
///
/// https://github.com/bitwarden/server/blob/f8ee2270409f7a13125cd414c450740af605a175/src/Sql/dbo/Auth/Stored%20Procedures/AuthRequest_DeleteIfExpired.sql
/// One statement per case rather than reading the table and deleting row by row, so the work
/// stays in the database however many requests have piled up.
pub async fn purge_expired_auth_requests(conn: &DbConn) {
let now = Utc::now().naive_utc();
let admin = AuthRequestType::AdminApproval as i32;
let between_devices = now - Self::user_request_expiration();
let for_an_admin = now - Self::admin_request_expiration();
let after_approval = now - Self::after_admin_approval_expiration();
let result = conn
.run(move |conn| -> EmptyResult {
// Between the user's own devices: 15 minutes from the moment it was asked.
let _: () = diesel::delete(
auth_requests::table
.filter(auth_requests::atype.ne(admin))
.filter(auth_requests::creation_date.lt(between_devices)),
)
.execute(conn)
.map_res("Error purging the expired auth requests")?;
// Approved by an administrator: half a day from the answer, so the user has time to
// come back and use it.
let _: () = diesel::delete(
auth_requests::table
.filter(auth_requests::atype.eq(admin))
.filter(auth_requests::approved.eq(true))
.filter(auth_requests::response_date.lt(after_approval)),
)
.execute(conn)
.map_res("Error purging the approved auth requests")?;
// Waiting for an administrator, or refused by one: a week from the moment it was
// asked either way, a refusal does not extend anything.
diesel::delete(
auth_requests::table
.filter(auth_requests::atype.eq(admin))
.filter(auth_requests::approved.is_null().or(auth_requests::approved.eq(false)))
.filter(auth_requests::creation_date.lt(for_an_admin)),
)
.execute(conn)
.map_res("Error purging the unanswered auth requests")
})
.await;
if let Err(e) = result {
error!("Error purging the expired auth requests: {e:#?}");
}
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct AuthRequestId(String);
#[cfg(test)]
mod tests {
use super::*;
fn request(atype: AuthRequestType, age: TimeDelta) -> AuthRequest {
let mut auth_request = AuthRequest::new(
String::from("user").into(),
None,
atype,
String::from("device").into(),
9,
String::from("127.0.0.1"),
String::from("code"),
String::from("2.public"),
);
auth_request.creation_date = Utc::now().naive_utc() - age;
auth_request
}
#[test]
fn a_request_between_the_users_own_devices_is_short_lived() {
assert!(!request(AuthRequestType::AuthenticateAndUnlock, TimeDelta::try_minutes(14).unwrap()).is_expired());
assert!(request(AuthRequestType::AuthenticateAndUnlock, TimeDelta::try_minutes(16).unwrap()).is_expired());
assert!(request(AuthRequestType::Unlock, TimeDelta::try_minutes(16).unwrap()).is_expired());
}
#[test]
fn an_administrator_gets_a_week_to_answer() {
assert!(!request(AuthRequestType::AdminApproval, TimeDelta::try_days(6).unwrap()).is_expired());
assert!(request(AuthRequestType::AdminApproval, TimeDelta::try_days(8).unwrap()).is_expired());
}
#[test]
fn a_request_nobody_answered_in_time_is_not_still_pending() {
// `find_pending_admin_approval` decides whether asking again reuses the open request or
// starts a new one, and filters on the same window as this. A request past it must not come
// back: reviving it by moving its date forward would leave the user waiting on something
// the administrators were never told about, because only a new request mails them.
let mut auth_request =
request(AuthRequestType::AdminApproval, AuthRequest::admin_request_expiration() + TimeDelta::seconds(1));
assert_eq!(auth_request.approved, None, "still unanswered");
assert!(auth_request.is_expired());
// One minute short of the window is still the same request, and asking again updates it
// rather than mailing everyone a second time.
auth_request.creation_date =
Utc::now().naive_utc() - AuthRequest::admin_request_expiration() + TimeDelta::minutes(1);
assert!(!auth_request.is_expired());
}
#[test]
fn the_answer_of_an_administrator_starts_its_own_clock() {
// Answered right at the end of the week, so the request itself is long past its window.
let mut auth_request = request(AuthRequestType::AdminApproval, TimeDelta::try_days(7).unwrap());
auth_request.approved = Some(true);
auth_request.response_date = Some(Utc::now().naive_utc() - TimeDelta::try_hours(11).unwrap());
assert!(!auth_request.is_expired(), "the user still has time to come back and use it");
auth_request.response_date = Some(Utc::now().naive_utc() - TimeDelta::try_hours(13).unwrap());
assert!(auth_request.is_expired());
// A refusal does not extend anything, the request stays dead after its own window.
auth_request.approved = Some(false);
auth_request.response_date = Some(Utc::now().naive_utc());
assert!(auth_request.is_expired());
}
#[test]
fn only_the_admin_approval_type_is_answered_by_an_organization() {
assert!(request(AuthRequestType::AdminApproval, TimeDelta::zero()).is_admin_approval());
assert!(!request(AuthRequestType::Unlock, TimeDelta::zero()).is_admin_approval());
assert!(!request(AuthRequestType::AuthenticateAndUnlock, TimeDelta::zero()).is_admin_approval());
}
#[test]
fn unknown_request_types_are_rejected() {
assert_eq!(AuthRequestType::from_i32(0), Some(AuthRequestType::AuthenticateAndUnlock));
assert_eq!(AuthRequestType::from_i32(1), Some(AuthRequestType::Unlock));
assert_eq!(AuthRequestType::from_i32(2), Some(AuthRequestType::AdminApproval));
assert_eq!(AuthRequestType::from_i32(3), None);
assert_eq!(AuthRequestType::from_i32(-1), None);
}
}