From 26850247fad2b9a3dde631217081f8db0f97b9c0 Mon Sep 17 00:00:00 2001 From: Rune Darrud Date: Mon, 10 Aug 2026 00:26:34 +0200 Subject: [PATCH] Add Public API organization event log endpoint Expose GET /public/events so an organization-scoped API client can read the organization event log using its organization API key. The same data is otherwise only reachable through the internal API, which requires an admin user session. Reuses the existing EventRange query model and continuation-token paging helper from the internal events endpoints, so the request and response shapes match /organizations//events. Co-Authored-By: Claude Opus 5 --- src/api/core/events.rs | 10 +++++----- src/api/core/public.rs | 44 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) 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]