Skip to main content

rustfs_audit/
entity.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use hashbrown::HashMap;
16use jiff::Timestamp;
17use rustfs_s3_types::EventName;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20
21/// ObjectVersion represents an object version with key and versionId
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
23pub struct ObjectVersion {
24    #[serde(rename = "objectName")]
25    pub object_name: String,
26    #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")]
27    pub version_id: Option<String>,
28}
29
30impl ObjectVersion {
31    pub fn new(object_name: String, version_id: Option<String>) -> Self {
32        Self { object_name, version_id }
33    }
34}
35
36/// `ApiDetails` contains API information for the audit entry.
37#[derive(Debug, Clone, Serialize, Deserialize, Default)]
38pub struct ApiDetails {
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub name: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub bucket: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub object: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub objects: Option<Vec<ObjectVersion>>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub status: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub status_code: Option<i32>,
51    #[serde(rename = "rx", skip_serializing_if = "Option::is_none")]
52    pub input_bytes: Option<i64>,
53    #[serde(rename = "tx", skip_serializing_if = "Option::is_none")]
54    pub output_bytes: Option<i64>,
55    #[serde(rename = "txHeaders", skip_serializing_if = "Option::is_none")]
56    pub header_bytes: Option<i64>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub time_to_first_byte: Option<String>,
59    #[serde(rename = "timeToFirstByteInNS", skip_serializing_if = "Option::is_none")]
60    pub time_to_first_byte_in_ns: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub time_to_response: Option<String>,
63    #[serde(rename = "timeToResponseInNS", skip_serializing_if = "Option::is_none")]
64    pub time_to_response_in_ns: Option<String>,
65}
66
67/// Builder for `ApiDetails`.
68#[derive(Default, Clone)]
69pub struct ApiDetailsBuilder(pub ApiDetails);
70
71impl ApiDetailsBuilder {
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    pub fn name(mut self, name: impl Into<String>) -> Self {
77        self.0.name = Some(name.into());
78        self
79    }
80
81    pub fn bucket(mut self, bucket: impl Into<String>) -> Self {
82        self.0.bucket = Some(bucket.into());
83        self
84    }
85
86    pub fn object(mut self, object: impl Into<String>) -> Self {
87        self.0.object = Some(object.into());
88        self
89    }
90
91    pub fn objects(mut self, objects: Vec<ObjectVersion>) -> Self {
92        self.0.objects = Some(objects);
93        self
94    }
95
96    pub fn status(mut self, status: impl Into<String>) -> Self {
97        self.0.status = Some(status.into());
98        self
99    }
100
101    pub fn status_code(mut self, code: i32) -> Self {
102        self.0.status_code = Some(code);
103        self
104    }
105
106    pub fn input_bytes(mut self, bytes: i64) -> Self {
107        self.0.input_bytes = Some(bytes);
108        self
109    }
110
111    pub fn output_bytes(mut self, bytes: i64) -> Self {
112        self.0.output_bytes = Some(bytes);
113        self
114    }
115
116    pub fn header_bytes(mut self, bytes: i64) -> Self {
117        self.0.header_bytes = Some(bytes);
118        self
119    }
120
121    pub fn time_to_first_byte(mut self, t: impl Into<String>) -> Self {
122        self.0.time_to_first_byte = Some(t.into());
123        self
124    }
125
126    pub fn time_to_first_byte_in_ns(mut self, t: impl Into<String>) -> Self {
127        self.0.time_to_first_byte_in_ns = Some(t.into());
128        self
129    }
130
131    pub fn time_to_response(mut self, t: impl Into<String>) -> Self {
132        self.0.time_to_response = Some(t.into());
133        self
134    }
135
136    pub fn time_to_response_in_ns(mut self, t: impl Into<String>) -> Self {
137        self.0.time_to_response_in_ns = Some(t.into());
138        self
139    }
140
141    pub fn build(self) -> ApiDetails {
142        self.0
143    }
144}
145
146/// `AuditEntry` represents an audit log entry.
147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148pub struct AuditEntry {
149    pub version: String,
150    #[serde(rename = "deploymentid", skip_serializing_if = "Option::is_none")]
151    pub deployment_id: Option<String>,
152    #[serde(rename = "siteName", skip_serializing_if = "Option::is_none")]
153    pub site_name: Option<String>,
154    #[serde(with = "jiff::fmt::serde::timestamp::millisecond::required")]
155    pub time: Timestamp,
156    pub event: EventName,
157    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
158    pub entry_type: Option<String>,
159    pub trigger: String,
160    pub api: ApiDetails,
161    #[serde(rename = "remotehost", skip_serializing_if = "Option::is_none")]
162    pub remote_host: Option<String>,
163    // Historical external audit contract: keep `requestID` instead of normalizing
164    // this field to `request_id` or `request-id`.
165    #[serde(rename = "requestID", skip_serializing_if = "Option::is_none")]
166    pub request_id: Option<String>,
167    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
168    pub user_agent: Option<String>,
169    #[serde(rename = "requestPath", skip_serializing_if = "Option::is_none")]
170    pub req_path: Option<String>,
171    #[serde(rename = "requestHost", skip_serializing_if = "Option::is_none")]
172    pub req_host: Option<String>,
173    #[serde(rename = "requestNode", skip_serializing_if = "Option::is_none")]
174    pub req_node: Option<String>,
175    #[serde(rename = "requestClaims", skip_serializing_if = "Option::is_none")]
176    pub req_claims: Option<HashMap<String, Value>>,
177    #[serde(rename = "requestQuery", skip_serializing_if = "Option::is_none")]
178    pub req_query: Option<HashMap<String, String>>,
179    #[serde(rename = "requestHeader", skip_serializing_if = "Option::is_none")]
180    pub req_header: Option<HashMap<String, String>>,
181    #[serde(rename = "responseHeader", skip_serializing_if = "Option::is_none")]
182    pub resp_header: Option<HashMap<String, String>>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub tags: Option<HashMap<String, Value>>,
185    #[serde(rename = "accessKey", skip_serializing_if = "Option::is_none")]
186    pub access_key: Option<String>,
187    #[serde(rename = "parentUser", skip_serializing_if = "Option::is_none")]
188    pub parent_user: Option<String>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub error: Option<String>,
191}
192
193/// Constructor for `AuditEntry`.
194pub struct AuditEntryBuilder(AuditEntry);
195
196impl AuditEntryBuilder {
197    /// Create a new builder with all required fields.
198    pub fn new(version: impl Into<String>, event: EventName, trigger: impl Into<String>, api: ApiDetails) -> Self {
199        Self(AuditEntry {
200            version: version.into(),
201            time: Timestamp::now(),
202            event,
203            trigger: trigger.into(),
204            api,
205            ..Default::default()
206        })
207    }
208
209    // event
210    pub fn version(mut self, version: impl Into<String>) -> Self {
211        self.0.version = version.into();
212        self
213    }
214
215    pub fn event(mut self, event: EventName) -> Self {
216        self.0.event = event;
217        self
218    }
219
220    pub fn api(mut self, api_details: ApiDetails) -> Self {
221        self.0.api = api_details;
222        self
223    }
224
225    pub fn deployment_id(mut self, id: impl Into<String>) -> Self {
226        self.0.deployment_id = Some(id.into());
227        self
228    }
229
230    pub fn site_name(mut self, name: impl Into<String>) -> Self {
231        self.0.site_name = Some(name.into());
232        self
233    }
234
235    pub fn time(mut self, time: Timestamp) -> Self {
236        self.0.time = time;
237        self
238    }
239
240    pub fn entry_type(mut self, entry_type: impl Into<String>) -> Self {
241        self.0.entry_type = Some(entry_type.into());
242        self
243    }
244
245    pub fn remote_host(mut self, host: impl Into<String>) -> Self {
246        self.0.remote_host = Some(host.into());
247        self
248    }
249
250    pub fn request_id(mut self, id: impl Into<String>) -> Self {
251        self.0.request_id = Some(id.into());
252        self
253    }
254
255    pub fn user_agent(mut self, agent: impl Into<String>) -> Self {
256        self.0.user_agent = Some(agent.into());
257        self
258    }
259
260    pub fn req_path(mut self, path: impl Into<String>) -> Self {
261        self.0.req_path = Some(path.into());
262        self
263    }
264
265    pub fn req_host(mut self, host: impl Into<String>) -> Self {
266        self.0.req_host = Some(host.into());
267        self
268    }
269
270    pub fn req_node(mut self, node: impl Into<String>) -> Self {
271        self.0.req_node = Some(node.into());
272        self
273    }
274
275    pub fn req_claims(mut self, claims: HashMap<String, Value>) -> Self {
276        self.0.req_claims = Some(claims);
277        self
278    }
279
280    pub fn req_query(mut self, query: HashMap<String, String>) -> Self {
281        self.0.req_query = Some(query);
282        self
283    }
284
285    pub fn req_header(mut self, header: HashMap<String, String>) -> Self {
286        self.0.req_header = Some(header);
287        self
288    }
289
290    pub fn resp_header(mut self, header: HashMap<String, String>) -> Self {
291        self.0.resp_header = Some(header);
292        self
293    }
294
295    pub fn tags(mut self, tags: HashMap<String, Value>) -> Self {
296        self.0.tags = Some(tags);
297        self
298    }
299
300    pub fn access_key(mut self, key: impl Into<String>) -> Self {
301        self.0.access_key = Some(key.into());
302        self
303    }
304
305    pub fn parent_user(mut self, user: impl Into<String>) -> Self {
306        self.0.parent_user = Some(user.into());
307        self
308    }
309
310    pub fn error(mut self, error: impl Into<String>) -> Self {
311        self.0.error = Some(error.into());
312        self
313    }
314
315    /// Construct the final `AuditEntry`.
316    pub fn build(self) -> AuditEntry {
317        self.0
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use serde_json::Value;
325
326    #[test]
327    fn audit_entry_serializes_historical_request_id_field_name() {
328        let entry = AuditEntryBuilder::new(
329            "1",
330            EventName::ObjectCreatedPut,
331            "s3",
332            ApiDetailsBuilder::new()
333                .name("PutObject")
334                .status("OK")
335                .status_code(200)
336                .build(),
337        )
338        .request_id("req-audit-123")
339        .build();
340
341        let value = serde_json::to_value(entry).expect("audit entry should serialize");
342        assert_eq!(value["requestID"], Value::String("req-audit-123".to_string()));
343        assert!(value.get("request_id").is_none(), "historical audit contract must not expose request_id");
344    }
345
346    #[test]
347    fn audit_entry_time_serializes_as_epoch_milliseconds() {
348        let entry = AuditEntryBuilder::new(
349            "1",
350            EventName::ObjectCreatedPut,
351            "s3",
352            ApiDetailsBuilder::new()
353                .name("PutObject")
354                .status("OK")
355                .status_code(200)
356                .build(),
357        )
358        .time(Timestamp::from_millisecond(1_711_423_698_870).expect("timestamp should be valid"))
359        .build();
360
361        let value = serde_json::to_value(entry).expect("audit entry should serialize");
362        assert_eq!(value["time"], Value::Number(1_711_423_698_870_i64.into()));
363    }
364}