arc_web/helpers/
access_log.rs1use crate::http::errors::AppError;
12use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Responder};
13use arc_core::access_log::{
14 AccessLogger, AccessedResource, FailurePolicy, Identity, PurposeOfUse, Sensitivity,
15};
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19#[derive(Debug)]
26pub struct Sensitive<T> {
27 data: T,
28 sensitivity: Sensitivity,
29}
30
31impl<T> Sensitive<T> {
32 pub fn new(data: T, sensitivity: Sensitivity) -> Self {
33 Self { data, sensitivity }
34 }
35
36 #[allow(dead_code)]
37 pub fn phi(data: T) -> Self {
38 Self::new(data, Sensitivity::Phi)
39 }
40
41 #[allow(dead_code)]
42 pub fn pci(data: T) -> Self {
43 Self::new(data, Sensitivity::Pci)
44 }
45
46 pub fn pii(data: T) -> Self {
47 Self::new(data, Sensitivity::Pii)
48 }
49
50 #[allow(dead_code)]
51 pub fn confidential(data: T) -> Self {
52 Self::new(data, Sensitivity::Confidential)
53 }
54
55 #[allow(dead_code)]
56 pub fn internal(data: T) -> Self {
57 Self::new(data, Sensitivity::Internal)
58 }
59
60 #[allow(dead_code)]
62 pub fn sensitivity(&self) -> Sensitivity {
63 self.sensitivity
64 }
65
66 pub fn into_parts(self) -> (T, Sensitivity) {
69 (self.data, self.sensitivity)
70 }
71}
72
73#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
79pub struct AccessLogged<T> {
80 data: T,
81}
82
83impl<T> AccessLogged<T> {
84 pub fn new(data: T) -> Self {
86 Self { data }
87 }
88
89 pub fn into_inner(self) -> T {
91 self.data
92 }
93}
94
95pub fn identity_from(req: &HttpRequest, actor_id: impl Into<String>) -> Identity {
97 Identity {
98 actor_id: actor_id.into(),
99 session_id: None,
100 source_ip: req
101 .connection_info()
102 .realip_remote_addr()
103 .map(str::to_string),
104 user_agent: req
105 .headers()
106 .get("User-Agent")
107 .and_then(|v| v.to_str().ok())
108 .map(str::to_string),
109 }
110}
111
112pub fn correlation_from(req: &HttpRequest) -> Option<Uuid> {
114 req.headers()
115 .get("X-Correlation-Id")
116 .and_then(|v| v.to_str().ok())
117 .and_then(|s| Uuid::parse_str(s).ok())
118}
119
120pub async fn record_read<T>(
125 logger: &dyn AccessLogger,
126 req: &HttpRequest,
127 actor_id: impl Into<String>,
128 mut resource: AccessedResource,
129 purpose: PurposeOfUse,
130 sensitive: Sensitive<T>,
131) -> Result<AccessLogged<T>, AppError> {
132 let (data, sensitivity) = sensitive.into_parts();
133 resource.sensitivity = sensitivity;
134
135 let policy = FailurePolicy::for_sensitivity(sensitivity);
136 let identity = identity_from(req, actor_id);
137 let correlation = correlation_from(req);
138
139 match logger
140 .log_access(identity, resource, purpose, correlation)
141 .await
142 {
143 Ok(()) => Ok(AccessLogged::new(data)),
144 Err(e) => match policy {
145 FailurePolicy::FailHard => {
146 tracing::error!(
147 error = %e,
148 "access log sink failed on regulated read — failing closed"
149 );
150 Err(AppError::AuditFailed {
151 status: StatusCode::SERVICE_UNAVAILABLE,
152 message: "Audit sink unavailable".into(),
153 })
154 }
155 FailurePolicy::FailOpenWarn => {
156 tracing::warn!(error = %e, "access log sink rejected read");
157 Ok(AccessLogged::new(data))
158 }
159 },
160 }
161}
162
163impl<T: Serialize> Responder for AccessLogged<T> {
164 type Body = actix_web::body::BoxBody;
165
166 fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
167 HttpResponse::Ok().json(self.into_inner())
168 }
169}