use std::sync::Arc;
use serde_json::{json, Map, Value};
use sha2::{Digest, Sha256};
use crate::error::Error;
use crate::http::{HttpTransport, QueryParams};
use crate::models::audit::*;
fn stable_stringify(v: &Value, out: &mut String) {
match v {
Value::Object(o) => {
let mut keys: Vec<&String> = o.keys().collect();
keys.sort();
out.push('{');
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&serde_json::to_string(k).expect("string key serializes"));
out.push(':');
stable_stringify(&o[*k], out);
}
out.push('}');
}
Value::Array(a) => {
out.push('[');
for (i, item) in a.iter().enumerate() {
if i > 0 {
out.push(',');
}
stable_stringify(item, out);
}
out.push(']');
}
scalar => out.push_str(&serde_json::to_string(scalar).expect("scalar serializes")),
}
}
pub fn content_idempotency_key(body: &Value) -> String {
let mut s = String::new();
stable_stringify(body, &mut s);
let mut hasher = Sha256::new();
hasher.update(s.as_bytes());
format!("idem_{}", hex::encode(hasher.finalize()))
}
fn now_rfc3339() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let days = secs.div_euclid(86_400);
let rem = secs.rem_euclid(86_400);
let (y, m, d) = civil_from_days(days);
let hh = rem / 3600;
let mi = (rem % 3600) / 60;
let ss = rem % 60;
format!("{y:04}-{m:02}-{d:02}T{hh:02}:{mi:02}:{ss:02}Z")
}
fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
(if m <= 2 { y + 1 } else { y }, m, d)
}
#[derive(Clone)]
pub struct AuditEventsResource {
t: Arc<HttpTransport>,
}
impl AuditEventsResource {
pub async fn ingest(&self, params: IngestAuditEventParams) -> Result<Value, Error> {
let mut body = Map::new();
body.insert("organization_id".into(), json!(params.organization_id));
body.insert("action".into(), json!(params.action));
body.insert(
"occurred_at".into(),
json!(params.occurred_at.unwrap_or_else(now_rfc3339)),
);
body.insert("actor".into(), Value::Object(params.actor));
body.insert(
"targets".into(),
Value::Array(params.targets.unwrap_or_default()),
);
if let Some(c) = params.context {
body.insert("context".into(), Value::Object(c));
}
if let Some(m) = params.metadata {
body.insert("metadata".into(), Value::Object(m));
}
let body = Value::Object(body);
let idem = params
.idempotency_key
.unwrap_or_else(|| content_idempotency_key(&body));
self.t.post("/audit/events", Some(&body), Some(&idem)).await
}
pub async fn list(
&self,
params: ListAuditEventsParams,
) -> Result<ListAuditEventsResponse, Error> {
let q: QueryParams = vec![
("organization_id", params.organization_id),
("actions", params.actions),
("actor_id", params.actor_id),
("target_id", params.target_id),
("range_start", params.range_start),
("range_end", params.range_end),
("limit", params.limit.map(|v| v.to_string())),
("cursor", params.cursor),
];
self.t.get("/audit/events", Some(q)).await
}
pub async fn get(&self, event_id: &str) -> Result<AuditEvent, Error> {
self.t.get(&format!("/audit/events/{event_id}"), None).await
}
pub async fn verify(&self, event_id: &str) -> Result<Value, Error> {
self.t
.get(&format!("/audit/events/{event_id}/verify"), None)
.await
}
}
#[derive(Clone)]
pub struct AuditOrgsResource {
t: Arc<HttpTransport>,
}
impl AuditOrgsResource {
pub async fn create(&self, params: CreateAuditOrgParams) -> Result<Value, Error> {
let mut body = Map::new();
body.insert("organization_id".into(), json!(params.organization_id));
if let Some(n) = ¶ms.name {
body.insert("name".into(), json!(n));
}
self.t
.post("/audit/orgs", Some(&Value::Object(body)), None)
.await
}
pub async fn list(&self) -> Result<Value, Error> {
self.t.get("/audit/orgs", None).await
}
pub async fn integrity(&self, organization_id: &str) -> Result<Value, Error> {
self.t
.get(&format!("/audit/orgs/{organization_id}/integrity"), None)
.await
}
pub async fn set_retention(&self, organization_id: &str, days: i64) -> Result<Value, Error> {
let body = json!({ "days": days });
self.t
.put(
&format!("/audit/orgs/{organization_id}/retention"),
Some(&body),
)
.await
}
}
#[derive(Clone)]
pub struct AuditStreamsResource {
t: Arc<HttpTransport>,
}
impl AuditStreamsResource {
pub async fn create(
&self,
organization_id: &str,
params: CreateAuditStreamParams,
) -> Result<Value, Error> {
let body = json!({
"type": params.r#type.unwrap_or_else(|| "webhook".to_string()),
"url": params.url,
});
self.t
.post(
&format!("/audit/orgs/{organization_id}/streams"),
Some(&body),
None,
)
.await
}
pub async fn list(&self, organization_id: &str) -> Result<Value, Error> {
self.t
.get(&format!("/audit/orgs/{organization_id}/streams"), None)
.await
}
pub async fn delete(&self, organization_id: &str, stream_id: &str) -> Result<Value, Error> {
self.t
.delete(&format!(
"/audit/orgs/{organization_id}/streams/{stream_id}"
))
.await
}
pub async fn test(&self, organization_id: &str, stream_id: &str) -> Result<Value, Error> {
self.t
.post(
&format!("/audit/orgs/{organization_id}/streams/{stream_id}/test"),
None,
None,
)
.await
}
}
#[derive(Clone)]
pub struct AuditPortalSessionsResource {
t: Arc<HttpTransport>,
}
impl AuditPortalSessionsResource {
pub async fn create(&self, params: CreatePortalSessionParams) -> Result<Value, Error> {
let mut body = Map::new();
body.insert("organization_id".into(), json!(params.organization_id));
body.insert("intent".into(), json!(params.intent));
if let Some(s) = params.session_duration_seconds {
body.insert("session_duration_seconds".into(), json!(s));
}
if let Some(s) = params.link_duration_seconds {
body.insert("link_duration_seconds".into(), json!(s));
}
self.t
.post("/audit/portal_sessions", Some(&Value::Object(body)), None)
.await
}
}
#[derive(Clone)]
pub struct AuditExportsResource {
t: Arc<HttpTransport>,
}
impl AuditExportsResource {
pub async fn create(&self, params: CreateAuditExportParams) -> Result<Value, Error> {
let mut body = Map::new();
body.insert("organization_id".into(), json!(params.organization_id));
body.insert("format".into(), json!(params.format));
if let Some(f) = params.filters {
body.insert("filters".into(), Value::Object(f));
}
self.t
.post("/audit/exports", Some(&Value::Object(body)), None)
.await
}
pub async fn get(&self, export_id: &str) -> Result<Value, Error> {
self.t
.get(&format!("/audit/exports/{export_id}"), None)
.await
}
}
#[derive(Clone)]
pub struct AuditResource {
pub events: AuditEventsResource,
pub orgs: AuditOrgsResource,
pub streams: AuditStreamsResource,
pub portal_sessions: AuditPortalSessionsResource,
pub exports: AuditExportsResource,
}
impl AuditResource {
pub(crate) fn new(t: Arc<HttpTransport>) -> Self {
Self {
events: AuditEventsResource { t: t.clone() },
orgs: AuditOrgsResource { t: t.clone() },
streams: AuditStreamsResource { t: t.clone() },
portal_sessions: AuditPortalSessionsResource { t: t.clone() },
exports: AuditExportsResource { t },
}
}
}