Files
vaultwarden/src/db/models/event.rs
Mathijs van Veluw ef7835d1b0 Sync with Upstream (#5798)
* WIP Sync with Upstream

WIP on syncing API Responses with upstream.
This to prevent issues with new clients, and find possible current issues like members, collections, groups etc..

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

* More API Response fixes

- Some 2fa checks
- Some org checks
- Reconfigured the experimental flags and noted which are deprecated
  Also removed some hard-coded defaults.
- Updated crates

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

* Add avatar color to emergency access api

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

* Fix spelling and some crate updates

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

* Use PushId and always generate the PushId

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

* Fix clippy lints

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

* Fix several Push issues and API's

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

* Check if push_uuid is empty and generate when needed

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

* Updated some comments and removed old export format

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

* cargo update

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

* Fix bulk edit Fixes #5737

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

* Send an email when an account exists already

When you want to change your email address into an account which already exists, upstream sends an email to the existing account.
Lets do the same.

Kinda fixes #5630

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

* Update 2fa removal/revoke email

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

* Allow col managers to import

This commit adds functionality to allow users with manage access to a collection, or managers with all access to import into an organization.

Fixes #5592

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

* Filter deprected flags and only return active flags

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

* Fix grammer

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

* Rename Small to Compact

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

* Rebase with upstream and fix conflicts

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

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2025-05-26 21:00:59 +02:00

348 lines
13 KiB
Rust

use chrono::{NaiveDateTime, TimeDelta, Utc};
//use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use super::{CipherId, CollectionId, GroupId, MembershipId, OrgPolicyId, OrganizationId, UserId};
use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG};
// https://bitwarden.com/help/event-logs/
db_object! {
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs
// Upstream SQL: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Sql/dbo/Tables/Event.sql
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = event)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Event {
pub uuid: EventId,
pub event_type: i32, // EventType
pub user_uuid: Option<UserId>,
pub org_uuid: Option<OrganizationId>,
pub cipher_uuid: Option<CipherId>,
pub collection_uuid: Option<CollectionId>,
pub group_uuid: Option<GroupId>,
pub org_user_uuid: Option<MembershipId>,
pub act_user_uuid: Option<UserId>,
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub device_type: Option<i32>,
pub ip_address: Option<String>,
pub event_date: NaiveDateTime,
pub policy_uuid: Option<OrgPolicyId>,
pub provider_uuid: Option<String>,
pub provider_user_uuid: Option<String>,
pub provider_org_uuid: Option<String>,
}
}
// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/EventType.cs
#[derive(Debug, Copy, Clone)]
pub enum EventType {
// User
UserLoggedIn = 1000,
UserChangedPassword = 1001,
UserUpdated2fa = 1002,
UserDisabled2fa = 1003,
UserRecovered2fa = 1004,
UserFailedLogIn = 1005,
UserFailedLogIn2fa = 1006,
UserClientExportedVault = 1007,
// UserUpdatedTempPassword = 1008, // Not supported
// UserMigratedKeyToKeyConnector = 1009, // Not supported
UserRequestedDeviceApproval = 1010,
// UserTdeOffboardingPasswordSet = 1011, // Not supported
// Cipher
CipherCreated = 1100,
CipherUpdated = 1101,
CipherDeleted = 1102,
CipherAttachmentCreated = 1103,
CipherAttachmentDeleted = 1104,
CipherShared = 1105,
CipherUpdatedCollections = 1106,
CipherClientViewed = 1107,
CipherClientToggledPasswordVisible = 1108,
CipherClientToggledHiddenFieldVisible = 1109,
CipherClientToggledCardCodeVisible = 1110,
CipherClientCopiedPassword = 1111,
CipherClientCopiedHiddenField = 1112,
CipherClientCopiedCardCode = 1113,
CipherClientAutofilled = 1114,
CipherSoftDeleted = 1115,
CipherRestored = 1116,
CipherClientToggledCardNumberVisible = 1117,
// Collection
CollectionCreated = 1300,
CollectionUpdated = 1301,
CollectionDeleted = 1302,
// Group
GroupCreated = 1400,
GroupUpdated = 1401,
GroupDeleted = 1402,
// OrganizationUser
OrganizationUserInvited = 1500,
OrganizationUserConfirmed = 1501,
OrganizationUserUpdated = 1502,
OrganizationUserRemoved = 1503, // Organization user data was deleted
OrganizationUserUpdatedGroups = 1504,
// OrganizationUserUnlinkedSso = 1505, // Not supported
OrganizationUserResetPasswordEnroll = 1506,
OrganizationUserResetPasswordWithdraw = 1507,
OrganizationUserAdminResetPassword = 1508,
// OrganizationUserResetSsoLink = 1509, // Not supported
// OrganizationUserFirstSsoLogin = 1510, // Not supported
OrganizationUserRevoked = 1511,
OrganizationUserRestored = 1512,
OrganizationUserApprovedAuthRequest = 1513,
OrganizationUserRejectedAuthRequest = 1514,
OrganizationUserDeleted = 1515, // Both user and organization user data were deleted
OrganizationUserLeft = 1516, // User voluntarily left the organization
// Organization
OrganizationUpdated = 1600,
OrganizationPurgedVault = 1601,
OrganizationClientExportedVault = 1602,
// OrganizationVaultAccessed = 1603,
// OrganizationEnabledSso = 1604, // Not supported
// OrganizationDisabledSso = 1605, // Not supported
// OrganizationEnabledKeyConnector = 1606, // Not supported
// OrganizationDisabledKeyConnector = 1607, // Not supported
// OrganizationSponsorshipsSynced = 1608, // Not supported
// OrganizationCollectionManagementUpdated = 1609, // Not supported
// Policy
PolicyUpdated = 1700,
// Provider (Not yet supported)
// ProviderUserInvited = 1800, // Not supported
// ProviderUserConfirmed = 1801, // Not supported
// ProviderUserUpdated = 1802, // Not supported
// ProviderUserRemoved = 1803, // Not supported
// ProviderOrganizationCreated = 1900, // Not supported
// ProviderOrganizationAdded = 1901, // Not supported
// ProviderOrganizationRemoved = 1902, // Not supported
// ProviderOrganizationVaultAccessed = 1903, // Not supported
// OrganizationDomainAdded = 2000, // Not supported
// OrganizationDomainRemoved = 2001, // Not supported
// OrganizationDomainVerified = 2002, // Not supported
// OrganizationDomainNotVerified = 2003, // Not supported
// SecretRetrieved = 2100, // Not supported
}
/// Local methods
impl Event {
pub fn new(event_type: i32, event_date: Option<NaiveDateTime>) -> Self {
let event_date = match event_date {
Some(d) => d,
None => Utc::now().naive_utc(),
};
Self {
uuid: EventId(crate::util::get_uuid()),
event_type,
user_uuid: None,
org_uuid: None,
cipher_uuid: None,
collection_uuid: None,
group_uuid: None,
org_user_uuid: None,
act_user_uuid: None,
device_type: None,
ip_address: None,
event_date,
policy_uuid: None,
provider_uuid: None,
provider_user_uuid: None,
provider_org_uuid: None,
}
}
pub fn to_json(&self) -> Value {
use crate::util::format_date;
json!({
"type": self.event_type,
"userId": self.user_uuid,
"organizationId": self.org_uuid,
"cipherId": self.cipher_uuid,
"collectionId": self.collection_uuid,
"groupId": self.group_uuid,
"organizationUserId": self.org_user_uuid,
"actingUserId": self.act_user_uuid,
"date": format_date(&self.event_date),
"deviceType": self.device_type,
"ipAddress": self.ip_address,
"policyId": self.policy_uuid,
"providerId": self.provider_uuid,
"providerUserId": self.provider_user_uuid,
"providerOrganizationId": self.provider_org_uuid,
// "installationId": null, // Not supported
})
}
}
/// Database methods
/// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs
impl Event {
pub const PAGE_SIZE: i64 = 30;
/// #############
/// Basic Queries
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
diesel::replace_into(event::table)
.values(EventDb::to_db(self))
.execute(conn)
.map_res("Error saving event")
}
postgresql {
diesel::insert_into(event::table)
.values(EventDb::to_db(self))
.on_conflict(event::uuid)
.do_update()
.set(EventDb::to_db(self))
.execute(conn)
.map_res("Error saving event")
}
}
}
pub async fn save_user_event(events: Vec<Event>, conn: &mut DbConn) -> EmptyResult {
// Special save function which is able to handle multiple events.
// SQLite doesn't support the DEFAULT argument, and does not support inserting multiple values at the same time.
// MySQL and PostgreSQL do.
// We also ignore duplicate if they ever will exists, else it could break the whole flow.
db_run! { conn:
// Unfortunately SQLite does not support inserting multiple records at the same time
// We loop through the events here and insert them one at a time.
sqlite {
for event in events {
diesel::insert_or_ignore_into(event::table)
.values(EventDb::to_db(&event))
.execute(conn)
.unwrap_or_default();
}
Ok(())
}
mysql {
let events: Vec<EventDb> = events.iter().map(EventDb::to_db).collect();
diesel::insert_or_ignore_into(event::table)
.values(&events)
.execute(conn)
.unwrap_or_default();
Ok(())
}
postgresql {
let events: Vec<EventDb> = events.iter().map(EventDb::to_db).collect();
diesel::insert_into(event::table)
.values(&events)
.on_conflict_do_nothing()
.execute(conn)
.unwrap_or_default();
Ok(())
}
}
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(event::table.filter(event::uuid.eq(self.uuid)))
.execute(conn)
.map_res("Error deleting event")
}}
}
/// ##############
/// Custom Queries
pub async fn find_by_organization_uuid(
org_uuid: &OrganizationId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
event::table
.filter(event::org_uuid.eq(org_uuid))
.filter(event::event_date.between(start, end))
.order_by(event::event_date.desc())
.limit(Self::PAGE_SIZE)
.load::<EventDb>(conn)
.expect("Error filtering events")
.from_db()
}}
}
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
event::table
.filter(event::org_uuid.eq(org_uuid))
.count()
.first::<i64>(conn)
.ok()
.unwrap_or(0)
}}
}
pub async fn find_by_org_and_member(
org_uuid: &OrganizationId,
member_uuid: &MembershipId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
event::table
.inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid)))
.filter(event::org_uuid.eq(org_uuid))
.filter(event::event_date.between(start, end))
.filter(event::user_uuid.eq(users_organizations::user_uuid.nullable()).or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())))
.select(event::all_columns)
.order_by(event::event_date.desc())
.limit(Self::PAGE_SIZE)
.load::<EventDb>(conn)
.expect("Error filtering events")
.from_db()
}}
}
pub async fn find_by_cipher_uuid(
cipher_uuid: &CipherId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
event::table
.filter(event::cipher_uuid.eq(cipher_uuid))
.filter(event::event_date.between(start, end))
.order_by(event::event_date.desc())
.limit(Self::PAGE_SIZE)
.load::<EventDb>(conn)
.expect("Error filtering events")
.from_db()
}}
}
pub async fn clean_events(conn: &mut DbConn) -> EmptyResult {
if let Some(days_to_retain) = CONFIG.events_days_retain() {
let dt = Utc::now().naive_utc() - TimeDelta::try_days(days_to_retain).unwrap();
db_run! { conn: {
diesel::delete(event::table.filter(event::event_date.lt(dt)))
.execute(conn)
.map_res("Error cleaning old events")
}}
} else {
Ok(())
}
}
}
#[derive(Clone, Debug, DieselNewType, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventId(String);