use crate::http::errors::AppError;
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Responder};
use arc_core::access_log::{
AccessLogger, AccessedResource, FailurePolicy, Identity, PurposeOfUse, Sensitivity,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug)]
pub struct Sensitive<T> {
data: T,
sensitivity: Sensitivity,
}
impl<T> Sensitive<T> {
pub fn new(data: T, sensitivity: Sensitivity) -> Self {
Self { data, sensitivity }
}
#[allow(dead_code)]
pub fn phi(data: T) -> Self {
Self::new(data, Sensitivity::Phi)
}
#[allow(dead_code)]
pub fn pci(data: T) -> Self {
Self::new(data, Sensitivity::Pci)
}
pub fn pii(data: T) -> Self {
Self::new(data, Sensitivity::Pii)
}
#[allow(dead_code)]
pub fn confidential(data: T) -> Self {
Self::new(data, Sensitivity::Confidential)
}
#[allow(dead_code)]
pub fn internal(data: T) -> Self {
Self::new(data, Sensitivity::Internal)
}
#[allow(dead_code)]
pub fn sensitivity(&self) -> Sensitivity {
self.sensitivity
}
pub fn into_parts(self) -> (T, Sensitivity) {
(self.data, self.sensitivity)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AccessLogged<T> {
data: T,
}
impl<T> AccessLogged<T> {
pub fn new(data: T) -> Self {
Self { data }
}
pub fn into_inner(self) -> T {
self.data
}
}
pub fn identity_from(req: &HttpRequest, actor_id: impl Into<String>) -> Identity {
Identity {
actor_id: actor_id.into(),
session_id: None,
source_ip: req
.connection_info()
.realip_remote_addr()
.map(str::to_string),
user_agent: req
.headers()
.get("User-Agent")
.and_then(|v| v.to_str().ok())
.map(str::to_string),
}
}
pub fn correlation_from(req: &HttpRequest) -> Option<Uuid> {
req.headers()
.get("X-Correlation-Id")
.and_then(|v| v.to_str().ok())
.and_then(|s| Uuid::parse_str(s).ok())
}
pub async fn record_read<T>(
logger: &dyn AccessLogger,
req: &HttpRequest,
actor_id: impl Into<String>,
mut resource: AccessedResource,
purpose: PurposeOfUse,
sensitive: Sensitive<T>,
) -> Result<AccessLogged<T>, AppError> {
let (data, sensitivity) = sensitive.into_parts();
resource.sensitivity = sensitivity;
let policy = FailurePolicy::for_sensitivity(sensitivity);
let identity = identity_from(req, actor_id);
let correlation = correlation_from(req);
match logger
.log_access(identity, resource, purpose, correlation)
.await
{
Ok(()) => Ok(AccessLogged::new(data)),
Err(e) => match policy {
FailurePolicy::FailHard => {
tracing::error!(
error = %e,
"access log sink failed on regulated read — failing closed"
);
Err(AppError::AuditFailed {
status: StatusCode::SERVICE_UNAVAILABLE,
message: "Audit sink unavailable".into(),
})
}
FailurePolicy::FailOpenWarn => {
tracing::warn!(error = %e, "access log sink rejected read");
Ok(AccessLogged::new(data))
}
},
}
}
impl<T: Serialize> Responder for AccessLogged<T> {
type Body = actix_web::body::BoxBody;
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
HttpResponse::Ok().json(self.into_inner())
}
}