forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
1.7 KiB
1//! Writes to the security audit log. Recording never fails the request: a store error is logged
2//! and the action continues. Passwords and tokens never go into an entry.
3
4use forge_core::{AuditAction, AuditEntry, Store, User};
5use serde_json::Value;
6use topcoat::context::Cx;
7
8use crate::client_ip::client_ip;
9
10/// Who did it. `Anonymous` is an unauthenticated attempt, `Cli` is the admin command line.
11pub enum Actor<'a> {
12 User(&'a User),
13 Anonymous,
14 Cli,
15}
16
17/// What it was done to. Empty kind and id for actions with no object (such as a login).
18#[derive(Default)]
19pub struct Target<'a> {
20 pub kind: &'a str,
21 pub id: &'a str,
22 pub name: &'a str,
23}
24
25/// Appends one entry. `cx` supplies the client IP for web requests; the CLI passes `None` and
26/// gets an empty IP. `data` is an optional JSON object (`Value::Null` stores `{}`).
27pub async fn record(
28 store: &Store,
29 cx: Option<&Cx>,
30 actor: Actor<'_>,
31 action: AuditAction,
32 target: Target<'_>,
33 data: Value,
34) {
35 let mut entry = AuditEntry::new(action);
36 match actor {
37 Actor::User(user) => {
38 entry.actor_id = Some(user.id.clone());
39 entry.actor_name = user.username.clone();
40 }
41 Actor::Anonymous => {}
42 Actor::Cli => entry.actor_name = "cli".to_string(),
43 }
44 entry.target_kind = target.kind.to_string();
45 entry.target_id = target.id.to_string();
46 entry.target_name = target.name.to_string();
47 entry.ip = cx.map(|cx| client_ip(cx).to_string()).unwrap_or_default();
48 if !data.is_null() {
49 entry.data = data.to_string();
50 }
51 if let Err(err) = store.record_audit(&entry).await {
52 tracing::error!(action = action.as_str(), error = %err, "failed to record audit entry");
53 }
54}