use std::fmt;
use std::net::IpAddr;
use jiff::Timestamp;
use secrecy::SecretString;
use url::Url;
use super::identifier::HomeCommunityId;
use ihe_atna::redact::{REDACTED, RedactedUrl};
use ihe_atna::user::OnBehalfOf;
pub const EVENT_ID: (&str, &str, &str) = ("110112", "DCM", "Query");
pub const EVENT_ACTION: &str = "E";
pub const EVENT_TYPE: (&str, &str, &str) = (
"ITI-55",
"IHE Transactions",
"Cross Gateway Patient Discovery",
);
pub const HOME_COMMUNITY_DETAIL: &str = "ihe:homeCommunityID";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EventOutcome {
Success,
MinorFailure,
SeriousFailure,
}
impl EventOutcome {
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::Success => "0",
Self::MinorFailure => "4",
Self::SeriousFailure => "8",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NetworkAccessPoint {
MachineName(String),
IpAddress(IpAddr),
}
impl NetworkAccessPoint {
#[must_use]
pub fn of(url: &Url) -> Option<Self> {
match url.host()? {
url::Host::Domain(name) => Some(Self::MachineName(name.to_owned())),
url::Host::Ipv4(address) => Some(Self::IpAddress(IpAddr::V4(address))),
url::Host::Ipv6(address) => Some(Self::IpAddress(IpAddr::V6(address))),
}
}
#[must_use]
pub fn type_code(&self) -> &'static str {
match self {
Self::MachineName(_) => "1",
Self::IpAddress(_) => "2",
}
}
#[must_use]
pub fn id(&self) -> String {
match self {
Self::MachineName(name) => name.clone(),
Self::IpAddress(address) => address.to_string(),
}
}
}
#[derive(Clone)]
pub struct AuditEvent {
pub date_time: Timestamp,
pub outcome: EventOutcome,
pub process_id: u32,
pub destination: Url,
pub destination_access_point: Option<NetworkAccessPoint>,
pub query: SecretString,
pub home_community: Option<HomeCommunityId>,
pub on_behalf: OnBehalfOf,
}
impl fmt::Debug for AuditEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuditEvent")
.field("date_time", &self.date_time)
.field("outcome", &self.outcome)
.field("process_id", &self.process_id)
.field("destination", &RedactedUrl(self.destination.as_str()))
.field("destination_access_point", &self.destination_access_point)
.field("query", &REDACTED)
.field("home_community", &self.home_community)
.field("on_behalf", &self.on_behalf)
.finish()
}
}
#[async_trait::async_trait]
pub trait AuditRecorder: Send + Sync {
async fn record(&self, event: AuditEvent) -> Result<(), AuditError>;
}
#[derive(Debug, thiserror::Error)]
#[error("the audit recorder could not accept the ITI-55 audit message")]
pub struct AuditError(#[source] pub Box<dyn std::error::Error + Send + Sync>);
#[cfg(feature = "atna")]
pub const SOURCE_ROLE: (&str, &str, &str) = ("110153", "DCM", "Source Role ID");
#[cfg(feature = "atna")]
pub const DESTINATION_ROLE: (&str, &str, &str) = ("110152", "DCM", "Destination Role ID");
#[cfg(feature = "atna")]
impl AuditEvent {
#[must_use]
pub fn message(
&self,
source: &ihe_atna::atna::message::AuditSource,
host: &ihe_atna::atna::message::AccessPoint,
) -> ihe_atna::atna::message::AuditMessage {
use ihe_atna::atna::message::{
AccessPoint, ActiveParticipant, AuditMessage, CodedValue, EventIdentification,
ParticipantObject,
};
let transaction = CodedValue::of(EVENT_TYPE);
let mut participants = vec![ActiveParticipant {
user_id: super::request::ANONYMOUS.to_owned(),
alternative_user_id: Some(self.process_id.to_string()),
user_name: None,
user_is_requestor: self.on_behalf.user().is_none(),
role_id_codes: vec![CodedValue::of(SOURCE_ROLE)],
access_point: Some(host.clone()),
}];
if let Some(user) = self.on_behalf.user() {
participants.push(ActiveParticipant {
user_id: user.subject().to_owned(),
alternative_user_id: None,
user_name: Some(user.atna_user_name()),
user_is_requestor: true,
role_id_codes: Vec::new(),
access_point: None,
});
}
participants.push(ActiveParticipant {
user_id: self.destination.to_string(),
alternative_user_id: None,
user_name: None,
user_is_requestor: false,
role_id_codes: vec![CodedValue::of(DESTINATION_ROLE)],
access_point: self
.destination_access_point
.as_ref()
.map(|point| AccessPoint {
type_code: point.type_code(),
id: point.id(),
}),
});
AuditMessage {
event: EventIdentification {
event_id: CodedValue::of(EVENT_ID),
action: EVENT_ACTION,
date_time: self.date_time,
outcome: self.outcome.code(),
type_codes: vec![transaction.clone()],
outcome_description: None,
},
participants,
source: source.clone(),
objects: vec![ParticipantObject {
id: SecretString::from(String::new()),
type_code: Some("2"),
type_code_role: Some("24"),
data_life_cycle: None,
id_type_code: transaction,
query: Some(self.query.clone()),
details: self
.home_community
.iter()
.map(|community| {
(
HOME_COMMUNITY_DETAIL.to_owned(),
SecretString::from(community.to_string()),
)
})
.collect(),
}],
}
}
}
pub(super) fn destination(endpoint: &Url) -> Url {
let mut shown = endpoint.clone();
let _cleared: Result<(), ()> = shown.set_username("");
let _cleared: Result<(), ()> = shown.set_password(None);
shown
}