use std::borrow::Cow;
use std::fmt;
use std::net::IpAddr;
pub mod recording;
mod write;
use fhir_types::r4::coding::Coding;
use fhir_types::r4::primitives;
use fhir_types::r4::schema::SCHEMAS;
use jiff::Timestamp;
use secrecy::{SecretSlice, SecretString};
use url::Url;
use crate::redact::{REDACTED, RedactedUrl};
use crate::user::OnBehalfOf;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Coded {
pub system: Cow<'static, str>,
pub code: Cow<'static, str>,
pub display: Cow<'static, str>,
}
impl Coded {
#[must_use]
pub const fn fixed(system: &'static str, code: &'static str, display: &'static str) -> Self {
Self {
system: Cow::Borrowed(system),
code: Cow::Borrowed(code),
display: Cow::Borrowed(display),
}
}
#[must_use]
pub fn new(
system: impl Into<String>,
code: impl Into<String>,
display: impl Into<String>,
) -> Self {
Self {
system: Cow::Owned(system.into()),
code: Cow::Owned(code.into()),
display: Cow::Owned(display.into()),
}
}
fn coding(&self) -> Coding {
Coding {
system: Some(primitives::Uri::from(self.system.as_ref())),
code: Some(primitives::Code::from(self.code.as_ref())),
display: Some(primitives::String::from(self.display.as_ref())),
..Coding::default()
}
}
}
pub const IHE_TRANSACTIONS: &str = "urn:ihe:event-type-code";
pub const REST: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/audit-event-type",
"rest",
"Restful Operation",
);
pub const SOURCE_ROLE: Coded = Coded::fixed(
"http://dicom.nema.org/resources/ontology/DCM",
"110153",
"Source Role ID",
);
pub const DESTINATION_ROLE: Coded = Coded::fixed(
"http://dicom.nema.org/resources/ontology/DCM",
"110152",
"Destination Role ID",
);
pub const APPLICATION: Coded = Coded::fixed(
"http://dicom.nema.org/resources/ontology/DCM",
"110150",
"Application",
);
pub const INFORMATION_RECIPIENT: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/v3-ParticipationType",
"IRCP",
"information recipient",
);
pub const CUSTODIAN: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/provenance-participant-type",
"custodian",
"Custodian",
);
pub const SEARCH: Coded = Coded::fixed(
"http://hl7.org/fhir/restful-interaction",
"search",
"search",
);
pub const READ: Coded = Coded::fixed("http://hl7.org/fhir/restful-interaction", "read", "read");
pub const CREATE: Coded = Coded::fixed(
"http://hl7.org/fhir/restful-interaction",
"create",
"create",
);
pub const UPDATE: Coded = Coded::fixed(
"http://hl7.org/fhir/restful-interaction",
"update",
"update",
);
pub const DELETE: Coded = Coded::fixed(
"http://hl7.org/fhir/restful-interaction",
"delete",
"delete",
);
pub const HISTORY_TYPE: Coded = Coded::fixed(
"http://hl7.org/fhir/restful-interaction",
"history-type",
"history-type",
);
pub const SYSTEM_OBJECT: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/audit-entity-type",
"2",
"System Object",
);
pub const OTHER: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/audit-entity-type",
"4",
"Other",
);
pub const REQUEST_ID: Coded = Coded::fixed(
"https://profiles.ihe.net/ITI/BALP/CodeSystem/BasicAuditEntityType",
"XrequestId",
"X-Request-Id",
);
pub const PERSON: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/audit-entity-type",
"1",
"Person",
);
pub const QUERY_ROLE: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/object-role",
"24",
"Query",
);
pub const DOMAIN_RESOURCE: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/object-role",
"4",
"Domain Resource",
);
pub const PATIENT_ROLE: Coded = Coded::fixed(
"http://terminology.hl7.org/CodeSystem/object-role",
"1",
"Patient",
);
pub const DCM: &str = "http://dicom.nema.org/resources/ontology/DCM";
pub const USER_AUTHENTICATION: Coded = Coded::fixed(DCM, "110114", "User Authentication");
pub const LOGIN: Coded = Coded::fixed(DCM, "110122", "Login");
pub const LOGOUT: Coded = Coded::fixed(DCM, "110123", "Logout");
pub const EXPORT: Coded = Coded::fixed(DCM, "110106", "Export");
pub const IMPORT: Coded = Coded::fixed(DCM, "110107", "Import");
pub const OPERATION: Coded = Coded::fixed(
"http://hl7.org/fhir/restful-interaction",
"operation",
"operation",
);
pub const SECURITY_SOURCE_TYPE: &str = "http://terminology.hl7.org/CodeSystem/security-source-type";
pub const SOURCE_USER_DEVICE: Coded = Coded::fixed(SECURITY_SOURCE_TYPE, "1", "User Device");
pub const SOURCE_DATA_INTERFACE: Coded = Coded::fixed(SECURITY_SOURCE_TYPE, "2", "Data Interface");
pub const SOURCE_WEB_SERVER: Coded = Coded::fixed(SECURITY_SOURCE_TYPE, "3", "Web Server");
pub const SOURCE_APPLICATION_SERVER: Coded =
Coded::fixed(SECURITY_SOURCE_TYPE, "4", "Application Server");
pub const SOURCE_DATABASE_SERVER: Coded =
Coded::fixed(SECURITY_SOURCE_TYPE, "5", "Database Server");
pub const SOURCE_SECURITY_SERVER: Coded =
Coded::fixed(SECURITY_SOURCE_TYPE, "6", "Security Server");
pub const SOURCE_NETWORK_DEVICE: Coded = Coded::fixed(SECURITY_SOURCE_TYPE, "7", "Network Device");
pub const SOURCE_NETWORK_ROUTER: Coded = Coded::fixed(SECURITY_SOURCE_TYPE, "8", "Network Router");
pub const SOURCE_OTHER: Coded = Coded::fixed(SECURITY_SOURCE_TYPE, "9", "Other");
pub const OAUTH_COMPREHENSIVE: &str = "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.OAUTHaccessTokenUse.Comprehensive";
pub const PATIENT_NAMESPACE: &str = "namespace";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventKind {
pub profile: &'static str,
pub event_type: Coded,
pub subtypes: Vec<Coded>,
pub action: &'static str,
pub client: Coded,
pub server: Coded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Outcome {
Success,
MinorFailure,
SeriousFailure,
MajorFailure,
}
impl Outcome {
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::Success => "0",
Self::MinorFailure => "4",
Self::SeriousFailure => "8",
Self::MajorFailure => "12",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum NetworkAddress {
MachineName(String),
Ip(IpAddr),
Uri(Url),
}
impl NetworkAddress {
#[must_use]
pub fn host(host: &str) -> Self {
host.parse::<IpAddr>()
.map_or_else(|_| Self::MachineName(host.to_owned()), Self::Ip)
}
#[must_use]
pub fn uri(url: &Url) -> Self {
Self::Uri(bare(url))
}
#[must_use]
pub fn type_code(&self) -> &'static str {
match self {
Self::MachineName(_) => "1",
Self::Ip(_) => "2",
Self::Uri(_) => "5",
}
}
#[must_use]
pub fn address(&self) -> String {
match self {
Self::MachineName(name) => name.clone(),
Self::Ip(address) => address.to_string(),
Self::Uri(url) => url.to_string(),
}
}
}
#[must_use]
pub fn request_text(url: &Url) -> SecretString {
let mut shown = url.clone();
let _cleared: Result<(), ()> = shown.set_username("");
let _cleared: Result<(), ()> = shown.set_password(None);
shown.set_fragment(None);
SecretString::from(String::from(shown))
}
fn bare(url: &Url) -> Url {
let mut shown = url.clone();
let _cleared: Result<(), ()> = shown.set_username("");
let _cleared: Result<(), ()> = shown.set_password(None);
shown.set_query(None);
shown.set_fragment(None);
shown
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Peer {
pub who: String,
pub network: NetworkAddress,
}
impl Peer {
#[must_use]
pub fn server(base: &Url) -> Self {
let shown = bare(base);
Self {
who: shown.to_string(),
network: NetworkAddress::Uri(shown),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Direction {
Sent {
server: Peer,
},
Received {
client: Peer,
endpoint: NetworkAddress,
},
}
#[derive(Clone)]
#[non_exhaustive]
pub enum Entity {
Query(SecretString),
Patient {
system: Option<String>,
value: SecretString,
namespace: Option<String>,
},
PatientReference(SecretString),
Resource {
reference: Option<String>,
resource_type: &'static str,
kind: Coded,
role: Option<Coded>,
name: Option<&'static str>,
query: Option<String>,
},
Described(Described),
}
#[derive(Clone)]
pub struct Described {
pub what: Option<What>,
pub kind: Coded,
pub role: Option<Coded>,
pub name: Option<String>,
pub description: Option<String>,
pub details: Vec<Detail>,
pub security_labels: Vec<SecurityLabel>,
}
#[derive(Clone)]
#[non_exhaustive]
pub enum What {
Reference {
reference: String,
resource_type: Option<String>,
},
Identifier {
system: Option<String>,
value: String,
},
}
#[must_use]
pub fn is_literal_reference(reference: &str) -> bool {
match Url::parse(reference) {
Ok(url) => matches!(url.scheme(), "http" | "https") && url.has_host(),
Err(_) => is_relative_reference(reference),
}
}
fn is_relative_reference(reference: &str) -> bool {
let mut segments = reference.split('/');
let (Some(resource_type), Some(id)) = (segments.next(), segments.next()) else {
return false;
};
if !SCHEMAS.is_resource(resource_type) || !is_id(id) {
return false;
}
match (segments.next(), segments.next(), segments.next()) {
(None, _, _) => true,
(Some("_history"), Some(version), None) => is_id(version),
_ => false,
}
}
fn is_id(value: &str) -> bool {
(1..=64).contains(&value.len())
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.')
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecurityLabel {
pub system: String,
pub version: Option<String>,
pub code: String,
}
#[derive(Clone)]
pub struct Detail {
pub kind: String,
pub value: String,
}
impl Detail {
#[must_use]
pub fn new(kind: impl Into<String>, value: impl Into<String>) -> Self {
Self {
kind: kind.into(),
value: value.into(),
}
}
}
impl fmt::Debug for Described {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let details: Vec<&str> = self
.details
.iter()
.map(|detail| detail.kind.as_str())
.collect();
f.debug_struct("Described")
.field("what", &self.what)
.field("kind", &self.kind)
.field("role", &self.role)
.field("details", &details)
.field("security_labels", &self.security_labels)
.finish_non_exhaustive()
}
}
impl fmt::Debug for What {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Reference { .. } => f.debug_tuple("Reference").field(&REDACTED).finish(),
Self::Identifier { .. } => f.debug_tuple("Identifier").field(&REDACTED).finish(),
}
}
}
impl fmt::Debug for Detail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Detail")
.field("kind", &self.kind)
.field("value", &REDACTED)
.finish()
}
}
impl fmt::Debug for Entity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Query(_) => f.debug_tuple("Query").field(&REDACTED).finish(),
Self::PatientReference(_) => {
f.debug_tuple("PatientReference").field(&REDACTED).finish()
}
Self::Patient {
system, namespace, ..
} => f
.debug_struct("Patient")
.field("system", system)
.field("value", &REDACTED)
.field("namespace", namespace)
.finish(),
Self::Resource {
reference,
resource_type,
kind,
role,
name,
query,
} => f
.debug_struct("Resource")
.field("reference", reference)
.field("resource_type", resource_type)
.field("kind", kind)
.field("role", role)
.field("name", name)
.field("query", query)
.finish(),
Self::Described(described) => described.fmt(f),
}
}
}
#[derive(Clone)]
pub struct Exchange {
pub kind: EventKind,
pub recorded: Timestamp,
pub outcome: Outcome,
pub outcome_description: Option<String>,
pub direction: Direction,
pub on_behalf: OnBehalfOf,
pub entities: Vec<Entity>,
pub security: Vec<SecurityLabel>,
}
impl fmt::Debug for Exchange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let direction = match &self.direction {
Direction::Sent { server } => format!("sent to {:?}", RedactedUrl(&server.who)),
Direction::Received { endpoint, .. } => {
format!("received at {:?}", RedactedUrl(&endpoint.address()))
}
};
f.debug_struct("Exchange")
.field("profile", &self.kind.profile)
.field("recorded", &self.recorded)
.field("outcome", &self.outcome)
.field("outcome_description", &self.outcome_description.is_some())
.field("direction", &direction)
.field("on_behalf", &self.on_behalf)
.field("entities", &self.entities)
.field("security", &self.security)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Observer {
pub source_id: String,
pub site: Option<String>,
pub host: NetworkAddress,
pub source_types: Vec<Coded>,
}
pub struct AuditRecord(SecretSlice<u8>);
impl AuditRecord {
#[must_use]
pub fn into_bytes(self) -> SecretSlice<u8> {
self.0
}
}
impl fmt::Debug for AuditRecord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("AuditRecord").field(&REDACTED).finish()
}
}
#[expect(
clippy::disallowed_types,
reason = "the seam to a caller's JSON store: the record leaves as a value behind this redacting type"
)]
pub struct AuditEventValue(serde_json::Value);
impl AuditEventValue {
#[must_use]
#[expect(
clippy::disallowed_types,
reason = "the seam to a caller's JSON store: the record leaves as a value behind this redacting type"
)]
pub fn into_value(self) -> serde_json::Value {
self.0
}
}
impl fmt::Debug for AuditEventValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("AuditEventValue").field(&REDACTED).finish()
}
}
#[derive(Debug, thiserror::Error)]
#[error("the AuditEvent could not be written")]
pub struct RecordError(#[source] serde_json::Error);
#[async_trait::async_trait]
pub trait AuditRecorder: Send + Sync {
async fn record(&self, exchange: Exchange) -> Result<(), AuditError>;
async fn reserve(&self) -> Result<Option<Box<dyn Reserved>>, AuditError> {
Ok(None)
}
}
#[async_trait::async_trait]
pub trait Reserved: Send + fmt::Debug {
async fn record(self: Box<Self>, exchange: Exchange) -> Result<(), AuditError>;
}
#[derive(Debug, thiserror::Error)]
#[error("the audit recorder could not accept the audit record")]
pub struct AuditError(#[source] pub Box<dyn std::error::Error + Send + Sync>);