Skip to main content

cortex/models/
audit.rs

1// Copyright 2015-2025 Deyan Ginev. See the LICENSE
2// file at the top-level directory of this distribution.
3//
4// Licensed under the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>.
5// This file may not be copied, modified, or distributed
6// except according to those terms.
7
8//! The **accounting** pillar (AAA — `docs/archive/AAA_DESIGN.md`): a persistent record of admin
9//! actions and who took them ("observability of actions taken"). Auth-agnostic — `actor` is
10//! whatever the auth layer resolved (today an admin token's `owner`), so this survives any future
11//! auth upgrade.
12
13use chrono::NaiveDateTime;
14use diesel::prelude::*;
15use diesel::result::Error;
16
17use crate::schema::audit_log;
18
19/// A recorded admin action (the read model for the audit view). Not `Serialize` directly — like the
20/// other timestamped models, the JSON/API surface uses a DTO with a formatted-string `at` (chrono's
21/// serde feature is intentionally off crate-wide).
22#[derive(Queryable, Identifiable, Debug, Clone)]
23#[diesel(table_name = audit_log)]
24pub struct AuditEntry {
25  /// Auto-incremented id.
26  pub id: i64,
27  /// The identity that acted (the signed-in admin / API token owner; empty if unresolved).
28  pub actor: String,
29  /// What was done — a stable verb (`rerun`, `import_corpus`, `deactivate_service`, …).
30  pub action: String,
31  /// The resource acted on (e.g. `corpus` or `corpus/service`); may be empty.
32  pub target: String,
33  /// The result (an HTTP status or `ok`/`denied`); may be empty.
34  pub outcome: String,
35  /// Optional short context (a params summary); never secrets.
36  pub details: String,
37  /// When it happened (server clock).
38  pub at: NaiveDateTime,
39}
40
41/// An admin action to record.
42#[derive(Insertable, Debug)]
43#[diesel(table_name = audit_log)]
44pub struct NewAuditEntry {
45  /// The acting identity (the token's `owner` / signed-in admin).
46  pub actor: String,
47  /// The action verb.
48  pub action: String,
49  /// The resource acted on.
50  pub target: String,
51  /// The outcome.
52  pub outcome: String,
53  /// Optional short context.
54  pub details: String,
55}
56
57impl NewAuditEntry {
58  /// A minimal entry: actor + action + target, no extra outcome/details.
59  pub fn new(
60    actor: impl Into<String>,
61    action: impl Into<String>,
62    target: impl Into<String>,
63  ) -> Self {
64    NewAuditEntry {
65      actor: actor.into(),
66      action: action.into(),
67      target: target.into(),
68      outcome: String::new(),
69      details: String::new(),
70    }
71  }
72
73  /// Sets the outcome (e.g. `ok`, `denied`, an HTTP status), builder-style.
74  pub fn outcome(mut self, outcome: impl Into<String>) -> Self {
75    self.outcome = outcome.into();
76    self
77  }
78
79  /// Records the action. **Best-effort**: the caller should ignore an error (a failed audit write
80  /// must never fail the action it describes — accounting is observability, not a gate).
81  pub fn record(&self, connection: &mut PgConnection) -> Result<usize, Error> {
82    diesel::insert_into(audit_log::table)
83      .values(self)
84      .execute(connection)
85  }
86}
87
88impl AuditEntry {
89  /// Lists audit entries, most-recent first, optionally filtered to a single `actor`, capped at
90  /// `limit` and starting at `offset` (for page-based pagination). The caller is responsible for
91  /// clamping `limit`/`offset` to sane bounds (see the read view).
92  pub fn list(
93    connection: &mut PgConnection,
94    actor: Option<&str>,
95    limit: i64,
96    offset: i64,
97  ) -> Result<Vec<Self>, Error> {
98    let mut query = audit_log::table.into_boxed();
99    if let Some(actor) = actor {
100      query = query.filter(audit_log::actor.eq(actor));
101    }
102    query
103      .order(audit_log::at.desc())
104      .limit(limit)
105      .offset(offset)
106      .get_results(connection)
107  }
108
109  /// Lists recent audit entries, most-recent first, capped at `limit` (no actor filter).
110  pub fn recent(connection: &mut PgConnection, limit: i64) -> Result<Vec<Self>, Error> {
111    Self::list(connection, None, limit, 0)
112  }
113}