Skip to main content

quipu_core/
model.rs

1use crate::id::Uid;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5/// Plain field value as supplied by the caller (before any protection is applied).
6///
7/// On disk the JSON variant is carried as its string form (`ValueRepr`):
8/// bincode is a non-self-describing format and cannot deserialize
9/// `serde_json::Value` directly.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(try_from = "ValueRepr", into = "ValueRepr")]
12pub enum Value {
13    Text(String),
14    Number(f64),
15    Json(serde_json::Value),
16}
17
18#[derive(Serialize, Deserialize)]
19enum ValueRepr {
20    Text(String),
21    Number(f64),
22    Json(String),
23}
24
25impl From<Value> for ValueRepr {
26    fn from(v: Value) -> Self {
27        match v {
28            Value::Text(s) => ValueRepr::Text(s),
29            Value::Number(n) => ValueRepr::Number(n),
30            Value::Json(j) => ValueRepr::Json(j.to_string()),
31        }
32    }
33}
34
35impl TryFrom<ValueRepr> for Value {
36    type Error = String;
37
38    fn try_from(r: ValueRepr) -> Result<Self, Self::Error> {
39        Ok(match r {
40            ValueRepr::Text(s) => Value::Text(s),
41            ValueRepr::Number(n) => Value::Number(n),
42            ValueRepr::Json(s) => Value::Json(serde_json::from_str(&s).map_err(|e| e.to_string())?),
43        })
44    }
45}
46
47impl Value {
48    pub fn kind(&self) -> ValueKind {
49        match self {
50            Value::Text(_) => ValueKind::Text,
51            Value::Number(_) => ValueKind::Number,
52            Value::Json(_) => ValueKind::Json,
53        }
54    }
55
56    /// Canonical byte representation used for hashing and for index keys.
57    pub fn canonical_bytes(&self) -> Vec<u8> {
58        match self {
59            Value::Text(s) => s.as_bytes().to_vec(),
60            Value::Number(n) => format!("{n}").into_bytes(),
61            Value::Json(v) => v.to_string().into_bytes(),
62        }
63    }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67pub enum ValueKind {
68    Text,
69    Number,
70    Json,
71}
72
73/// A field value as it sits on disk, after the schema's protection was applied.
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub enum StoredValue {
76    Plain(Value),
77    /// SHA-256 of the canonical bytes (hex). Still searchable: queries hash
78    /// the probe value and compare digests. The original is unrecoverable by
79    /// design (though low-entropy values can be brute-forced — see
80    /// [`crate::schema::FieldProtection::Sha256`]).
81    Sha256(String),
82    /// HMAC-SHA-256 of the canonical bytes (hex), keyed with the store's HMAC
83    /// key of `key_version`. Still searchable: queries MAC the probe value
84    /// under every held key version and compare digests. The original is
85    /// unrecoverable by design, and without the key the digest cannot be
86    /// brute-forced from disk.
87    Hmac {
88        /// [`crate::crypto::KeyVersion`] of the HMAC key the digest was made
89        /// with — recorded at write time so probes survive key rotations.
90        key_version: u32,
91        digest: String,
92    },
93    /// Hybrid encryption: the value is AES-256-GCM encrypted under a random
94    /// data key, which is RSA-OAEP(SHA-256) wrapped with the store's public
95    /// key of `key_version`. Recoverable with that version's private key via
96    /// [`crate::crypto::KeyRing::decrypt`]; not searchable. GCM authenticates
97    /// the ciphertext, so any in-place modification fails decryption.
98    Rsa {
99        /// [`crate::crypto::KeyVersion`] of the RSA keypair whose public key
100        /// wrapped the data key — names the private key that can unwrap it.
101        key_version: u32,
102        wrapped_key: String,
103        nonce: String,
104        ciphertext: String,
105    },
106}
107
108/// Audit-log body column: free text or structured JSON (request/response dumps etc).
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110#[serde(try_from = "ContentRepr", into = "ContentRepr")]
111pub enum Content {
112    Text(String),
113    Json(serde_json::Value),
114}
115
116#[derive(Serialize, Deserialize)]
117enum ContentRepr {
118    Text(String),
119    Json(String),
120}
121
122impl From<Content> for ContentRepr {
123    fn from(c: Content) -> Self {
124        match c {
125            Content::Text(s) => ContentRepr::Text(s),
126            Content::Json(j) => ContentRepr::Json(j.to_string()),
127        }
128    }
129}
130
131impl TryFrom<ContentRepr> for Content {
132    type Error = String;
133
134    fn try_from(r: ContentRepr) -> Result<Self, Self::Error> {
135        Ok(match r {
136            ContentRepr::Text(s) => Content::Text(s),
137            ContentRepr::Json(s) => {
138                Content::Json(serde_json::from_str(&s).map_err(|e| e.to_string())?)
139            }
140        })
141    }
142}
143
144/// One row of the audit log table.
145///
146/// `targets` live in the relation table ([`TargetRelation`]), not here, so a log
147/// can point at any number of entities.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct AuditLog {
150    pub log_id: Uid,
151    /// UTC+0, microseconds since the unix epoch. See [`crate::time`].
152    pub timestamp: u64,
153    /// Version uid of the actor's registry record at record time.
154    pub actor: Uid,
155    pub actor_type: String,
156    /// HTTP method of the audited API call.
157    pub method: String,
158    /// URL of the audited API call.
159    pub url: String,
160    pub content: Content,
161    /// Values for registered custom columns, validated against
162    /// [`crate::schema::CustomColumnDef`] at append time.
163    pub custom: BTreeMap<String, Value>,
164}
165
166/// Relation-table row binding a log to one target entity.
167/// `entity_registry_uid` points at the exact registry *version* that was current
168/// when the log was written, which is what makes as-recorded rendering possible.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct TargetRelation {
171    pub log_id: Uid,
172    pub entity_registry_uid: Uid,
173    pub entity_type: String,
174}