diff options
author | BlackDex <[email protected]> | 2022-11-20 19:15:45 +0100 |
---|---|---|
committer | Daniel GarcĂa <[email protected]> | 2022-12-01 22:38:59 +0100 |
commit | 7035700c8d2454b190ce860bb28b3098e73dcf65 (patch) | |
tree | 11266819455ff76bf574f48969fb080053179ac0 /src/api | |
parent | 0aa33a2cb48ee5dbf31a6f448effb41e7467e417 (diff) | |
download | vaultwarden-7035700c8d2454b190ce860bb28b3098e73dcf65.tar.gz vaultwarden-7035700c8d2454b190ce860bb28b3098e73dcf65.zip |
Add Organizational event logging feature
This PR adds event/audit logging support for organizations.
By default this feature is disabled, since it does log a lot and adds
extra database transactions.
All events are touched except a few, since we do not support those
features (yet), like SSO for example.
This feature is tested with multiple clients and all database types.
Fixes #229
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/admin.rs | 44 | ||||
-rw-r--r-- | src/api/core/accounts.rs | 25 | ||||
-rw-r--r-- | src/api/core/ciphers.rs | 334 | ||||
-rw-r--r-- | src/api/core/events.rs | 341 | ||||
-rw-r--r-- | src/api/core/mod.rs | 10 | ||||
-rw-r--r-- | src/api/core/organizations.rs | 467 | ||||
-rw-r--r-- | src/api/core/two_factor/authenticator.rs | 21 | ||||
-rw-r--r-- | src/api/core/two_factor/duo.rs | 31 | ||||
-rw-r--r-- | src/api/core/two_factor/email.rs | 37 | ||||
-rw-r--r-- | src/api/core/two_factor/mod.rs | 25 | ||||
-rw-r--r-- | src/api/core/two_factor/webauthn.rs | 39 | ||||
-rw-r--r-- | src/api/core/two_factor/yubikey.rs | 27 | ||||
-rw-r--r-- | src/api/identity.rs | 147 | ||||
-rw-r--r-- | src/api/mod.rs | 1 |
14 files changed, 1321 insertions, 228 deletions
diff --git a/src/api/admin.rs b/src/api/admin.rs index 1900db2b..656f63bf 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -13,7 +13,7 @@ use rocket::{ }; use crate::{ - api::{ApiResult, EmptyResult, JsonResult, NumberOrString}, + api::{core::log_event, ApiResult, EmptyResult, JsonResult, NumberOrString}, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, config::ConfigBuilder, db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, @@ -88,6 +88,8 @@ const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z"; const BASE_TEMPLATE: &str = "admin/base"; +const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000"; + fn admin_path() -> String { format!("{}{}", CONFIG.domain_path(), ADMIN_PATH) } @@ -354,9 +356,27 @@ async fn get_user_json(uuid: String, _token: AdminToken, mut conn: DbConn) -> Js } #[post("/users/<uuid>/delete")] -async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult { +async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn, ip: ClientIp) -> EmptyResult { let user = get_user_or_404(&uuid, &mut conn).await?; - user.delete(&mut conn).await + + // Get the user_org records before deleting the actual user + let user_orgs = UserOrganization::find_any_state_by_user(&uuid, &mut conn).await; + let res = user.delete(&mut conn).await; + + for user_org in user_orgs { + log_event( + EventType::OrganizationUserRemoved as i32, + &user_org.uuid, + user_org.org_uuid, + String::from(ACTING_ADMIN_USER), + 14, // Use UnknownBrowser type + &ip.ip, + &mut conn, + ) + .await; + } + + res } #[post("/users/<uuid>/deauth")] @@ -402,7 +422,12 @@ struct UserOrgTypeData { } #[post("/users/org_type", data = "<data>")] -async fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, mut conn: DbConn) -> EmptyResult { +async fn update_user_org_type( + data: Json<UserOrgTypeData>, + _token: AdminToken, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { let data: UserOrgTypeData = data.into_inner(); let mut user_to_edit = @@ -437,6 +462,17 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, m } } + log_event( + EventType::OrganizationUserUpdated as i32, + &user_to_edit.uuid, + data.org_uuid, + String::from(ACTING_ADMIN_USER), + 14, // Use UnknownBrowser type + &ip.ip, + &mut conn, + ) + .await; + user_to_edit.atype = new_type; user_to_edit.save(&mut conn).await } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 2cd500d1..3315fbce 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -3,8 +3,10 @@ use rocket::serde::json::Json; use serde_json::Value; use crate::{ - api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType}, - auth::{decode_delete, decode_invite, decode_verify_email, Headers}, + api::{ + core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType, + }, + auth::{decode_delete, decode_invite, decode_verify_email, ClientIp, Headers}, crypto, db::{models::*, DbConn}, mail, CONFIG, @@ -268,7 +270,12 @@ struct ChangePassData { } #[post("/accounts/password", data = "<data>")] -async fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn post_password( + data: JsonUpcase<ChangePassData>, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { let data: ChangePassData = data.into_inner().data; let mut user = headers.user; @@ -279,6 +286,8 @@ async fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, mut c user.password_hint = clean_password_hint(&data.MasterPasswordHint); enforce_password_hint_setting(&user.password_hint)?; + log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await; + user.set_password( &data.NewMasterPasswordHash, Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]), @@ -334,7 +343,13 @@ struct KeyData { } #[post("/accounts/key", data = "<data>")] -async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { +async fn post_rotatekey( + data: JsonUpcase<KeyData>, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> EmptyResult { let data: KeyData = data.into_inner().data; if !headers.user.check_valid_password(&data.MasterPasswordHash) { @@ -373,7 +388,7 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D // 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. - update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None) + update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None) .await? } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 39635efb..c8c741d4 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -10,8 +10,8 @@ use rocket::{ use serde_json::Value; use crate::{ - api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType}, - auth::Headers, + api::{self, core::log_event, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType}, + auth::{ClientIp, Headers}, crypto, db::{models::*, DbConn, DbPool}, CONFIG, @@ -247,9 +247,10 @@ async fn post_ciphers_admin( data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { - post_ciphers_create(data, headers, conn, nt).await + post_ciphers_create(data, headers, conn, ip, nt).await } /// Called when creating a new org-owned cipher, or cloning a cipher (whether @@ -260,6 +261,7 @@ async fn post_ciphers_create( data: JsonUpcase<ShareCipherData>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { let mut data: ShareCipherData = data.into_inner().data; @@ -287,12 +289,18 @@ async fn post_ciphers_create( // or otherwise), we can just ignore this field entirely. data.Cipher.LastKnownRevisionDate = None; - share_cipher_by_uuid(&cipher.uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&cipher.uuid, data, &headers, &mut conn, &ip, &nt).await } /// Called when creating a new user-owned cipher. #[post("/ciphers", data = "<data>")] -async fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { +async fn post_ciphers( + data: JsonUpcase<CipherData>, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> JsonResult { let mut data: CipherData = data.into_inner().data; // The web/browser clients set this field to null as expected, but the @@ -302,7 +310,7 @@ async fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, mut conn: data.LastKnownRevisionDate = None; let mut cipher = Cipher::new(data.Type, data.Name.clone()); - update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::CipherCreate).await?; + update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::CipherCreate).await?; Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) } @@ -329,12 +337,14 @@ async fn enforce_personal_ownership_policy( Ok(()) } +#[allow(clippy::too_many_arguments)] pub async fn update_cipher_from_data( cipher: &mut Cipher, data: CipherData, headers: &Headers, shared_to_collection: bool, conn: &mut DbConn, + ip: &ClientIp, nt: &Notify<'_>, ut: UpdateType, ) -> EmptyResult { @@ -356,6 +366,9 @@ pub async fn update_cipher_from_data( err!("Organization mismatch. Please resync the client before updating the cipher") } + // Check if this cipher is being transferred from a personal to an organization vault + let transfer_cipher = cipher.organization_uuid.is_none() && data.OrganizationId.is_some(); + if let Some(org_id) = data.OrganizationId { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await { None => err!("You don't have permission to add item to organization"), @@ -460,6 +473,26 @@ pub async fn update_cipher_from_data( cipher.set_favorite(data.Favorite, &headers.user.uuid, conn).await?; if ut != UpdateType::None { + // Only log events for organizational ciphers + if let Some(org_uuid) = &cipher.organization_uuid { + let event_type = match (&ut, transfer_cipher) { + (UpdateType::CipherCreate, true) => EventType::CipherCreated, + (UpdateType::CipherUpdate, true) => EventType::CipherShared, + (_, _) => EventType::CipherUpdated, + }; + + log_event( + event_type as i32, + &cipher.uuid, + String::from(org_uuid), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + conn, + ) + .await; + } + nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await).await; } @@ -488,6 +521,7 @@ async fn post_ciphers_import( data: JsonUpcase<ImportData>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { enforce_personal_ownership_policy(None, &headers, &mut conn).await?; @@ -516,7 +550,8 @@ async fn post_ciphers_import( cipher_data.FolderId = folder_uuid; let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); - update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None).await?; + update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None) + .await?; } let mut user = headers.user; @@ -532,9 +567,10 @@ async fn put_cipher_admin( data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { - put_cipher(uuid, data, headers, conn, nt).await + put_cipher(uuid, data, headers, conn, ip, nt).await } #[post("/ciphers/<uuid>/admin", data = "<data>")] @@ -543,9 +579,10 @@ async fn post_cipher_admin( data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { - post_cipher(uuid, data, headers, conn, nt).await + post_cipher(uuid, data, headers, conn, ip, nt).await } #[post("/ciphers/<uuid>", data = "<data>")] @@ -554,9 +591,10 @@ async fn post_cipher( data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { - put_cipher(uuid, data, headers, conn, nt).await + put_cipher(uuid, data, headers, conn, ip, nt).await } #[put("/ciphers/<uuid>", data = "<data>")] @@ -565,6 +603,7 @@ async fn put_cipher( data: JsonUpcase<CipherData>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { let data: CipherData = data.into_inner().data; @@ -583,7 +622,7 @@ async fn put_cipher( err!("Cipher is not write accessible") } - update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &nt, UpdateType::CipherUpdate).await?; + update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::CipherUpdate).await?; Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) } @@ -600,8 +639,9 @@ async fn put_collections_update( data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn).await + post_collections_admin(uuid, data, headers, conn, ip).await } #[post("/ciphers/<uuid>/collections", data = "<data>")] @@ -610,8 +650,9 @@ async fn post_collections_update( data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn).await + post_collections_admin(uuid, data, headers, conn, ip).await } #[put("/ciphers/<uuid>/collections-admin", data = "<data>")] @@ -620,8 +661,9 @@ async fn put_collections_admin( data: JsonUpcase<CollectionsAdminData>, headers: Headers, conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn).await + post_collections_admin(uuid, data, headers, conn, ip).await } #[post("/ciphers/<uuid>/collections-admin", data = "<data>")] @@ -630,6 +672,7 @@ async fn post_collections_admin( data: JsonUpcase<CollectionsAdminData>, headers: Headers, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { let data: CollectionsAdminData = data.into_inner().data; @@ -665,6 +708,17 @@ async fn post_collections_admin( } } + log_event( + EventType::CipherUpdatedCollections as i32, + &cipher.uuid, + cipher.organization_uuid.unwrap(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + Ok(()) } @@ -681,11 +735,12 @@ async fn post_cipher_share( data: JsonUpcase<ShareCipherData>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { let data: ShareCipherData = data.into_inner().data; - share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &ip, &nt).await } #[put("/ciphers/<uuid>/share", data = "<data>")] @@ -694,11 +749,12 @@ async fn put_cipher_share( data: JsonUpcase<ShareCipherData>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { let data: ShareCipherData = data.into_inner().data; - share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &ip, &nt).await } #[derive(Deserialize)] @@ -713,6 +769,7 @@ async fn put_cipher_share_selected( data: JsonUpcase<ShareSelectedCipherData>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { let mut data: ShareSelectedCipherData = data.into_inner().data; @@ -740,7 +797,7 @@ async fn put_cipher_share_selected( }; match shared_cipher_data.Cipher.Id.take() { - Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &mut conn, &nt).await?, + Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &mut conn, &ip, &nt).await?, None => err!("Request missing ids field"), }; } @@ -753,6 +810,7 @@ async fn share_cipher_by_uuid( data: ShareCipherData, headers: &Headers, conn: &mut DbConn, + ip: &ClientIp, nt: &Notify<'_>, ) -> JsonResult { let mut cipher = match Cipher::find_by_uuid(uuid, conn).await { @@ -768,37 +826,30 @@ async fn share_cipher_by_uuid( let mut shared_to_collection = false; - match data.Cipher.OrganizationId.clone() { - // If we don't get an organization ID, we don't do anything - // No error because this is used when using the Clone functionality - None => {} - Some(organization_uuid) => { - for uuid in &data.CollectionIds { - match Collection::find_by_uuid_and_org(uuid, &organization_uuid, conn).await { - None => err!("Invalid collection ID provided"), - Some(collection) => { - if collection.is_writable_by_user(&headers.user.uuid, conn).await { - CollectionCipher::save(&cipher.uuid, &collection.uuid, conn).await?; - shared_to_collection = true; - } else { - err!("No rights to modify the collection") - } + if let Some(organization_uuid) = &data.Cipher.OrganizationId { + for uuid in &data.CollectionIds { + match Collection::find_by_uuid_and_org(uuid, organization_uuid, conn).await { + None => err!("Invalid collection ID provided"), + Some(collection) => { + if collection.is_writable_by_user(&headers.user.uuid, conn).await { + CollectionCipher::save(&cipher.uuid, &collection.uuid, conn).await?; + shared_to_collection = true; + } else { + err!("No rights to modify the collection") } } } } }; - update_cipher_from_data( - &mut cipher, - data.Cipher, - headers, - shared_to_collection, - conn, - nt, - UpdateType::CipherUpdate, - ) - .await?; + // When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate. + let ut = if data.Cipher.LastKnownRevisionDate.is_some() { + UpdateType::CipherUpdate + } else { + UpdateType::CipherCreate + }; + + update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, ip, nt, ut).await?; Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await)) } @@ -893,6 +944,7 @@ async fn save_attachment( data: Form<UploadData<'_>>, headers: &Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> Result<(Cipher, DbConn), crate::error::Error> { let cipher = match Cipher::find_by_uuid(&cipher_uuid, &mut conn).await { @@ -1011,6 +1063,19 @@ async fn save_attachment( nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&mut conn).await).await; + if let Some(org_uuid) = &cipher.organization_uuid { + log_event( + EventType::CipherAttachmentCreated as i32, + &cipher.uuid, + String::from(org_uuid), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + } + Ok((cipher, conn)) } @@ -1025,6 +1090,7 @@ async fn post_attachment_v2_data( data: Form<UploadData<'_>>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { let attachment = match Attachment::find_by_id(&attachment_id, &mut conn).await { @@ -1033,7 +1099,7 @@ async fn post_attachment_v2_data( None => err!("Attachment doesn't exist"), }; - save_attachment(attachment, uuid, data, &headers, conn, nt).await?; + save_attachment(attachment, uuid, data, &headers, conn, ip, nt).await?; Ok(()) } @@ -1045,13 +1111,14 @@ async fn post_attachment( data: Form<UploadData<'_>>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { // Setting this as None signifies to save_attachment() that it should create // the attachment database record as well as saving the data to disk. let attachment = None; - let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, nt).await?; + let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, ip, nt).await?; Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) } @@ -1062,9 +1129,10 @@ async fn post_attachment_admin( data: Form<UploadData<'_>>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { - post_attachment(uuid, data, headers, conn, nt).await + post_attachment(uuid, data, headers, conn, ip, nt).await } #[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")] @@ -1074,10 +1142,11 @@ async fn post_attachment_share( data: Form<UploadData<'_>>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await?; - post_attachment(uuid, data, headers, conn, nt).await + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &ip, &nt).await?; + post_attachment(uuid, data, headers, conn, ip, nt).await } #[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")] @@ -1086,9 +1155,10 @@ async fn delete_attachment_post_admin( attachment_id: String, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - delete_attachment(uuid, attachment_id, headers, conn, nt).await + delete_attachment(uuid, attachment_id, headers, conn, ip, nt).await } #[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")] @@ -1097,9 +1167,10 @@ async fn delete_attachment_post( attachment_id: String, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - delete_attachment(uuid, attachment_id, headers, conn, nt).await + delete_attachment(uuid, attachment_id, headers, conn, ip, nt).await } #[delete("/ciphers/<uuid>/attachment/<attachment_id>")] @@ -1108,9 +1179,10 @@ async fn delete_attachment( attachment_id: String, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &ip, &nt).await } #[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")] @@ -1119,39 +1191,70 @@ async fn delete_attachment_admin( attachment_id: String, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &ip, &nt).await } #[post("/ciphers/<uuid>/delete")] -async fn delete_cipher_post(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_post( + uuid: String, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &ip, &nt).await // permanent delete } #[post("/ciphers/<uuid>/delete-admin")] -async fn delete_cipher_post_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_post_admin( + uuid: String, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &ip, &nt).await // permanent delete } #[put("/ciphers/<uuid>/delete")] -async fn delete_cipher_put(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await +async fn delete_cipher_put( + uuid: String, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &ip, &nt).await // soft delete } #[put("/ciphers/<uuid>/delete-admin")] -async fn delete_cipher_put_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await +async fn delete_cipher_put_admin( + uuid: String, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &ip, &nt).await } #[delete("/ciphers/<uuid>")] -async fn delete_cipher(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher(uuid: String, headers: Headers, mut conn: DbConn, ip: ClientIp, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &ip, &nt).await // permanent delete } #[delete("/ciphers/<uuid>/admin")] -async fn delete_cipher_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_admin( + uuid: String, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &ip, &nt).await // permanent delete } #[delete("/ciphers", data = "<data>")] @@ -1159,9 +1262,10 @@ async fn delete_cipher_selected( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, false, nt).await + _delete_multiple_ciphers(data, headers, conn, false, ip, nt).await // permanent delete } #[post("/ciphers/delete", data = "<data>")] @@ -1169,9 +1273,10 @@ async fn delete_cipher_selected_post( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, false, nt).await + _delete_multiple_ciphers(data, headers, conn, false, ip, nt).await // permanent delete } #[put("/ciphers/delete", data = "<data>")] @@ -1179,9 +1284,10 @@ async fn delete_cipher_selected_put( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - _delete_multiple_ciphers(data, headers, conn, true, nt).await // soft delete + _delete_multiple_ciphers(data, headers, conn, true, ip, nt).await // soft delete } #[delete("/ciphers/admin", data = "<data>")] @@ -1189,9 +1295,10 @@ async fn delete_cipher_selected_admin( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - delete_cipher_selected(data, headers, conn, nt).await + _delete_multiple_ciphers(data, headers, conn, false, ip, nt).await // permanent delete } #[post("/ciphers/delete-admin", data = "<data>")] @@ -1199,9 +1306,10 @@ async fn delete_cipher_selected_post_admin( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - delete_cipher_selected_post(data, headers, conn, nt).await + _delete_multiple_ciphers(data, headers, conn, false, ip, nt).await // permanent delete } #[put("/ciphers/delete-admin", data = "<data>")] @@ -1209,19 +1317,32 @@ async fn delete_cipher_selected_put_admin( data: JsonUpcase<Value>, headers: Headers, conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { - delete_cipher_selected_put(data, headers, conn, nt).await + _delete_multiple_ciphers(data, headers, conn, true, ip, nt).await // soft delete } #[put("/ciphers/<uuid>/restore")] -async fn restore_cipher_put(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await +async fn restore_cipher_put( + uuid: String, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> JsonResult { + _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &ip, &nt).await } #[put("/ciphers/<uuid>/restore-admin")] -async fn restore_cipher_put_admin(uuid: String, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await +async fn restore_cipher_put_admin( + uuid: String, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> JsonResult { + _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &ip, &nt).await } #[put("/ciphers/restore", data = "<data>")] @@ -1229,9 +1350,10 @@ async fn restore_cipher_selected( data: JsonUpcase<Value>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { - _restore_multiple_ciphers(data, &headers, &mut conn, &nt).await + _restore_multiple_ciphers(data, &headers, &mut conn, ip, &nt).await } #[derive(Deserialize)] @@ -1303,6 +1425,7 @@ async fn delete_all( data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { let data: PasswordData = data.into_inner().data; @@ -1323,6 +1446,18 @@ async fn delete_all( if user_org.atype == UserOrgType::Owner { Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?; nt.send_user_update(UpdateType::Vault, &user).await; + + log_event( + EventType::OrganizationPurgedVault as i32, + &org_data.org_id, + org_data.org_id.clone(), + user.uuid, + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + Ok(()) } else { err!("You don't have permission to purge the organization vault"); @@ -1354,6 +1489,7 @@ async fn _delete_cipher_by_uuid( headers: &Headers, conn: &mut DbConn, soft_delete: bool, + ip: &ClientIp, nt: &Notify<'_>, ) -> EmptyResult { let mut cipher = match Cipher::find_by_uuid(uuid, conn).await { @@ -1374,6 +1510,16 @@ async fn _delete_cipher_by_uuid( nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(conn).await).await; } + if let Some(org_uuid) = cipher.organization_uuid { + let event_type = match soft_delete { + true => EventType::CipherSoftDeleted as i32, + false => EventType::CipherDeleted as i32, + }; + + log_event(event_type, &cipher.uuid, org_uuid, headers.user.uuid.clone(), headers.device.atype, &ip.ip, conn) + .await; + } + Ok(()) } @@ -1382,6 +1528,7 @@ async fn _delete_multiple_ciphers( headers: Headers, mut conn: DbConn, soft_delete: bool, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { let data: Value = data.into_inner().data; @@ -1395,7 +1542,7 @@ async fn _delete_multiple_ciphers( }; for uuid in uuids { - if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &mut conn, soft_delete, &nt).await { + if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &mut conn, soft_delete, &ip, &nt).await { return error; }; } @@ -1403,7 +1550,13 @@ async fn _delete_multiple_ciphers( Ok(()) } -async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>) -> JsonResult { +async fn _restore_cipher_by_uuid( + uuid: &str, + headers: &Headers, + conn: &mut DbConn, + ip: &ClientIp, + nt: &Notify<'_>, +) -> JsonResult { let mut cipher = match Cipher::find_by_uuid(uuid, conn).await { Some(cipher) => cipher, None => err!("Cipher doesn't exist"), @@ -1417,6 +1570,19 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon cipher.save(conn).await?; nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(conn).await).await; + if let Some(org_uuid) = &cipher.organization_uuid { + log_event( + EventType::CipherRestored as i32, + &cipher.uuid.clone(), + String::from(org_uuid), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + conn, + ) + .await; + } + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await)) } @@ -1424,6 +1590,7 @@ async fn _restore_multiple_ciphers( data: JsonUpcase<Value>, headers: &Headers, conn: &mut DbConn, + ip: ClientIp, nt: &Notify<'_>, ) -> JsonResult { let data: Value = data.into_inner().data; @@ -1438,7 +1605,7 @@ async fn _restore_multiple_ciphers( let mut ciphers: Vec<Value> = Vec::new(); for uuid in uuids { - match _restore_cipher_by_uuid(uuid, headers, conn, nt).await { + match _restore_cipher_by_uuid(uuid, headers, conn, &ip, nt).await { Ok(json) => ciphers.push(json.into_inner()), err => return err, } @@ -1456,6 +1623,7 @@ async fn _delete_cipher_attachment_by_id( attachment_id: &str, headers: &Headers, conn: &mut DbConn, + ip: &ClientIp, nt: &Notify<'_>, ) -> EmptyResult { let attachment = match Attachment::find_by_id(attachment_id, conn).await { @@ -1479,6 +1647,18 @@ async fn _delete_cipher_attachment_by_id( // Delete attachment attachment.delete(conn).await?; nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(conn).await).await; + if let Some(org_uuid) = cipher.organization_uuid { + log_event( + EventType::CipherAttachmentDeleted as i32, + &cipher.uuid, + org_uuid, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + conn, + ) + .await; + } Ok(()) } diff --git a/src/api/core/events.rs b/src/api/core/events.rs new file mode 100644 index 00000000..43102712 --- /dev/null +++ b/src/api/core/events.rs @@ -0,0 +1,341 @@ +use std::net::IpAddr; + +use chrono::NaiveDateTime; +use rocket::{form::FromForm, serde::json::Json, Route}; +use serde_json::Value; + +use crate::{ + api::{EmptyResult, JsonResult, JsonUpcaseVec}, + auth::{AdminHeaders, ClientIp, Headers}, + db::{ + models::{Cipher, Event, UserOrganization}, + DbConn, DbPool, + }, + util::parse_date, + CONFIG, +}; + +/// ############################################################################################################### +/// /api routes +pub fn routes() -> Vec<Route> { + routes![get_org_events, get_cipher_events, get_user_events,] +} + +#[derive(FromForm)] +#[allow(non_snake_case)] +struct EventRange { + start: String, + end: String, + #[field(name = "continuationToken")] + continuation_token: Option<String>, +} + +// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41 +#[get("/organizations/<org_id>/events?<data..>")] +async fn get_org_events(org_id: String, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { + // 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 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) + }; + + Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &mut conn) + .await + .iter() + .map(|e| e.to_json()) + .collect() + }; + + Ok(Json(json!({ + "Data": events_json, + "Object": "list", + "ContinuationToken": get_continuation_token(&events_json), + }))) +} + +#[get("/ciphers/<cipher_id>/events?<data..>")] +async fn get_cipher_events(cipher_id: String, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult { + // 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 mut events_json = Vec::with_capacity(0); + if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut 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, &mut conn) + .await + .iter() + .map(|e| e.to_json()) + .collect() + } + events_json + }; + + Ok(Json(json!({ + "Data": events_json, + "Object": "list", + "ContinuationToken": get_continuation_token(&events_json), + }))) +} + +#[get("/organizations/<org_id>/users/<user_org_id>/events?<data..>")] +async fn get_user_events( + org_id: String, + user_org_id: String, + data: EventRange, + _headers: AdminHeaders, + mut conn: DbConn, +) -> JsonResult { + // 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 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) + }; + + Event::find_by_org_and_user_org(&org_id, &user_org_id, &start_date, &end_date, &mut conn) + .await + .iter() + .map(|e| e.to_json()) + .collect() + }; + + Ok(Json(json!({ + "Data": events_json, + "Object": "list", + "ContinuationToken": get_continuation_token(&events_json), + }))) +} + +fn get_continuation_token(events_json: &Vec<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 { + if let Some(last_event) = events_json.last() { + last_event["date"].as_str() + } else { + None + } + } else { + None + } +} + +/// ############################################################################################################### +/// /events routes +pub fn main_routes() -> Vec<Route> { + routes![post_events_collect,] +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct EventCollection { + // Mandatory + Type: i32, + Date: String, + + // Optional + CipherId: Option<String>, + OrganizationId: Option<String>, +} + +// Upstream: +// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Events/Controllers/CollectController.cs +// https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs +#[post("/collect", format = "application/json", data = "<data>")] +async fn post_events_collect( + data: JsonUpcaseVec<EventCollection>, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { + if !CONFIG.org_events_enabled() { + return Ok(()); + } + + for event in data.iter().map(|d| &d.data) { + let event_date = parse_date(&event.Date); + match event.Type { + 1000..=1099 => { + _log_user_event( + event.Type, + &headers.user.uuid, + headers.device.atype, + Some(event_date), + &ip.ip, + &mut conn, + ) + .await; + } + 1600..=1699 => { + if let Some(org_uuid) = &event.OrganizationId { + _log_event( + event.Type, + org_uuid, + String::from(org_uuid), + &headers.user.uuid, + headers.device.atype, + Some(event_date), + &ip.ip, + &mut conn, + ) + .await; + } + } + _ => { + if let Some(cipher_uuid) = &event.CipherId { + if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await { + if let Some(org_uuid) = cipher.organization_uuid { + _log_event( + event.Type, + cipher_uuid, + org_uuid, + &headers.user.uuid, + headers.device.atype, + Some(event_date), + &ip.ip, + &mut conn, + ) + .await; + } + } + } + } + } + } + Ok(()) +} + +pub async fn log_user_event(event_type: i32, user_uuid: &str, device_type: i32, ip: &IpAddr, conn: &mut DbConn) { + if !CONFIG.org_events_enabled() { + return; + } + _log_user_event(event_type, user_uuid, device_type, None, ip, conn).await; +} + +async fn _log_user_event( + event_type: i32, + user_uuid: &str, + device_type: i32, + event_date: Option<NaiveDateTime>, + ip: &IpAddr, + conn: &mut DbConn, +) { + let orgs = UserOrganization::get_org_uuid_by_user(user_uuid, conn).await; + let mut events: Vec<Event> = Vec::with_capacity(orgs.len() + 1); // We need an event per org and one without an org + + // Upstream saves the event also without any org_uuid. + let mut event = Event::new(event_type, event_date); + event.user_uuid = Some(String::from(user_uuid)); + event.act_user_uuid = Some(String::from(user_uuid)); + event.device_type = Some(device_type); + event.ip_address = Some(ip.to_string()); + events.push(event); + + // For each org a user is a member of store these events per org + for org_uuid in orgs { + let mut event = Event::new(event_type, event_date); + event.user_uuid = Some(String::from(user_uuid)); + event.org_uuid = Some(org_uuid); + event.act_user_uuid = Some(String::from(user_uuid)); + event.device_type = Some(device_type); + event.ip_address = Some(ip.to_string()); + events.push(event); + } + + Event::save_user_event(events, conn).await.unwrap_or(()); +} + +pub async fn log_event( + event_type: i32, + source_uuid: &str, + org_uuid: String, + act_user_uuid: String, + device_type: i32, + ip: &IpAddr, + conn: &mut DbConn, +) { + if !CONFIG.org_events_enabled() { + return; + } + _log_event(event_type, source_uuid, org_uuid, &act_user_uuid, device_type, None, ip, conn).await; +} + +#[allow(clippy::too_many_arguments)] +async fn _log_event( + event_type: i32, + source_uuid: &str, + org_uuid: String, + act_user_uuid: &str, + device_type: i32, + event_date: Option<NaiveDateTime>, + ip: &IpAddr, + conn: &mut DbConn, +) { + // Create a new empty event + let mut event = Event::new(event_type, event_date); + match event_type { + // 1000..=1099 Are user events, they need to be logged via log_user_event() + // Collection Events + 1100..=1199 => { + event.cipher_uuid = Some(String::from(source_uuid)); + } + // Collection Events + 1300..=1399 => { + event.collection_uuid = Some(String::from(source_uuid)); + } + // Group Events + 1400..=1499 => { + event.group_uuid = Some(String::from(source_uuid)); + } + // Org User Events + 1500..=1599 => { + event.org_user_uuid = Some(String::from(source_uuid)); + } + // 1600..=1699 Are organizational events, and they do not need the source_uuid + // Policy Events + 1700..=1799 => { + event.policy_uuid = Some(String::from(source_uuid)); + } + // Ignore others + _ => {} + } + + event.org_uuid = Some(org_uuid); + event.act_user_uuid = Some(String::from(act_user_uuid)); + event.device_type = Some(device_type); + event.ip_address = Some(ip.to_string()); + event.save(conn).await.unwrap_or(()); +} + +pub async fn event_cleanup_job(pool: DbPool) { + debug!("Start events cleanup job"); + if CONFIG.events_days_retain().is_none() { + debug!("events_days_retain is not configured, abort"); + return; + } + + if let Ok(mut conn) = pool.get().await { + Event::clean_events(&mut conn).await.ok(); + } else { + error!("Failed to get DB connection while trying to cleanup the events table") + } +} diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 0df9a9dc..885fae81 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -1,6 +1,7 @@ pub mod accounts; mod ciphers; mod emergency_access; +mod events; mod folders; mod organizations; mod sends; @@ -9,6 +10,7 @@ pub mod two_factor; pub use ciphers::purge_trashed_ciphers; pub use ciphers::{CipherSyncData, CipherSyncType}; pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job}; +pub use events::{event_cleanup_job, log_event, log_user_event}; pub use sends::purge_sends; pub use two_factor::send_incomplete_2fa_notifications; @@ -22,6 +24,7 @@ pub fn routes() -> Vec<Route> { routes.append(&mut accounts::routes()); routes.append(&mut ciphers::routes()); routes.append(&mut emergency_access::routes()); + routes.append(&mut events::routes()); routes.append(&mut folders::routes()); routes.append(&mut organizations::routes()); routes.append(&mut two_factor::routes()); @@ -34,6 +37,13 @@ pub fn routes() -> Vec<Route> { routes } +pub fn events_routes() -> Vec<Route> { + let mut routes = Vec::new(); + routes.append(&mut events::main_routes()); + + routes +} + // // Move this somewhere else // diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 744b0a12..57a982f9 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -5,11 +5,11 @@ use serde_json::Value; use crate::{ api::{ - core::{CipherSyncData, CipherSyncType}, + core::{log_event, CipherSyncData, CipherSyncType}, ApiResult, EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordData, UpdateType, }, - auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, + auth::{decode_invite, AdminHeaders, ClientIp, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, db::{models::*, DbConn}, error::Error, mail, @@ -203,7 +203,7 @@ async fn post_delete_organization( } #[post("/organizations/<org_id>/leave")] -async fn leave_organization(org_id: String, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn leave_organization(org_id: String, headers: Headers, mut conn: DbConn, ip: ClientIp) -> EmptyResult { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { None => err!("User not part of organization"), Some(user_org) => { @@ -213,6 +213,17 @@ async fn leave_organization(org_id: String, headers: Headers, mut conn: DbConn) err!("The last owner can't leave") } + log_event( + EventType::OrganizationUserRemoved as i32, + &user_org.uuid, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + user_org.delete(&mut conn).await } } @@ -232,16 +243,18 @@ async fn put_organization( headers: OwnerHeaders, data: JsonUpcase<OrganizationUpdateData>, conn: DbConn, + ip: ClientIp, ) -> JsonResult { - post_organization(org_id, headers, data, conn).await + post_organization(org_id, headers, data, conn, ip).await } #[post("/organizations/<org_id>", data = "<data>")] async fn post_organization( org_id: String, - _headers: OwnerHeaders, + headers: OwnerHeaders, data: JsonUpcase<OrganizationUpdateData>, mut conn: DbConn, + ip: ClientIp, ) -> JsonResult { let data: OrganizationUpdateData = data.into_inner().data; @@ -254,6 +267,18 @@ async fn post_organization( org.billing_email = data.BillingEmail; org.save(&mut conn).await?; + + log_event( + EventType::OrganizationUpdated as i32, + &org_id, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + Ok(Json(org.to_json())) } @@ -290,6 +315,7 @@ async fn post_organization_collections( headers: ManagerHeadersLoose, data: JsonUpcase<NewCollectionData>, mut conn: DbConn, + ip: ClientIp, ) -> JsonResult { let data: NewCollectionData = data.into_inner().data; @@ -307,6 +333,17 @@ async fn post_organization_collections( let collection = Collection::new(org.uuid, data.Name); collection.save(&mut conn).await?; + log_event( + EventType::CollectionCreated as i32, + &collection.uuid, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + for group in data.Groups { CollectionGroup::new(collection.uuid.clone(), group.Id, group.ReadOnly, group.HidePasswords) .save(&mut conn) @@ -330,17 +367,19 @@ async fn put_organization_collection_update( headers: ManagerHeaders, data: JsonUpcase<NewCollectionData>, conn: DbConn, + ip: ClientIp, ) -> JsonResult { - post_organization_collection_update(org_id, col_id, headers, data, conn).await + post_organization_collection_update(org_id, col_id, headers, data, conn, ip).await } #[post("/organizations/<org_id>/collections/<col_id>", data = "<data>")] async fn post_organization_collection_update( org_id: String, col_id: String, - _headers: ManagerHeaders, + headers: ManagerHeaders, data: JsonUpcase<NewCollectionData>, mut conn: DbConn, + ip: ClientIp, ) -> JsonResult { let data: NewCollectionData = data.into_inner().data; @@ -361,6 +400,17 @@ async fn post_organization_collection_update( collection.name = data.Name; collection.save(&mut conn).await?; + log_event( + EventType::CollectionUpdated as i32, + &collection.uuid, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?; for group in data.Groups { @@ -415,13 +465,24 @@ async fn post_organization_collection_delete_user( async fn delete_organization_collection( org_id: String, col_id: String, - _headers: ManagerHeaders, + headers: ManagerHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { match Collection::find_by_uuid(&col_id, &mut conn).await { None => err!("Collection not found"), Some(collection) => { if collection.org_uuid == org_id { + log_event( + EventType::CollectionDeleted as i32, + &collection.uuid, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; collection.delete(&mut conn).await } else { err!("Collection and Organization id do not match") @@ -444,8 +505,9 @@ async fn post_organization_collection_delete( headers: ManagerHeaders, _data: JsonUpcase<DeleteCollectionData>, conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - delete_organization_collection(org_id, col_id, headers, conn).await + delete_organization_collection(org_id, col_id, headers, conn, ip).await } #[get("/organizations/<org_id>/collections/<coll_id>/details")] @@ -632,6 +694,7 @@ async fn send_invite( data: JsonUpcase<InviteData>, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { let data: InviteData = data.into_inner().data; @@ -700,6 +763,17 @@ async fn send_invite( new_user.save(&mut conn).await?; + log_event( + EventType::OrganizationUserInvited as i32, + &new_user.uuid, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + if CONFIG.mail_enabled() { let org_name = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(org) => org.name, @@ -882,6 +956,7 @@ async fn bulk_confirm_invite( data: JsonUpcase<Value>, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> Json<Value> { let data = data.into_inner().data; @@ -891,7 +966,7 @@ async fn bulk_confirm_invite( for invite in keys { let org_user_id = invite["Id"].as_str().unwrap_or_default(); let user_key = invite["Key"].as_str().unwrap_or_default(); - let err_msg = match _confirm_invite(&org_id, org_user_id, user_key, &headers, &mut conn).await { + let err_msg = match _confirm_invite(&org_id, org_user_id, user_key, &headers, &mut conn, &ip).await { Ok(_) => String::new(), Err(e) => format!("{:?}", e), }; @@ -922,10 +997,11 @@ async fn confirm_invite( data: JsonUpcase<Value>, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { let data = data.into_inner().data; let user_key = data["Key"].as_str().unwrap_or_default(); - _confirm_invite(&org_id, &org_user_id, user_key, &headers, &mut conn).await + _confirm_invite(&org_id, &org_user_id, user_key, &headers, &mut conn, &ip).await } async fn _confirm_invite( @@ -934,6 +1010,7 @@ async fn _confirm_invite( key: &str, headers: &AdminHeaders, conn: &mut DbConn, + ip: &ClientIp, ) -> EmptyResult { if key.is_empty() || org_user_id.is_empty() { err!("Key or UserId is not set, unable to process request"); @@ -969,6 +1046,17 @@ async fn _confirm_invite( user_to_confirm.status = UserOrgStatus::Confirmed as i32; user_to_confirm.akey = key.to_string(); + log_event( + EventType::OrganizationUserConfirmed as i32, + &user_to_confirm.uuid, + String::from(org_id), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + conn, + ) + .await; + if CONFIG.mail_enabled() { let org_name = match Organization::find_by_uuid(org_id, conn).await { Some(org) => org.name, @@ -1009,8 +1097,9 @@ async fn put_organization_user( data: JsonUpcase<EditUserData>, headers: AdminHeaders, conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - edit_user(org_id, org_user_id, data, headers, conn).await + edit_user(org_id, org_user_id, data, headers, conn, ip).await } #[post("/organizations/<org_id>/users/<org_user_id>", data = "<data>", rank = 1)] @@ -1020,6 +1109,7 @@ async fn edit_user( data: JsonUpcase<EditUserData>, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { let data: EditUserData = data.into_inner().data; @@ -1095,6 +1185,17 @@ async fn edit_user( } } + log_event( + EventType::OrganizationUserUpdated as i32, + &user_to_edit.uuid, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + user_to_edit.save(&mut conn).await } @@ -1104,12 +1205,13 @@ async fn bulk_delete_user( data: JsonUpcase<OrgBulkIds>, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> Json<Value> { let data: OrgBulkIds = data.into_inner().data; let mut bulk_response = Vec::new(); for org_user_id in data.Ids { - let err_msg = match _delete_user(&org_id, &org_user_id, &headers, &mut conn).await { + let err_msg = match _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await { Ok(_) => String::new(), Err(e) => format!("{:?}", e), }; @@ -1131,11 +1233,34 @@ async fn bulk_delete_user( } #[delete("/organizations/<org_id>/users/<org_user_id>")] -async fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - _delete_user(&org_id, &org_user_id, &headers, &mut conn).await +async fn delete_user( + org_id: String, + org_user_id: String, + headers: AdminHeaders, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { + _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await +} + +#[post("/organizations/<org_id>/users/<org_user_id>/delete")] +async fn post_delete_user( + org_id: String, + org_user_id: String, + headers: AdminHeaders, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { + _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } -async fn _delete_user(org_id: &str, org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult { +async fn _delete_user( + org_id: &str, + org_user_id: &str, + headers: &AdminHeaders, + conn: &mut DbConn, + ip: &ClientIp, +) -> EmptyResult { let user_to_delete = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { Some(user) => user, None => err!("User to delete isn't member of the organization"), @@ -1152,12 +1277,18 @@ async fn _delete_user(org_id: &str, org_user_id: &str, headers: &AdminHeaders, c } } - user_to_delete.delete(conn).await -} + log_event( + EventType::OrganizationUserRemoved as i32, + &user_to_delete.uuid, + String::from(org_id), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + conn, + ) + .await; -#[post("/organizations/<org_id>/users/<org_user_id>/delete")] -async fn post_delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult { - delete_user(org_id, org_user_id, headers, conn).await + user_to_delete.delete(conn).await } #[post("/organizations/<org_id>/users/public-keys", data = "<data>")] @@ -1223,6 +1354,7 @@ async fn post_org_import( data: JsonUpcase<ImportData>, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, nt: Notify<'_>, ) -> EmptyResult { let data: ImportData = data.into_inner().data; @@ -1249,7 +1381,9 @@ async fn post_org_import( let mut ciphers = Vec::new(); for cipher_data in data.Ciphers { let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); - update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None).await.ok(); + update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None) + .await + .ok(); ciphers.push(cipher); } @@ -1333,8 +1467,9 @@ async fn put_policy( org_id: String, pol_type: i32, data: Json<PolicyData>, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> JsonResult { let data: PolicyData = data.into_inner(); @@ -1360,6 +1495,18 @@ async fn put_policy( mail::send_2fa_removed_from_org(&user.email, &org.name).await?; } + + log_event( + EventType::OrganizationUserRemoved as i32, + &member.uuid, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + member.delete(&mut conn).await?; } } @@ -1382,6 +1529,18 @@ async fn put_policy( mail::send_single_org_removed_from_org(&user.email, &org.name).await?; } + + log_event( + EventType::OrganizationUserRemoved as i32, + &member.uuid, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + member.delete(&mut conn).await?; } } @@ -1389,13 +1548,24 @@ async fn put_policy( let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await { Some(p) => p, - None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), + None => OrgPolicy::new(org_id.clone(), pol_type_enum, "{}".to_string()), }; policy.enabled = data.enabled; policy.data = serde_json::to_string(&data.data)?; policy.save(&mut conn).await?; + log_event( + EventType::PolicyUpdated as i32, + &policy.uuid, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + Ok(Json(policy.to_json())) } @@ -1467,7 +1637,13 @@ struct OrgImportData { } #[post("/organizations/<org_id>/import", data = "<data>")] -async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn import( + org_id: String, + data: JsonUpcase<OrgImportData>, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { let data = data.into_inner().data; // TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way @@ -1487,6 +1663,17 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header // If user is marked for deletion and it exists, delete it if let Some(user_org) = UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await { + log_event( + EventType::OrganizationUserRemoved as i32, + &user_org.uuid, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + user_org.delete(&mut conn).await?; } @@ -1506,6 +1693,17 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header new_org_user.save(&mut conn).await?; + log_event( + EventType::OrganizationUserInvited as i32, + &new_org_user.uuid, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + if CONFIG.mail_enabled() { let org_name = match Organization::find_by_uuid(&org_id, &mut conn).await { Some(org) => org.name, @@ -1531,6 +1729,17 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User, &mut conn).await { if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.email) { if !data.Users.iter().any(|u| u.Email == user_email) { + log_event( + EventType::OrganizationUserRemoved as i32, + &user_org.uuid, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + user_org.delete(&mut conn).await?; } } @@ -1547,8 +1756,9 @@ async fn deactivate_organization_user( org_user_id: String, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn).await + _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } // Pre web-vault v2022.9.x endpoint @@ -1558,8 +1768,9 @@ async fn bulk_deactivate_organization_user( data: JsonUpcase<Value>, headers: AdminHeaders, conn: DbConn, + ip: ClientIp, ) -> Json<Value> { - bulk_revoke_organization_user(org_id, data, headers, conn).await + bulk_revoke_organization_user(org_id, data, headers, conn, ip).await } #[put("/organizations/<org_id>/users/<org_user_id>/revoke")] @@ -1568,8 +1779,9 @@ async fn revoke_organization_user( org_user_id: String, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn).await + _revoke_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } #[put("/organizations/<org_id>/users/revoke", data = "<data>")] @@ -1578,6 +1790,7 @@ async fn bulk_revoke_organization_user( data: JsonUpcase<Value>, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> Json<Value> { let data = data.into_inner().data; @@ -1586,7 +1799,7 @@ async fn bulk_revoke_organization_user( Some(org_users) => { for org_user_id in org_users { let org_user_id = org_user_id.as_str().unwrap_or_default(); - let err_msg = match _revoke_organization_user(&org_id, org_user_id, &headers, &mut conn).await { + let err_msg = match _revoke_organization_user(&org_id, org_user_id, &headers, &mut conn, &ip).await { Ok(_) => String::new(), Err(e) => format!("{:?}", e), }; @@ -1615,6 +1828,7 @@ async fn _revoke_organization_user( org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn, + ip: &ClientIp, ) -> EmptyResult { match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { Some(mut user_org) if user_org.status > UserOrgStatus::Revoked as i32 => { @@ -1632,6 +1846,17 @@ async fn _revoke_organization_user( user_org.revoke(); user_org.save(conn).await?; + + log_event( + EventType::OrganizationUserRevoked as i32, + &user_org.uuid, + org_id.to_string(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + conn, + ) + .await; } Some(_) => err!("User is already revoked"), None => err!("User not found in organization"), @@ -1646,8 +1871,9 @@ async fn activate_organization_user( org_user_id: String, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn).await + _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } // Pre web-vault v2022.9.x endpoint @@ -1657,8 +1883,9 @@ async fn bulk_activate_organization_user( data: JsonUpcase<Value>, headers: AdminHeaders, conn: DbConn, + ip: ClientIp, ) -> Json<Value> { - bulk_restore_organization_user(org_id, data, headers, conn).await + bulk_restore_organization_user(org_id, data, headers, conn, ip).await } #[put("/organizations/<org_id>/users/<org_user_id>/restore")] @@ -1667,8 +1894,9 @@ async fn restore_organization_user( org_user_id: String, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn).await + _restore_organization_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await } #[put("/organizations/<org_id>/users/restore", data = "<data>")] @@ -1677,6 +1905,7 @@ async fn bulk_restore_organization_user( data: JsonUpcase<Value>, headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> Json<Value> { let data = data.into_inner().data; @@ -1685,7 +1914,7 @@ async fn bulk_restore_organization_user( Some(org_users) => { for org_user_id in org_users { let org_user_id = org_user_id.as_str().unwrap_or_default(); - let err_msg = match _restore_organization_user(&org_id, org_user_id, &headers, &mut conn).await { + let err_msg = match _restore_organization_user(&org_id, org_user_id, &headers, &mut conn, &ip).await { Ok(_) => String::new(), Err(e) => format!("{:?}", e), }; @@ -1714,6 +1943,7 @@ async fn _restore_organization_user( org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn, + ip: &ClientIp, ) -> EmptyResult { match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { Some(mut user_org) if user_org.status < UserOrgStatus::Accepted as i32 => { @@ -1740,6 +1970,17 @@ async fn _restore_organization_user( user_org.restore(); user_org.save(conn).await?; + + log_event( + EventType::OrganizationUserRestored as i32, + &user_org.uuid, + org_id.to_string(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + conn, + ) + .await; } Some(_) => err!("User is already active"), None => err!("User not found in organization"), @@ -1828,37 +2069,51 @@ impl SelectionReadOnly { } } -#[post("/organizations/<_org_id>/groups/<group_id>", data = "<data>")] +#[post("/organizations/<org_id>/groups/<group_id>", data = "<data>")] async fn post_group( - _org_id: String, + org_id: String, group_id: String, data: JsonUpcase<GroupRequest>, - _headers: AdminHeaders, + headers: AdminHeaders, conn: DbConn, + ip: ClientIp, ) -> JsonResult { - put_group(_org_id, group_id, data, _headers, conn).await + put_group(org_id, group_id, data, headers, conn, ip).await } #[post("/organizations/<org_id>/groups", data = "<data>")] async fn post_groups( org_id: String, - _headers: AdminHeaders, + headers: AdminHeaders, data: JsonUpcase<GroupRequest>, mut conn: DbConn, + ip: ClientIp, ) -> JsonResult { let group_request = data.into_inner().data; let group = group_request.to_group(&org_id)?; + log_event( + EventType::GroupCreated as i32, + &group.uuid, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + add_update_group(group, group_request.Collections, &mut conn).await } -#[put("/organizations/<_org_id>/groups/<group_id>", data = "<data>")] +#[put("/organizations/<org_id>/groups/<group_id>", data = "<data>")] async fn put_group( - _org_id: String, + org_id: String, group_id: String, data: JsonUpcase<GroupRequest>, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> JsonResult { let group = match Group::find_by_uuid(&group_id, &mut conn).await { Some(group) => group, @@ -1870,6 +2125,17 @@ async fn put_group( CollectionGroup::delete_all_by_group(&group_id, &mut conn).await?; + log_event( + EventType::GroupUpdated as i32, + &updated_group.uuid, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + add_update_group(updated_group, group_request.Collections, &mut conn).await } @@ -1915,17 +2181,40 @@ async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHea } #[post("/organizations/<org_id>/groups/<group_id>/delete")] -async fn post_delete_group(org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult { - delete_group(org_id, group_id, _headers, conn).await +async fn post_delete_group( + org_id: String, + group_id: String, + headers: AdminHeaders, + conn: DbConn, + ip: ClientIp, +) -> EmptyResult { + delete_group(org_id, group_id, headers, conn, ip).await } -#[delete("/organizations/<_org_id>/groups/<group_id>")] -async fn delete_group(_org_id: String, group_id: String, _headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { +#[delete("/organizations/<org_id>/groups/<group_id>")] +async fn delete_group( + org_id: String, + group_id: String, + headers: AdminHeaders, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { let group = match Group::find_by_uuid(&group_id, &mut conn).await { Some(group) => group, _ => err!("Group not found"), }; + log_event( + EventType::GroupDeleted as i32, + &group.uuid, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + group.delete(&mut conn).await } @@ -1955,13 +2244,14 @@ async fn get_group_users(_org_id: String, group_id: String, _headers: AdminHeade Ok(Json(json!(group_users))) } -#[put("/organizations/<_org_id>/groups/<group_id>/users", data = "<data>")] +#[put("/organizations/<org_id>/groups/<group_id>/users", data = "<data>")] async fn put_group_users( - _org_id: String, + org_id: String, group_id: String, - _headers: AdminHeaders, + headers: AdminHeaders, data: JsonVec<String>, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { match Group::find_by_uuid(&group_id, &mut conn).await { Some(_) => { /* Do nothing */ } @@ -1972,8 +2262,19 @@ async fn put_group_users( let assigned_user_ids = data.into_inner(); for assigned_user_id in assigned_user_ids { - let mut user_entry = GroupUser::new(group_id.clone(), assigned_user_id); + let mut user_entry = GroupUser::new(group_id.clone(), assigned_user_id.clone()); user_entry.save(&mut conn).await?; + + log_event( + EventType::OrganizationUserUpdatedGroups as i32, + &assigned_user_id, + org_id.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; } Ok(()) @@ -1998,61 +2299,76 @@ struct OrganizationUserUpdateGroupsRequest { GroupIds: Vec<String>, } -#[post("/organizations/<_org_id>/users/<user_id>/groups", data = "<data>")] +#[post("/organizations/<org_id>/users/<org_user_id>/groups", data = "<data>")] async fn post_user_groups( - _org_id: String, - user_id: String, + org_id: String, + org_user_id: String, data: JsonUpcase<OrganizationUserUpdateGroupsRequest>, - _headers: AdminHeaders, + headers: AdminHeaders, conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - put_user_groups(_org_id, user_id, data, _headers, conn).await + put_user_groups(org_id, org_user_id, data, headers, conn, ip).await } -#[put("/organizations/<_org_id>/users/<user_id>/groups", data = "<data>")] +#[put("/organizations/<org_id>/users/<org_user_id>/groups", data = "<data>")] async fn put_user_groups( - _org_id: String, - user_id: String, + org_id: String, + org_user_id: String, data: JsonUpcase<OrganizationUserUpdateGroupsRequest>, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - match UserOrganization::find_by_uuid(&user_id, &mut conn).await { + match UserOrganization::find_by_uuid(&org_user_id, &mut conn).await { Some(_) => { /* Do nothing */ } _ => err!("User could not be found!"), }; - GroupUser::delete_all_by_user(&user_id, &mut conn).await?; + GroupUser::delete_all_by_user(&org_user_id, &mut conn).await?; let assigned_group_ids = data.into_inner().data; for assigned_group_id in assigned_group_ids.GroupIds { - let mut group_user = GroupUser::new(assigned_group_id.clone(), user_id.clone()); + let mut group_user = GroupUser::new(assigned_group_id.clone(), org_user_id.clone()); group_user.save(&mut conn).await?; } + log_event( + EventType::OrganizationUserUpdatedGroups as i32, + &org_user_id, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + Ok(()) } -#[post("/organizations/<org_id>/groups/<group_id>/delete-user/<user_id>")] +#[post("/organizations/<org_id>/groups/<group_id>/delete-user/<org_user_id>")] async fn post_delete_group_user( org_id: String, group_id: String, - user_id: String, + org_user_id: String, headers: AdminHeaders, conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - delete_group_user(org_id, group_id, user_id, headers, conn).await + delete_group_user(org_id, group_id, org_user_id, headers, conn, ip).await } -#[delete("/organizations/<_org_id>/groups/<group_id>/users/<user_id>")] +#[delete("/organizations/<org_id>/groups/<group_id>/users/<org_user_id>")] async fn delete_group_user( - _org_id: String, + org_id: String, group_id: String, - user_id: String, - _headers: AdminHeaders, + org_user_id: String, + headers: AdminHeaders, mut conn: DbConn, + ip: ClientIp, ) -> EmptyResult { - match UserOrganization::find_by_uuid(&user_id, &mut conn).await { + match UserOrganization::find_by_uuid(&org_user_id, &mut conn).await { Some(_) => { /* Do nothing */ } _ => err!("User could not be found!"), }; @@ -2062,7 +2378,18 @@ async fn delete_group_user( _ => err!("Group could not be found!"), }; - GroupUser::delete_by_group_id_and_user_id(&group_id, &user_id, &mut conn).await + log_event( + EventType::OrganizationUserUpdatedGroups as i32, + &org_user_id, + org_id, + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + + GroupUser::delete_by_group_id_and_user_id(&group_id, &org_user_id, &mut conn).await } // This is a new function active since the v2022.9.x clients. diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 68342e95..fa1792f2 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -4,12 +4,13 @@ use rocket::Route; use crate::{ api::{ - core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData, + core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, + NumberOrString, PasswordData, }, auth::{ClientIp, Headers}, crypto, db::{ - models::{TwoFactor, TwoFactorType}, + models::{EventType, TwoFactor, TwoFactorType}, DbConn, }, }; @@ -85,6 +86,8 @@ async fn activate_authenticator( _generate_recover_code(&mut user, &mut conn).await; + log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await; + Ok(Json(json!({ "Enabled": true, "Key": key, @@ -167,10 +170,20 @@ pub async fn validate_totp_code( return Ok(()); } else if generated == totp_code && time_step <= i64::from(twofactor.last_used) { warn!("This TOTP or a TOTP code within {} steps back or forward has already been used!", steps); - err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip)); + err!( + format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip), + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ); } } // Else no valide code received, deny access - err!(format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip)); + err!( + format!("Invalid TOTP code! Server time: {} IP: {}", current_time.format("%F %T UTC"), ip.ip), + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ); } diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 42cc709e..06210d23 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -4,11 +4,14 @@ use rocket::serde::json::Json; use rocket::Route; use crate::{ - api::{core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData}, - auth::Headers, + api::{ + core::log_user_event, core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, + PasswordData, + }, + auth::{ClientIp, Headers}, crypto, db::{ - models::{TwoFactor, TwoFactorType, User}, + models::{EventType, TwoFactor, TwoFactorType, User}, DbConn, }, error::MapResult, @@ -152,7 +155,7 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool { } #[post("/two-factor/duo", data = "<data>")] -async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut conn: DbConn, ip: ClientIp) -> JsonResult { let data: EnableDuoData = data.into_inner().data; let mut user = headers.user; @@ -175,6 +178,8 @@ async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut con _generate_recover_code(&mut user, &mut conn).await; + log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await; + Ok(Json(json!({ "Enabled": true, "Host": data.host, @@ -185,8 +190,8 @@ async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut con } #[put("/two-factor/duo", data = "<data>")] -async fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult { - activate_duo(data, headers, conn).await +async fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn, ip: ClientIp) -> JsonResult { + activate_duo(data, headers, conn, ip).await } async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult { @@ -282,7 +287,12 @@ pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) let split: Vec<&str> = response.split(':').collect(); if split.len() != 2 { - err!("Invalid response length"); + err!( + "Invalid response length", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ); } let auth_sig = split[0]; @@ -296,7 +306,12 @@ pub async fn validate_duo_login(email: &str, response: &str, conn: &mut DbConn) let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?; if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) { - err!("Error validating duo authentication") + err!( + "Error validating duo authentication", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ) } Ok(()) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 90247f53..9a95c465 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -3,11 +3,14 @@ use rocket::serde::json::Json; use rocket::Route; use crate::{ - api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData}, - auth::Headers, + api::{ + core::{log_user_event, two_factor::_generate_recover_code}, + EmptyResult, JsonResult, JsonUpcase, PasswordData, + }, + auth::{ClientIp, Headers}, crypto, db::{ - models::{TwoFactor, TwoFactorType}, + models::{EventType, TwoFactor, TwoFactorType}, DbConn, }, error::{Error, MapResult}, @@ -147,7 +150,7 @@ struct EmailData { /// Verify email belongs to user and can be used for 2FA email codes. #[put("/two-factor/email", data = "<data>")] -async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn, ip: ClientIp) -> JsonResult { let data: EmailData = data.into_inner().data; let mut user = headers.user; @@ -177,6 +180,8 @@ async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn) _generate_recover_code(&mut user, &mut conn).await; + log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await; + Ok(Json(json!({ "Email": email_data.email, "Enabled": "true", @@ -192,7 +197,12 @@ pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, c .map_res("Two factor not found")?; let issued_token = match &email_data.last_token { Some(t) => t, - _ => err!("No token available"), + _ => err!( + "No token available", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ), }; if !crypto::ct_eq(issued_token, token) { @@ -203,21 +213,32 @@ pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, c twofactor.data = email_data.to_json(); twofactor.save(conn).await?; - err!("Token is invalid") + err!( + "Token is invalid", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ) } email_data.reset_token(); twofactor.data = email_data.to_json(); twofactor.save(conn).await?; - let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0); + let date = NaiveDateTime::from_timestamp_opt(email_data.token_sent, 0).expect("Email token timestamp invalid."); let max_time = CONFIG.email_expiration_time() as i64; if date + Duration::seconds(max_time) < Utc::now().naive_utc() { - err!("Token has expired") + err!( + "Token has expired", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ) } Ok(()) } + /// Data stored in the TwoFactor table in the db #[derive(Serialize, Deserialize)] pub struct EmailTokenData { diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 3d5eee83..ce3cfb72 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -5,8 +5,8 @@ use rocket::Route; use serde_json::Value; use crate::{ - api::{JsonResult, JsonUpcase, NumberOrString, PasswordData}, - auth::Headers, + api::{core::log_user_event, JsonResult, JsonUpcase, NumberOrString, PasswordData}, + auth::{ClientIp, Headers}, crypto, db::{models::*, DbConn, DbPool}, mail, CONFIG, @@ -73,7 +73,7 @@ struct RecoverTwoFactor { } #[post("/two-factor/recover", data = "<data>")] -async fn recover(data: JsonUpcase<RecoverTwoFactor>, mut conn: DbConn) -> JsonResult { +async fn recover(data: JsonUpcase<RecoverTwoFactor>, headers: Headers, mut conn: DbConn, ip: ClientIp) -> JsonResult { let data: RecoverTwoFactor = data.into_inner().data; use crate::db::models::User; @@ -97,6 +97,8 @@ async fn recover(data: JsonUpcase<RecoverTwoFactor>, mut conn: DbConn) -> JsonRe // Remove all twofactors from the user TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?; + log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await; + // Remove the recovery code, not needed without twofactors user.totp_recover = None; user.save(&mut conn).await?; @@ -119,7 +121,12 @@ struct DisableTwoFactorData { } #[post("/two-factor/disable", data = "<data>")] -async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn disable_twofactor( + data: JsonUpcase<DisableTwoFactorData>, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, +) -> JsonResult { let data: DisableTwoFactorData = data.into_inner().data; let password_hash = data.MasterPasswordHash; let user = headers.user; @@ -132,6 +139,7 @@ async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Head if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await { twofactor.delete(&mut conn).await?; + log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await; } let twofactor_disabled = TwoFactor::find_by_user(&user.uuid, &mut conn).await.is_empty(); @@ -160,8 +168,13 @@ async fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Head } #[put("/two-factor/disable", data = "<data>")] -async fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult { - disable_twofactor(data, headers, conn).await +async fn disable_twofactor_put( + data: JsonUpcase<DisableTwoFactorData>, + headers: Headers, + conn: DbConn, + ip: ClientIp, +) -> JsonResult { + disable_twofactor(data, headers, conn, ip).await } pub async fn send_incomplete_2fa_notifications(pool: DbPool) { diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 0d9e5542..97711c75 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -6,11 +6,12 @@ use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, use crate::{ api::{ - core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData, + core::{log_user_event, two_factor::_generate_recover_code}, + EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData, }, - auth::Headers, + auth::{ClientIp, Headers}, db::{ - models::{TwoFactor, TwoFactorType}, + models::{EventType, TwoFactor, TwoFactorType}, DbConn, }, error::Error, @@ -241,7 +242,12 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential { } #[post("/two-factor/webauthn", data = "<data>")] -async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn activate_webauthn( + data: JsonUpcase<EnableWebauthnData>, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, +) -> JsonResult { let data: EnableWebauthnData = data.into_inner().data; let mut user = headers.user; @@ -280,6 +286,8 @@ async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Header .await?; _generate_recover_code(&mut user, &mut conn).await; + log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await; + let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect(); Ok(Json(json!({ "Enabled": true, @@ -289,8 +297,13 @@ async fn activate_webauthn(data: JsonUpcase<EnableWebauthnData>, headers: Header } #[put("/two-factor/webauthn", data = "<data>")] -async fn activate_webauthn_put(data: JsonUpcase<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult { - activate_webauthn(data, headers, conn).await +async fn activate_webauthn_put( + data: JsonUpcase<EnableWebauthnData>, + headers: Headers, + conn: DbConn, + ip: ClientIp, +) -> JsonResult { + activate_webauthn(data, headers, conn, ip).await } #[derive(Deserialize, Debug)] @@ -391,7 +404,12 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut tf.delete(conn).await?; state } - None => err!("Can't recover login challenge"), + None => err!( + "Can't recover login challenge", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ), }; let rsp: crate::util::UpCase<PublicKeyCredentialCopy> = serde_json::from_str(response)?; @@ -414,5 +432,10 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut } } - err!("Credential not present") + err!( + "Credential not present", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ) } diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index 7994bea0..00ef7df2 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -4,10 +4,13 @@ use serde_json::Value; use yubico::{config::Config, verify}; use crate::{ - api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData}, - auth::Headers, + api::{ + core::{log_user_event, two_factor::_generate_recover_code}, + EmptyResult, JsonResult, JsonUpcase, PasswordData, + }, + auth::{ClientIp, Headers}, db::{ - models::{TwoFactor, TwoFactorType}, + models::{EventType, TwoFactor, TwoFactorType}, DbConn, }, error::{Error, MapResult}, @@ -113,7 +116,12 @@ async fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, mut } #[post("/two-factor/yubikey", data = "<data>")] -async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn activate_yubikey( + data: JsonUpcase<EnableYubikeyData>, + headers: Headers, + mut conn: DbConn, + ip: ClientIp, +) -> JsonResult { let data: EnableYubikeyData = data.into_inner().data; let mut user = headers.user; @@ -159,6 +167,8 @@ async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, _generate_recover_code(&mut user, &mut conn).await; + log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await; + let mut result = jsonify_yubikeys(yubikey_metadata.Keys); result["Enabled"] = Value::Bool(true); @@ -169,8 +179,13 @@ async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, } #[put("/two-factor/yubikey", data = "<data>")] -async fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult { - activate_yubikey(data, headers, conn).await +async fn activate_yubikey_put( + data: JsonUpcase<EnableYubikeyData>, + headers: Headers, + conn: DbConn, + ip: ClientIp, +) -> JsonResult { + activate_yubikey(data, headers, conn, ip).await } pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult { diff --git a/src/api/identity.rs b/src/api/identity.rs index 9e747c7d..6499ee38 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -10,6 +10,7 @@ use serde_json::Value; use crate::{ api::{ core::accounts::{PreloginData, RegisterData, _prelogin, _register}, + core::log_user_event, core::two_factor::{duo, email, email::EmailTokenData, yubikey}, ApiResult, EmptyResult, JsonResult, JsonUpcase, }, @@ -24,13 +25,16 @@ pub fn routes() -> Vec<Route> { } #[post("/connect/token", data = "<data>")] -async fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult { +async fn login(data: Form<ConnectData>, mut conn: DbConn, ip: ClientIp) -> JsonResult { let data: ConnectData = data.into_inner(); - match data.grant_type.as_ref() { + let mut user_uuid: Option<String> = None; + let device_type = data.device_type.clone(); + + let login_result = match data.grant_type.as_ref() { "refresh_token" => { _check_is_some(&data.refresh_token, "refresh_token cannot be blank")?; - _refresh_login(data, conn).await + _refresh_login(data, &mut conn).await } "password" => { _check_is_some(&data.client_id, "client_id cannot be blank")?; @@ -42,34 +46,51 @@ async fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResul _check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?; - _password_login(data, conn, &ip).await + _password_login(data, &mut user_uuid, &mut conn, &ip).await } "client_credentials" => { _check_is_some(&data.client_id, "client_id cannot be blank")?; _check_is_some(&data.client_secret, "client_secret cannot be blank")?; _check_is_some(&data.scope, "scope cannot be blank")?; - _api_key_login(data, conn, &ip).await + _api_key_login(data, &mut user_uuid, &mut conn, &ip).await } t => err!("Invalid type", t), + }; + + if let Some(user_uuid) = user_uuid { + // When unknown or unable to parse, return 14, which is 'Unknown Browser' + let device_type = util::try_parse_string(device_type).unwrap_or(14); + match &login_result { + Ok(_) => { + log_user_event(EventType::UserLoggedIn as i32, &user_uuid, device_type, &ip.ip, &mut conn).await; + } + Err(e) => { + if let Some(ev) = e.get_event() { + log_user_event(ev.event as i32, &user_uuid, device_type, &ip.ip, &mut conn).await + } + } + } } + + login_result } -async fn _refresh_login(data: ConnectData, mut conn: DbConn) -> JsonResult { +async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult { // Extract token let token = data.refresh_token.unwrap(); // Get device by refresh token - let mut device = Device::find_by_refresh_token(&token, &mut conn).await.map_res("Invalid refresh token")?; + let mut device = Device::find_by_refresh_token(&token, conn).await.map_res("Invalid refresh token")?; let scope = "api offline_access"; let scope_vec = vec!["api".into(), "offline_access".into()]; // Common - let user = User::find_by_uuid(&device.user_uuid, &mut conn).await.unwrap(); - let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &mut conn).await; + let user = User::find_by_uuid(&device.user_uuid, conn).await.unwrap(); + let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec); - device.save(&mut conn).await?; + device.save(conn).await?; Ok(Json(json!({ "access_token": access_token, @@ -87,7 +108,12 @@ async fn _refresh_login(data: ConnectData, mut conn: DbConn) -> JsonResult { }))) } -async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> JsonResult { +async fn _password_login( + data: ConnectData, + user_uuid: &mut Option<String>, + conn: &mut DbConn, + ip: &ClientIp, +) -> JsonResult { // Validate scope let scope = data.scope.as_ref().unwrap(); if scope != "api offline_access" { @@ -100,20 +126,35 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> // Get the user let username = data.username.as_ref().unwrap().trim(); - let user = match User::find_by_mail(username, &mut conn).await { + let user = match User::find_by_mail(username, conn).await { Some(user) => user, None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)), }; + // Set the user_uuid here to be passed back used for event logging. + *user_uuid = Some(user.uuid.clone()); + // Check password let password = data.password.as_ref().unwrap(); if !user.check_valid_password(password) { - err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)) + err!( + "Username or password is incorrect. Try again", + format!("IP: {}. Username: {}.", ip.ip, username), + ErrorEvent { + event: EventType::UserFailedLogIn, + } + ) } // Check if the user is disabled if !user.enabled { - err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username)) + err!( + "This user has been disabled", + format!("IP: {}. Username: {}.", ip.ip, username), + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) } let now = Utc::now().naive_utc(); @@ -131,7 +172,7 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> user.last_verifying_at = Some(now); user.login_verify_count += 1; - if let Err(e) = user.save(&mut conn).await { + if let Err(e) = user.save(conn).await { error!("Error updating user: {:#?}", e); } @@ -142,27 +183,38 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> } // We still want the login to fail until they actually verified the email address - err!("Please verify your email before trying again.", format!("IP: {}. Username: {}.", ip.ip, username)) + err!( + "Please verify your email before trying again.", + format!("IP: {}. Username: {}.", ip.ip, username), + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) } - let (mut device, new_device) = get_device(&data, &mut conn, &user).await; + let (mut device, new_device) = get_device(&data, conn, &user).await; - let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, &mut conn).await?; + let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, conn).await?; if CONFIG.mail_enabled() && new_device { if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await { error!("Error sending new device email: {:#?}", e); if CONFIG.require_device_email() { - err!("Could not send login notification email. Please contact your administrator.") + err!( + "Could not send login notification email. Please contact your administrator.", + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) } } } // Common - let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &mut conn).await; + let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec); - device.save(&mut conn).await?; + device.save(conn).await?; let mut result = json!({ "access_token": access_token, @@ -188,7 +240,12 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> Ok(Json(result)) } -async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> JsonResult { +async fn _api_key_login( + data: ConnectData, + user_uuid: &mut Option<String>, + conn: &mut DbConn, + ip: &ClientIp, +) -> JsonResult { // Validate scope let scope = data.scope.as_ref().unwrap(); if scope != "api" { @@ -201,27 +258,42 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J // Get the user via the client_id let client_id = data.client_id.as_ref().unwrap(); - let user_uuid = match client_id.strip_prefix("user.") { + let client_user_uuid = match client_id.strip_prefix("user.") { Some(uuid) => uuid, None => err!("Malformed client_id", format!("IP: {}.", ip.ip)), }; - let user = match User::find_by_uuid(user_uuid, &mut conn).await { + let user = match User::find_by_uuid(client_user_uuid, conn).await { Some(user) => user, None => err!("Invalid client_id", format!("IP: {}.", ip.ip)), }; + // Set the user_uuid here to be passed back used for event logging. + *user_uuid = Some(user.uuid.clone()); + // Check if the user is disabled if !user.enabled { - err!("This user has been disabled (API key login)", format!("IP: {}. Username: {}.", ip.ip, user.email)) + err!( + "This user has been disabled (API key login)", + format!("IP: {}. Username: {}.", ip.ip, user.email), + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) } // Check API key. Note that API key logins bypass 2FA. let client_secret = data.client_secret.as_ref().unwrap(); if !user.check_valid_api_key(client_secret) { - err!("Incorrect client_secret", format!("IP: {}. Username: {}.", ip.ip, user.email)) + err!( + "Incorrect client_secret", + format!("IP: {}. Username: {}.", ip.ip, user.email), + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) } - let (mut device, new_device) = get_device(&data, &mut conn, &user).await; + let (mut device, new_device) = get_device(&data, conn, &user).await; if CONFIG.mail_enabled() && new_device { let now = Utc::now().naive_utc(); @@ -229,15 +301,20 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J error!("Error sending new device email: {:#?}", e); if CONFIG.require_device_email() { - err!("Could not send login notification email. Please contact your administrator.") + err!( + "Could not send login notification email. Please contact your administrator.", + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) } } } // Common - let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &mut conn).await; + let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec); - device.save(&mut conn).await?; + device.save(conn).await?; info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip); @@ -261,7 +338,8 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J /// Retrieves an existing device or creates a new device from ConnectData and the User async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> (Device, bool) { // On iOS, device_type sends "iOS", on others it sends a number - let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0); + // When unknown or unable to parse, return 14, which is 'Unknown Browser' + let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(14); let device_id = data.device_identifier.clone().expect("No device id provided"); let device_name = data.device_name.clone().expect("No device name provided"); @@ -338,7 +416,12 @@ async fn twofactor_auth( } } } - _ => err!("Invalid two factor provider"), + _ => err!( + "Invalid two factor provider", + ErrorEvent { + event: EventType::UserFailedLogIn2fa + } + ), } TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn).await?; diff --git a/src/api/mod.rs b/src/api/mod.rs index 9b151300..06db310b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -17,6 +17,7 @@ pub use crate::api::{ core::routes as core_routes, core::two_factor::send_incomplete_2fa_notifications, core::{emergency_notification_reminder_job, emergency_request_timeout_job}, + core::{event_cleanup_job, events_routes as core_events_routes}, icons::routes as icons_routes, identity::routes as identity_routes, notifications::routes as notifications_routes, |