mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-13 12:05:58 +03:00
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
This commit is contained in:
@@ -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?
|
||||
}
|
||||
|
||||
|
@@ -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(())
|
||||
}
|
||||
|
||||
|
341
src/api/core/events.rs
Normal file
341
src/api/core/events.rs
Normal file
@@ -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")
|
||||
}
|
||||
}
|
@@ -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
|
||||
//
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
async fn _delete_user(org_id: &str, org_user_id: &str, headers: &AdminHeaders, conn: &mut DbConn) -> EmptyResult {
|
||||
#[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,
|
||||
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.
|
||||
|
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -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(())
|
||||
|
@@ -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 {
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user