use ihe_atna::atna::codes;
use ihe_atna::atna::message::{
AccessPoint, ActiveParticipant, AuditMessage, AuditSource, CodedValue, EventIdentification,
ParticipantObject,
};
use ihe_atna::balp::{Coded, Described, Direction, Entity, Exchange, NetworkAddress, What};
use secrecy::{ExposeSecret as _, SecretString};
use crate::balp::{ObjectIds, exchange_with};
use crate::record::{AccessRecord, Coded as RecordCoded, InvalidRecord, NameId};
pub const LOCAL_CODES: &str = "local";
pub const SERVICE_CONSUMER: (&str, &str, &str) = (
"ServiceConsumer",
"eHealth DSI",
"eHealth DSI Service Consumer",
);
pub const SERVICE_PROVIDER: (&str, &str, &str) = (
"ServiceProvider",
"eHealth DSI",
"eHealth DSI Service Provider",
);
const ENTITY_TYPES: &str = "http://terminology.hl7.org/CodeSystem/audit-entity-type";
const OBJECT_ROLES: &str = "http://terminology.hl7.org/CodeSystem/object-role";
const TYPE_CODES: [&str; 4] = ["1", "2", "3", "4"];
const ROLE_CODES: [&str; 24] = [
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17",
"18", "19", "20", "21", "22", "23", "24",
];
const DESCRIPTION: &str = "description";
const IDENTIFIER_SYSTEM: &str = "identifier-system";
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AtnaError {
#[error("the access record is not one a sink stores")]
Invalid(#[from] InvalidRecord),
#[error("the access record holds an entity the audit message has no form for")]
Unsupported,
}
pub fn audit_message(
record: &AccessRecord,
endpoint: &NetworkAddress,
source: &AuditSource,
) -> Result<AuditMessage, AtnaError> {
audit_message_with(record, endpoint, source, &ObjectIds::default())
}
pub fn audit_message_with(
record: &AccessRecord,
endpoint: &NetworkAddress,
source: &AuditSource,
ids: &ObjectIds,
) -> Result<AuditMessage, AtnaError> {
record.validate()?;
let exchange = exchange_with(record, endpoint, ids);
let kind = &exchange.kind;
let event = EventIdentification {
event_id: coded(&kind.event_type),
action: kind.action,
date_time: exchange.recorded,
outcome: exchange.outcome.code(),
type_codes: kind.subtypes.iter().map(coded).collect(),
outcome_description: exchange.outcome_description.clone(),
purposes_of_use: record
.accessor
.purposes
.iter()
.map(|purpose| local(purpose.system.as_deref(), &purpose.code))
.collect(),
};
let objects = exchange
.entities
.iter()
.map(|entity| object(entity, record))
.collect::<Result<Vec<_>, _>>()?;
Ok(AuditMessage {
event,
participants: participants(record, &exchange),
source: source.clone(),
objects,
})
}
fn participants(record: &AccessRecord, exchange: &Exchange) -> Vec<ActiveParticipant> {
let accessor = &record.accessor;
let mut participants = Vec::new();
if let Some(relayed) = &accessor.relayed {
let parts = relayed.as_parts();
participants.push(match &parts.professional {
Some(professional) => ActiveParticipant {
user_id: professional
.name_id
.as_ref()
.map_or_else(|| professional.identifier.clone(), NameId::user_id),
alternative_user_id: Some(professional.name.clone()),
user_name: None,
user_is_requestor: true,
role_id_codes: professional.roles.iter().map(record_coded).collect(),
access_point: None,
},
None => ActiveParticipant {
user_id: parts.provider.identifier.clone(),
alternative_user_id: None,
user_name: parts.provider.name.clone(),
user_is_requestor: true,
role_id_codes: Vec::new(),
access_point: None,
},
});
for (gateway, requestor, role) in [
(&parts.gateways.consumer, true, SERVICE_CONSUMER),
(&parts.gateways.provider, false, SERVICE_PROVIDER),
] {
participants.extend(gateway.iter().map(|cn| ActiveParticipant {
user_id: cn.clone(),
alternative_user_id: None,
user_name: None,
user_is_requestor: requestor,
role_id_codes: vec![CodedValue::of(role)],
access_point: None,
}));
}
}
if let Some(user) = exchange.on_behalf.user() {
let mut roles: Vec<CodedValue> = accessor
.acting
.map(|acting| local(None, acting.code()))
.into_iter()
.collect();
roles.extend(accessor.roles.iter().map(record_coded));
participants.push(ActiveParticipant {
user_id: accessor.subject.clone(),
alternative_user_id: accessor.alt_id.clone(),
user_name: Some(user.atna_user_name()),
user_is_requestor: true,
role_id_codes: roles,
access_point: None,
});
}
if let Direction::Received { client, endpoint } = &exchange.direction {
participants.push(ActiveParticipant {
user_id: client.who.clone(),
alternative_user_id: None,
user_name: None,
user_is_requestor: false,
role_id_codes: vec![coded(&exchange.kind.client)],
access_point: Some(access_point(&client.network)),
});
participants.push(ActiveParticipant {
user_id: endpoint.address(),
alternative_user_id: None,
user_name: None,
user_is_requestor: false,
role_id_codes: vec![coded(&exchange.kind.server)],
access_point: Some(access_point(endpoint)),
});
}
participants.extend(accessor.provider.iter().map(|provider| ActiveParticipant {
user_id: provider.clone(),
alternative_user_id: None,
user_name: None,
user_is_requestor: false,
role_id_codes: Vec::new(),
access_point: None,
}));
participants
}
fn object(entity: &Entity, record: &AccessRecord) -> Result<ParticipantObject, AtnaError> {
let plain = |id: SecretString, id_type_code: CodedValue| ParticipantObject {
id,
type_code: None,
type_code_role: None,
data_life_cycle: None,
id_type_code,
name: None,
query: None,
details: Vec::new(),
};
Ok(match entity {
Entity::Query(query) => ParticipantObject {
type_code: Some("2"),
type_code_role: Some("24"),
query: Some(query.clone()),
..plain(
SecretString::from(record.request.operation.clone()),
CodedValue::of(codes::OBJECT_ID_SEARCH_CRITERIA),
)
},
Entity::Patient {
system,
value,
namespace,
} => ParticipantObject {
type_code: Some("1"),
type_code_role: Some("1"),
..plain(
SecretString::from(cx(
value.expose_secret(),
system.as_deref(),
namespace.as_deref(),
)),
CodedValue::of(codes::OBJECT_ID_PATIENT_NUMBER),
)
},
Entity::PatientReference(reference) => ParticipantObject {
type_code: Some("1"),
type_code_role: Some("1"),
..plain(reference.clone(), CodedValue::of(codes::OBJECT_ID_URI))
},
Entity::Described(described) => described_object(described)?,
_ => return Err(AtnaError::Unsupported),
})
}
fn described_object(described: &Described) -> Result<ParticipantObject, AtnaError> {
let mut details = Vec::new();
let id = match &described.what {
Some(What::Identifier { system, value }) => {
details.extend(system.iter().map(|system| {
(
IDENTIFIER_SYSTEM.to_owned(),
SecretString::from(system.clone()),
)
}));
value.clone()
}
Some(What::Reference { reference, .. }) => reference.clone(),
None => described
.name
.clone()
.unwrap_or_else(|| described.kind.code.to_string()),
Some(_) => return Err(AtnaError::Unsupported),
};
details.extend(
described
.description
.iter()
.map(|text| (DESCRIPTION.to_owned(), SecretString::from(text.clone()))),
);
details.extend(described.details.iter().map(|detail| {
(
detail.kind.clone(),
SecretString::from(detail.value.clone()),
)
}));
Ok(ParticipantObject {
id: SecretString::from(id),
type_code: dicom_code(&described.kind, ENTITY_TYPES, &TYPE_CODES),
type_code_role: described
.role
.as_ref()
.and_then(|role| dicom_code(role, OBJECT_ROLES, &ROLE_CODES)),
data_life_cycle: None,
id_type_code: coded(&described.kind),
name: described.name.clone().map(SecretString::from),
query: None,
details,
})
}
fn dicom_code(code: &Coded, system: &str, codes: &[&'static str]) -> Option<&'static str> {
(code.system == system)
.then(|| {
codes
.iter()
.copied()
.find(|candidate| *candidate == code.code)
})
.flatten()
}
fn cx(value: &str, system: Option<&str>, namespace: Option<&str>) -> String {
let id = hl7_escaped(value);
let authority = match (system, namespace) {
(Some(system), _) => match system.strip_prefix("urn:oid:") {
Some(oid) => format!("&{}&ISO", hl7_escaped(oid)),
None => format!("&{}&URI", hl7_escaped(system)),
},
(None, Some(namespace)) => hl7_escaped(namespace),
(None, None) => return id,
};
format!("{id}^^^{authority}")
}
fn hl7_escaped(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
match character {
'|' => escaped.push_str(r"\F\"),
'^' => escaped.push_str(r"\S\"),
'&' => escaped.push_str(r"\T\"),
'~' => escaped.push_str(r"\R\"),
'\\' => escaped.push_str(r"\E\"),
other => escaped.push(other),
}
}
escaped
}
fn access_point(address: &NetworkAddress) -> AccessPoint {
AccessPoint {
type_code: address.type_code(),
id: address.address(),
}
}
fn coded(code: &Coded) -> CodedValue {
CodedValue {
code: code.code.to_string(),
system_name: code.system.to_string(),
original_text: code.display.to_string(),
}
}
fn record_coded(code: &RecordCoded) -> CodedValue {
local(code.system.as_deref(), &code.code)
}
fn local(system: Option<&str>, code: &str) -> CodedValue {
CodedValue {
code: code.to_owned(),
system_name: system.unwrap_or(LOCAL_CODES).to_owned(),
original_text: code.to_owned(),
}
}