use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use fhir_types::r4::audit_event::{
AuditEvent, AuditEventAgent, AuditEventAgentNetwork, AuditEventEntity, AuditEventEntityDetail,
AuditEventEntityDetailValue, AuditEventSource,
};
use fhir_types::r4::codeable_concept::CodeableConcept;
use fhir_types::r4::coding::Coding;
use fhir_types::r4::extension::{Extension, ExtensionValue};
use fhir_types::r4::identifier::Identifier;
use fhir_types::r4::meta::Meta;
use fhir_types::r4::primitives;
use fhir_types::r4::reference::Reference;
use secrecy::{ExposeSecret, SecretSlice};
use super::{
APPLICATION, AuditEventValue, AuditRecord, Coded, Described, Direction, Entity, Exchange,
INFORMATION_RECIPIENT, NetworkAddress, OAUTH_COMPREHENSIVE, Observer, PATIENT_NAMESPACE,
PATIENT_ROLE, PERSON, Peer, QUERY_ROLE, RecordError, SYSTEM_OBJECT, SecurityLabel, What,
};
use crate::user::{Code, PurposeOfUse, User};
const ASSURANCE_LEVEL_EXTENSION: &str =
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/ihe-assuranceLevel";
const OTHER_ID_EXTENSION: &str =
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/ihe-otherId";
const IDENTIFIER_TYPES: &str = "http://terminology.hl7.org/CodeSystem/v2-0203";
const NATIONAL_PROVIDER_IDENTIFIER: &str = "NPI";
const PERSON_IDENTIFIER: &str = "PRN";
impl Exchange {
pub fn audit_event(&self, observer: &Observer) -> Result<AuditRecord, RecordError> {
serde_json::to_vec(&self.escaped(observer)?)
.map(|json| AuditRecord(SecretSlice::from(json)))
.map_err(RecordError)
}
pub fn audit_event_value(&self, observer: &Observer) -> Result<AuditEventValue, RecordError> {
self.escaped(observer).map(AuditEventValue)
}
#[expect(
clippy::disallowed_types,
reason = "the escape walks the written resource as a JSON value"
)]
fn escaped(&self, observer: &Observer) -> Result<serde_json::Value, RecordError> {
let mut value = serde_json::to_value(self.resource(observer)).map_err(RecordError)?;
escape_strings(&mut value);
Ok(value)
}
fn resource(&self, observer: &Observer) -> AuditEvent {
let own = |kind: &Coded, network: NetworkAddress| AuditEventAgent {
r#type: Some(concept(kind)),
who: Some(display(&observer.source_id)),
requestor: primitives::Boolean::from(false),
network: Some(network_of(&network)),
..AuditEventAgent::default()
};
let other = |kind: &Coded, peer: &Peer| AuditEventAgent {
r#type: Some(concept(kind)),
who: Some(display(&peer.who)),
requestor: primitives::Boolean::from(false),
network: Some(network_of(&peer.network)),
..AuditEventAgent::default()
};
let mut agent = match &self.direction {
Direction::Sent { server } => vec![
own(&self.kind.client, observer.host.clone()),
other(&self.kind.server, server),
],
Direction::Received { client, endpoint } => vec![
other(&self.kind.client, client),
own(&self.kind.server, endpoint.clone()),
],
};
let mut profiles: Vec<&str> = Vec::new();
if !self.kind.profile.is_empty() {
profiles.push(self.kind.profile);
}
if let Some(user) = self.on_behalf.user() {
let (person, application) = user_agents(user);
agent.push(person);
let named_client = match application {
Some(application) if self.kind.client != APPLICATION => {
agent.push(application);
true
}
_ => false,
};
if user.token_id().is_some() && user.policies().is_empty() && named_client {
profiles.push(OAUTH_COMPREHENSIVE);
}
agent.extend(organisation_agent(user));
}
let purpose_of_event = purposes_of_event(&agent);
AuditEvent {
meta: (!profiles.is_empty() || !self.security.is_empty()).then(|| Meta {
profile: profiles
.into_iter()
.map(primitives::Canonical::from)
.collect(),
security: self.security.iter().map(label).collect(),
..Meta::default()
}),
r#type: self.kind.event_type.coding(),
subtype: self.kind.subtypes.iter().map(Coded::coding).collect(),
action: Some(primitives::Code::from(self.kind.action)),
recorded: primitives::Instant::from(self.recorded.to_string()),
outcome: Some(primitives::Code::from(self.outcome.code())),
outcome_desc: self
.outcome_description
.as_deref()
.map(primitives::String::from),
purpose_of_event,
agent,
source: AuditEventSource {
site: observer.site.as_deref().map(primitives::String::from),
observer: Reference {
identifier: Some(Box::new(Identifier {
value: Some(primitives::String::from(observer.source_id.as_str())),
..Identifier::default()
})),
..display(&observer.source_id)
},
r#type: observer.source_types.iter().map(Coded::coding).collect(),
..AuditEventSource::default()
},
entity: self.entities.iter().map(entity).collect(),
..AuditEvent::default()
}
}
}
#[expect(
clippy::disallowed_types,
reason = "the escape walks the written resource as a JSON value"
)]
fn escape_strings(value: &mut serde_json::Value) {
match value {
serde_json::Value::String(text) => {
if let std::borrow::Cow::Owned(escaped) = crate::escape::untrusted(text) {
*text = escaped;
}
}
serde_json::Value::Array(items) => items.iter_mut().for_each(escape_strings),
serde_json::Value::Object(members) => members.values_mut().for_each(escape_strings),
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {}
}
}
fn purposes_of_event(agents: &[AuditEventAgent]) -> Vec<CodeableConcept> {
let mut purposes: Vec<CodeableConcept> = Vec::new();
for purpose in agents.iter().flat_map(|agent| &agent.purpose_of_use) {
if !purposes.contains(purpose) {
purposes.push(purpose.clone());
}
}
purposes
}
fn user_agents(user: &User) -> (AuditEventAgent, Option<AuditEventAgent>) {
let identified = |system: Option<&str>, value: &str| Reference {
identifier: Some(Box::new(Identifier {
system: system.map(primitives::Uri::from),
value: Some(primitives::String::from(value)),
..Identifier::default()
})),
..Reference::default()
};
let person = AuditEventAgent {
extension: user_extensions(user),
r#type: Some(concept(&INFORMATION_RECIPIENT)),
role: user.roles().iter().map(coded).collect(),
who: Some(Reference {
display: user.name().map(primitives::String::from),
..identified(user.issuer(), user.subject())
}),
alt_id: user.alt_id().map(primitives::String::from),
requestor: primitives::Boolean::from(true),
policy: user
.token_id()
.into_iter()
.chain(user.policies().iter().map(String::as_str))
.map(primitives::Uri::from)
.collect(),
purpose_of_use: user.purposes().iter().map(purpose).collect(),
..AuditEventAgent::default()
};
let application = user.client_id().map(|client_id| AuditEventAgent {
r#type: Some(concept(&APPLICATION)),
who: Some(identified(None, client_id)),
requestor: primitives::Boolean::from(false),
..AuditEventAgent::default()
});
(person, application)
}
fn organisation_agent(user: &User) -> Option<AuditEventAgent> {
let organisation = user.organisation()?;
Some(AuditEventAgent {
who: Some(Reference {
r#type: Some(primitives::Uri::from("Organization")),
identifier: Some(Box::new(Identifier {
value: Some(primitives::String::from(organisation)),
..Identifier::default()
})),
..Reference::default()
}),
requestor: primitives::Boolean::from(false),
..AuditEventAgent::default()
})
}
fn user_extensions(user: &User) -> Vec<Extension> {
let assurance = user.assurance().map(|level| Extension {
url: ASSURANCE_LEVEL_EXTENSION.to_owned(),
value: Some(ExtensionValue::CodeableConcept(Box::new(coded(level)))),
..Extension::default()
});
let provider = user.provider_identifier().map(|identifier| Extension {
url: OTHER_ID_EXTENSION.to_owned(),
value: Some(ExtensionValue::Identifier(Box::new(Identifier {
r#type: Some(identifier_type(NATIONAL_PROVIDER_IDENTIFIER)),
value: Some(primitives::String::from(identifier)),
..Identifier::default()
}))),
..Extension::default()
});
let person = user.person_id().map(|person| Extension {
url: OTHER_ID_EXTENSION.to_owned(),
value: Some(ExtensionValue::Identifier(Box::new(Identifier {
r#type: Some(identifier_type(PERSON_IDENTIFIER)),
system: person.system.as_deref().map(primitives::Uri::from),
value: Some(primitives::String::from(person.value.as_str())),
..Identifier::default()
}))),
..Extension::default()
});
assurance
.into_iter()
.chain(provider)
.chain(person)
.collect()
}
fn identifier_type(code: &str) -> CodeableConcept {
CodeableConcept {
coding: vec![Coding {
system: Some(primitives::Uri::from(IDENTIFIER_TYPES)),
code: Some(primitives::Code::from(code)),
..Coding::default()
}],
..CodeableConcept::default()
}
}
fn coded(code: &Code) -> CodeableConcept {
CodeableConcept {
coding: vec![Coding {
system: code.system.as_deref().map(primitives::Uri::from),
code: Some(primitives::Code::from(code.code.as_str())),
..Coding::default()
}],
..CodeableConcept::default()
}
}
fn purpose(purpose: &PurposeOfUse) -> CodeableConcept {
CodeableConcept {
coding: vec![Coding {
system: purpose.system.as_deref().map(primitives::Uri::from),
code: Some(primitives::Code::from(purpose.code.as_str())),
..Coding::default()
}],
..CodeableConcept::default()
}
}
fn concept(code: &Coded) -> CodeableConcept {
CodeableConcept {
coding: vec![code.coding()],
..CodeableConcept::default()
}
}
fn display(text: &str) -> Reference {
Reference {
display: Some(primitives::String::from(text)),
..Reference::default()
}
}
fn network_of(address: &NetworkAddress) -> AuditEventAgentNetwork {
AuditEventAgentNetwork {
address: Some(primitives::String::from(address.address())),
r#type: Some(primitives::Code::from(address.type_code())),
..AuditEventAgentNetwork::default()
}
}
fn entity(entity: &Entity) -> AuditEventEntity {
match entity {
Entity::Query(request) => AuditEventEntity {
r#type: Some(SYSTEM_OBJECT.coding()),
role: Some(QUERY_ROLE.coding()),
query: Some(primitives::Base64Binary::from(
STANDARD.encode(request.expose_secret()),
)),
..AuditEventEntity::default()
},
Entity::Patient {
system,
value,
namespace,
} => AuditEventEntity {
what: Some(Reference {
identifier: Some(Box::new(Identifier {
system: system.as_deref().map(primitives::Uri::from),
value: Some(primitives::String::from(value.expose_secret())),
..Identifier::default()
})),
..Reference::default()
}),
r#type: Some(PERSON.coding()),
role: Some(PATIENT_ROLE.coding()),
detail: namespace
.iter()
.map(|namespace| string_detail(PATIENT_NAMESPACE, namespace))
.collect(),
..AuditEventEntity::default()
},
Entity::PatientReference(reference) => AuditEventEntity {
what: Some(Reference {
reference: Some(primitives::String::from(reference.expose_secret())),
..Reference::default()
}),
r#type: Some(PERSON.coding()),
role: Some(PATIENT_ROLE.coding()),
..AuditEventEntity::default()
},
Entity::Resource {
reference,
resource_type,
kind,
role,
name,
query,
} => AuditEventEntity {
what: Some(Reference {
reference: reference.as_deref().map(primitives::String::from),
r#type: Some(primitives::Uri::from(*resource_type)),
..Reference::default()
}),
r#type: Some(kind.coding()),
role: role.as_ref().map(Coded::coding),
name: name.map(primitives::String::from),
query: query
.as_deref()
.map(|text| primitives::Base64Binary::from(STANDARD.encode(text))),
..AuditEventEntity::default()
},
Entity::Described(described) => described_entity(described),
}
}
fn described_entity(described: &Described) -> AuditEventEntity {
let what = described.what.as_ref().map(|what| match what {
What::Reference {
reference,
resource_type,
} => Reference {
reference: Some(primitives::String::from(reference.as_str())),
r#type: resource_type.as_deref().map(primitives::Uri::from),
..Reference::default()
},
What::Identifier { system, value } => Reference {
identifier: Some(Box::new(Identifier {
system: system.as_deref().map(primitives::Uri::from),
value: Some(primitives::String::from(value.as_str())),
..Identifier::default()
})),
..Reference::default()
},
});
AuditEventEntity {
what,
r#type: Some(described.kind.coding()),
role: described.role.as_ref().map(Coded::coding),
name: described.name.as_deref().map(primitives::String::from),
description: described
.description
.as_deref()
.map(primitives::String::from),
detail: described
.details
.iter()
.map(|detail| string_detail(&detail.kind, &detail.value))
.collect(),
security_label: described.security_labels.iter().map(label).collect(),
..AuditEventEntity::default()
}
}
fn string_detail(kind: &str, value: &str) -> AuditEventEntityDetail {
AuditEventEntityDetail {
id: None,
extension: Vec::new(),
modifier_extension: Vec::new(),
r#type: primitives::String::from(kind),
value: AuditEventEntityDetailValue::String(primitives::String::from(value)),
}
}
fn label(label: &SecurityLabel) -> Coding {
Coding {
system: Some(primitives::Uri::from(label.system.as_str())),
version: label.version.as_deref().map(primitives::String::from),
code: Some(primitives::Code::from(label.code.as_str())),
..Coding::default()
}
}
#[cfg(test)]
mod tests {
use fhir_types::r4::audit_event::AuditEventAgent;
use super::{purpose, purposes_of_event};
use crate::user::PurposeOfUse;
fn coded(code: &str) -> PurposeOfUse {
PurposeOfUse {
system: Some("http://terminology.hl7.org/CodeSystem/v3-ActReason".to_owned()),
code: code.to_owned(),
}
}
fn carrying(codes: &[&str]) -> AuditEventAgent {
AuditEventAgent {
purpose_of_use: codes.iter().map(|code| purpose(&coded(code))).collect(),
..AuditEventAgent::default()
}
}
#[test]
fn a_purpose_two_agents_carry_appears_once_in_first_seen_order() {
let agents = [
carrying(&["BTG", "TREAT"]),
carrying(&[]),
carrying(&["TREAT", "ETREAT"]),
];
let expected: Vec<_> = ["BTG", "TREAT", "ETREAT"]
.iter()
.map(|code| purpose(&coded(code)))
.collect();
assert_eq!(purposes_of_event(&agents), expected);
}
}