diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 5518fa3c..d3d8cc84 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -22,11 +22,11 @@ pub fn routes() -> Vec { } #[derive(FromForm)] -struct EventRange { - start: String, - end: String, +pub struct EventRange { + pub start: String, + pub end: String, #[field(name = "continuationToken")] - continuation_token: Option, + pub continuation_token: Option, } // Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/EventsController.cs#L87 @@ -125,7 +125,7 @@ async fn get_user_events( }))) } -fn get_continuation_token(events_json: &[Value]) -> Option<&str> { +pub fn get_continuation_token(events_json: &[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. #[expect(clippy::cast_possible_truncation, reason = "PAGE_SIZE fits within usize")] diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 3db25df9..f8fd1c2a 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -6,23 +6,27 @@ use rocket::{ request::{FromRequest, Outcome}, serde::json::Json, }; +use serde_json::Value; use crate::{ CONFIG, - api::EmptyResult, + api::{EmptyResult, JsonResult}, auth, db::{ DbConn, models::{ - Group, GroupUser, Invitation, Membership, MembershipStatus, MembershipType, OrgPolicy, Organization, + Event, Group, GroupUser, Invitation, Membership, MembershipStatus, MembershipType, OrgPolicy, Organization, OrganizationApiKey, OrganizationId, User, }, }, mail, + util::parse_date, }; +use super::events::{EventRange, get_continuation_token}; + pub fn routes() -> Vec { - routes![ldap_import] + routes![ldap_import, get_events] } #[derive(Deserialize)] @@ -196,6 +200,40 @@ async fn ldap_import(data: Json, token: PublicToken, conn: DbConn Ok(()) } +// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Public/Controllers/EventsController.cs +// Exposes the organization event log to an organization-scoped API client. The +// same data is otherwise only reachable through the internal API, which requires +// an admin user session instead of an organization API key. +#[get("/public/events?")] +async fn get_events(data: EventRange, token: PublicToken, conn: DbConn) -> JsonResult { + let org_id = token.0; + + // Return an empty vec when the org events are disabled. + // This prevents client errors + let events_json: Vec = if CONFIG.org_events_enabled() { + 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, &conn) + .await + .iter() + .map(Event::to_json) + .collect() + } else { + Vec::new() + }; + + Ok(Json(json!({ + "object": "list", + "data": events_json, + "continuationToken": get_continuation_token(&events_json), + }))) +} + pub struct PublicToken(OrganizationId); #[rocket::async_trait]