Skip to main content

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;