dinio_sdk/api/audit_logs.rs
1//! `/api/v1/audit-logs` (`../core/src/api/audit_logs.ts`). Read-only; Core decides what
2//! each caller sees.
3
4use reqwest::Method;
5use uuid::Uuid;
6
7use crate::client::Client;
8use crate::envelope::CursorPage;
9use crate::error::Result;
10use crate::models::AuditLog;
11use crate::time::Timestamp;
12
13/// Filters and page for [`AuditLogs::list`]. Every field is optional; together they
14/// narrow. Admin-only filters (`core_id`, `nats_seq`, `ip`) are not here.
15#[derive(Debug, Clone, Default)]
16pub struct AuditLogFilter {
17 /// Start of the range, inclusive. Core's default: 24 hours before `to`. The range may
18 /// span at most 31 days.
19 pub from: Option<Timestamp>,
20 /// End of the range. Core's default: now.
21 pub to: Option<Timestamp>,
22 /// `user`, `user_key`, `zone_key`, `edge`, `system` or `anonymous`.
23 pub actor_type: Option<String>,
24 /// The user who acted.
25 pub actor_user_id: Option<Uuid>,
26 /// The zone the operation belongs to.
27 pub zone_id: Option<Uuid>,
28 /// `api`, `relay`, `auth` or `system`.
29 pub category: Option<String>,
30 /// Exact action (`record.create`), or a prefix ending in `*` (`record.*`).
31 pub action: Option<String>,
32 /// `success`, `denied`, `invalid`, `conflict`, `not_found` or `error`.
33 pub outcome: Option<String>,
34 /// e.g. `record`, `zone`.
35 pub resource_type: Option<String>,
36 /// Id of the resource.
37 pub resource_id: Option<String>,
38 /// The entries of one API request.
39 pub request_id: Option<Uuid>,
40 /// Entries per page, 1–500; Core's default 50.
41 pub limit: Option<u32>,
42 /// [`CursorPage::next_cursor`] of the previous page; keep the other filters the same.
43 pub cursor: Option<String>,
44}
45
46impl AuditLogFilter {
47 fn query(&self) -> Result<Vec<(&'static str, String)>> {
48 let mut query = Vec::new();
49 if let Some(from) = &self.from {
50 query.push(("from", from.to_query()?));
51 }
52 if let Some(to) = &self.to {
53 query.push(("to", to.to_query()?));
54 }
55 let mut text = |name, value: &Option<String>| {
56 if let Some(value) = value {
57 query.push((name, value.clone()));
58 }
59 };
60 text("actor_type", &self.actor_type);
61 text("category", &self.category);
62 text("action", &self.action);
63 text("outcome", &self.outcome);
64 text("resource_type", &self.resource_type);
65 text("resource_id", &self.resource_id);
66 text("cursor", &self.cursor);
67 let ids = [
68 ("actor_user_id", self.actor_user_id),
69 ("zone_id", self.zone_id),
70 ("request_id", self.request_id),
71 ];
72 for (name, id) in ids {
73 if let Some(id) = id {
74 query.push((name, id.to_string()));
75 }
76 }
77 if let Some(limit) = self.limit {
78 query.push(("limit", limit.to_string()));
79 }
80 Ok(query)
81 }
82}
83
84/// Audit log routes, from [`Client::audit_logs`].
85///
86/// A personal key sees its user's own actions plus everything on the zones the user is
87/// Owner or Administrator of (membership as of now, so a new Owner sees the zone's
88/// history); a zone key sees its zone. Some fields (e.g. other people's IP addresses)
89/// are left out for non-Admins.
90pub struct AuditLogs<'a> {
91 client: &'a Client,
92}
93
94impl<'a> AuditLogs<'a> {
95 pub(crate) fn new(client: &'a Client) -> Self {
96 Self { client }
97 }
98
99 /// One page of entries, newest first.
100 ///
101 /// `GET /api/v1/audit-logs`
102 ///
103 /// # Errors
104 ///
105 /// * [`ErrorCode::Validation`](crate::ErrorCode::Validation) for a bad filter, `from`
106 /// not before `to`, a range over 31 days, or a cursor not from this API.
107 /// * [`ErrorCode::AuditDisabled`](crate::ErrorCode::AuditDisabled) if Core runs
108 /// without the audit log.
109 /// * [`Error::InvalidInput`](crate::Error::InvalidInput) for a time before 1970.
110 ///
111 /// # Examples
112 ///
113 /// ```no_run
114 /// use dinio_sdk::api::AuditLogFilter;
115 ///
116 /// # async fn example(client: &dinio_sdk::Client, zone: &dinio_sdk::models::Zone) -> dinio_sdk::Result<()> {
117 /// let mut filter = AuditLogFilter {
118 /// zone_id: Some(zone.uuid),
119 /// action: Some("record.*".into()),
120 /// ..Default::default()
121 /// };
122 /// let first = client.audit_logs().list(&filter).await?;
123 /// if let Some(cursor) = first.next_cursor {
124 /// filter.cursor = Some(cursor);
125 /// let second = client.audit_logs().list(&filter).await?;
126 /// }
127 /// # Ok(())
128 /// # }
129 /// ```
130 pub async fn list(&self, filter: &AuditLogFilter) -> Result<CursorPage<AuditLog>> {
131 let request = self
132 .client
133 .request(Method::GET, &["audit-logs"])?
134 .query(&filter.query()?);
135 let (items, next_cursor) = self.client.send_cursor(request).await?;
136 Ok(CursorPage { items, next_cursor })
137 }
138
139 /// Every matching entry, newest first, following the cursor page by page (500 per
140 /// page unless `limit` says otherwise), from `cursor` if given. Without `to` the range
141 /// ends when the first page is read, so later pages do not shift it.
142 ///
143 /// # Errors
144 ///
145 /// As [`list`](Self::list); entries already read are dropped.
146 pub async fn list_all(&self, filter: &AuditLogFilter) -> Result<Vec<AuditLog>> {
147 let mut page_filter = filter.clone();
148 page_filter.to = Some(
149 filter
150 .to
151 .clone()
152 .unwrap_or_else(|| Timestamp::At(std::time::SystemTime::now())),
153 );
154 page_filter.limit = Some(filter.limit.unwrap_or(MAX_LIMIT));
155 let mut all = Vec::new();
156 loop {
157 let page = self.list(&page_filter).await?;
158 all.extend(page.items);
159 match page.next_cursor {
160 Some(cursor) => page_filter.cursor = Some(cursor),
161 None => return Ok(all),
162 }
163 }
164 }
165}
166
167/// Largest page Core accepts.
168const MAX_LIMIT: u32 = 500;