2022-11-20 19:15:45 +01:00
|
|
|
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},
|
2023-03-09 16:31:28 +01:00
|
|
|
auth::{AdminHeaders, Headers},
|
2022-11-20 19:15:45 +01:00
|
|
|
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..>")]
|
2023-04-30 17:18:12 +02:00
|
|
|
async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
2022-11-20 19:15:45 +01:00
|
|
|
// 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)
|
|
|
|
};
|
|
|
|
|
2023-04-30 17:18:12 +02:00
|
|
|
Event::find_by_organization_uuid(org_id, &start_date, &end_date, &mut conn)
|
2022-11-20 19:15:45 +01:00
|
|
|
.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..>")]
|
2023-04-30 17:18:12 +02:00
|
|
|
async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
|
2022-11-20 19:15:45 +01:00
|
|
|
// 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);
|
2023-04-30 17:18:12 +02:00
|
|
|
if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, cipher_id, &mut conn).await {
|
2022-11-20 19:15:45 +01:00
|
|
|
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)
|
|
|
|
};
|
|
|
|
|
2023-04-30 17:18:12 +02:00
|
|
|
events_json = Event::find_by_cipher_uuid(cipher_id, &start_date, &end_date, &mut conn)
|
2022-11-20 19:15:45 +01:00
|
|
|
.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(
|
2023-04-30 17:18:12 +02:00
|
|
|
org_id: &str,
|
|
|
|
user_org_id: &str,
|
2022-11-20 19:15:45 +01:00
|
|
|
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)
|
|
|
|
};
|
|
|
|
|
2023-04-30 17:18:12 +02:00
|
|
|
Event::find_by_org_and_user_org(org_id, user_org_id, &start_date, &end_date, &mut conn)
|
2022-11-20 19:15:45 +01:00
|
|
|
.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>")]
|
2023-03-09 16:31:28 +01:00
|
|
|
async fn post_events_collect(data: JsonUpcaseVec<EventCollection>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
2022-11-20 19:15:45 +01:00
|
|
|
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),
|
2023-03-09 16:31:28 +01:00
|
|
|
&headers.ip.ip,
|
2022-11-20 19:15:45 +01:00
|
|
|
&mut conn,
|
|
|
|
)
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
1600..=1699 => {
|
|
|
|
if let Some(org_uuid) = &event.OrganizationId {
|
|
|
|
_log_event(
|
|
|
|
event.Type,
|
|
|
|
org_uuid,
|
2023-04-30 17:18:12 +02:00
|
|
|
org_uuid,
|
2022-11-20 19:15:45 +01:00
|
|
|
&headers.user.uuid,
|
|
|
|
headers.device.atype,
|
|
|
|
Some(event_date),
|
2023-03-09 16:31:28 +01:00
|
|
|
&headers.ip.ip,
|
2022-11-20 19:15:45 +01:00
|
|
|
&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,
|
2023-04-30 17:18:12 +02:00
|
|
|
&org_uuid,
|
2022-11-20 19:15:45 +01:00
|
|
|
&headers.user.uuid,
|
|
|
|
headers.device.atype,
|
|
|
|
Some(event_date),
|
2023-03-09 16:31:28 +01:00
|
|
|
&headers.ip.ip,
|
2022-11-20 19:15:45 +01:00
|
|
|
&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,
|
2023-04-30 17:18:12 +02:00
|
|
|
org_uuid: &str,
|
2024-01-01 19:41:40 +01:00
|
|
|
act_user_uuid: &str,
|
2022-11-20 19:15:45 +01:00
|
|
|
device_type: i32,
|
|
|
|
ip: &IpAddr,
|
|
|
|
conn: &mut DbConn,
|
|
|
|
) {
|
|
|
|
if !CONFIG.org_events_enabled() {
|
|
|
|
return;
|
|
|
|
}
|
2024-01-01 19:41:40 +01:00
|
|
|
_log_event(event_type, source_uuid, org_uuid, act_user_uuid, device_type, None, ip, conn).await;
|
2022-11-20 19:15:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
async fn _log_event(
|
|
|
|
event_type: i32,
|
|
|
|
source_uuid: &str,
|
2023-04-30 17:18:12 +02:00
|
|
|
org_uuid: &str,
|
2022-11-20 19:15:45 +01:00
|
|
|
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
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
|
2023-04-30 17:18:12 +02:00
|
|
|
event.org_uuid = Some(String::from(org_uuid));
|
2022-11-20 19:15:45 +01:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|