1pub mod detail;
191pub mod inserted;
192pub mod transformation;
193
194use std::collections::BTreeSet;
195use std::sync::Arc;
196
197use ihe_atna::balp::{
198 APPLICATION, AuditRecorder, CREATE, CUSTODIAN, Coded, DELETE, DESTINATION_ROLE,
199 DOMAIN_RESOURCE, Described, Detail, Direction, Entity, EventKind, Exchange, NetworkAddress,
200 OTHER, Peer, READ, REQUEST_ID, REST, Reserved, SEARCH, SOURCE_ROLE, SYSTEM_OBJECT,
201 SecurityLabel, UPDATE, What, is_literal_reference,
202};
203use ihe_atna::user::{Code, OnBehalfOf, PurposeOfUse, User};
204use secrecy::{ExposeSecret as _, SecretString};
205use url::Url;
206
207use crate::classify::{Basis, Classification};
208use crate::emergency::Emergency;
209use crate::label::Label;
210use crate::record::{
211 AccessRecord, Action, Origin, Outcome, PatientIdentifier, PatientLookup, Population, Relayed,
212};
213use crate::retention::Retention;
214use crate::sink::{AccessSink, Reservation, SinkError};
215
216pub const EMERGENCY_ENTITY: &str = "ehds-emergency-access";
218
219pub const EMERGENCY_DESCRIPTION: &str = "Emergency access: the accessor declared that the access \
221 was necessary to protect the vital interests of the data subject \
222 (Regulation (EU) 2025/327 Art 11(5)).";
223
224pub const POPULATION_ENTITY: &str = "ehds-population";
226
227pub const POPULATION_DESCRIPTION: &str = "Population access: a query over many \
229 patients' data ran over each origin this record names; the record does not name \
230 the persons whose records it read, and does not say that it read any one \
231 person's record (Regulation (EU) 2025/327 Art 9(1)).";
232
233pub struct BalpSink {
237 recorder: Arc<dyn AuditRecorder>,
238 endpoint: NetworkAddress,
239 ids: ObjectIds,
240}
241
242#[derive(Debug, Clone, Default, PartialEq, Eq)]
252#[non_exhaustive]
253pub enum ObjectIds {
254 #[default]
259 References,
260 Identifiers {
263 system: Option<Url>,
266 },
267}
268
269impl std::fmt::Debug for BalpSink {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 f.debug_struct("BalpSink").finish_non_exhaustive()
272 }
273}
274
275impl BalpSink {
276 #[must_use]
281 pub fn new(recorder: Arc<dyn AuditRecorder>, endpoint: NetworkAddress) -> Self {
282 Self {
283 recorder,
284 endpoint,
285 ids: ObjectIds::default(),
286 }
287 }
288
289 #[must_use]
293 pub fn with_object_ids(self, ids: ObjectIds) -> Self {
294 Self { ids, ..self }
295 }
296}
297
298#[async_trait::async_trait]
299impl AccessSink for BalpSink {
300 async fn store(&self, record: AccessRecord) -> Result<(), SinkError> {
301 self.recorder
302 .record(exchange_with(&record, &self.endpoint, &self.ids))
303 .await
304 .map_err(|error| SinkError(Box::new(error)))
305 }
306
307 async fn reserve(&self) -> Result<Box<dyn Reservation>, SinkError> {
308 let place = self
309 .recorder
310 .reserve()
311 .await
312 .map_err(|error| SinkError(Box::new(error)))?;
313 Ok(Box::new(BalpReservation {
314 place,
315 recorder: Arc::clone(&self.recorder),
316 endpoint: self.endpoint.clone(),
317 ids: self.ids.clone(),
318 }))
319 }
320}
321
322struct BalpReservation {
325 place: Option<Box<dyn Reserved>>,
326 recorder: Arc<dyn AuditRecorder>,
327 endpoint: NetworkAddress,
328 ids: ObjectIds,
329}
330
331impl std::fmt::Debug for BalpReservation {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 f.debug_struct("BalpReservation")
334 .field("place", &self.place)
335 .finish_non_exhaustive()
336 }
337}
338
339#[async_trait::async_trait]
340impl Reservation for BalpReservation {
341 async fn store(self: Box<Self>, record: AccessRecord) -> Result<(), SinkError> {
342 let exchange = exchange_with(&record, &self.endpoint, &self.ids);
343 match self.place {
344 Some(place) => place.record(exchange).await,
345 None => self.recorder.record(exchange).await,
346 }
347 .map_err(|error| SinkError(Box::new(error)))
348 }
349}
350
351#[must_use]
355pub fn exchange(record: &AccessRecord, endpoint: &NetworkAddress) -> Exchange {
356 exchange_with(record, endpoint, &ObjectIds::default())
357}
358
359#[must_use]
362pub fn exchange_with(
363 record: &AccessRecord,
364 endpoint: &NetworkAddress,
365 ids: &ObjectIds,
366) -> Exchange {
367 let accessor = &record.accessor;
368 let roles = accessor
371 .acting
372 .map(|acting| code(acting.code()))
373 .into_iter()
374 .chain(accessor.roles.iter().map(|role| Code {
375 system: role.system.clone(),
376 code: role.code.clone(),
377 }))
378 .collect();
379 let user = User::local(accessor.subject.clone())
380 .with_issuer(accessor.issuer.clone())
381 .with_client_id(accessor.client_id.clone())
382 .with_token_id(accessor.token_id.clone())
383 .with_policies(accessor.policies.clone())
384 .with_audience(accessor.audience.clone())
385 .with_purposes(
386 accessor
387 .purposes
388 .iter()
389 .map(|purpose| PurposeOfUse {
390 system: purpose.system.clone(),
391 code: purpose.code.clone(),
392 })
393 .collect(),
394 )
395 .with_organisation(accessor.provider.clone())
396 .with_alt_id(accessor.alt_id.clone())
397 .with_name(accessor.professional.name.clone())
398 .with_provider_identifier(accessor.professional.identifier.clone())
399 .with_assurance(accessor.assurance.map(|level| code(level.code())))
402 .with_roles(roles);
403 let client = Peer {
404 who: accessor
405 .client_id
406 .clone()
407 .unwrap_or_else(|| accessor.subject.clone()),
408 network: record.request.client_address.map_or_else(
409 || NetworkAddress::MachineName(String::from("unknown")),
410 NetworkAddress::Ip,
411 ),
412 };
413 Exchange {
414 kind: kind(record),
415 recorded: record.recorded,
416 outcome: match record.outcome {
417 Outcome::Success => ihe_atna::balp::Outcome::Success,
418 Outcome::MinorFailure => ihe_atna::balp::Outcome::MinorFailure,
419 Outcome::SeriousFailure => ihe_atna::balp::Outcome::SeriousFailure,
420 Outcome::MajorFailure => ihe_atna::balp::Outcome::MajorFailure,
421 },
422 outcome_description: record.outcome_description.clone(),
423 direction: Direction::Received {
424 client,
425 endpoint: endpoint.clone(),
426 },
427 on_behalf: OnBehalfOf::User(user),
428 entities: entities(record, ids),
429 security: Vec::new(),
432 }
433}
434
435fn code(code: &str) -> Code {
437 Code {
438 system: None,
439 code: code.to_owned(),
440 }
441}
442
443fn kind(record: &AccessRecord) -> EventKind {
445 let patient = patients(record).len() == 1;
446 let subtypes = |fixed: Coded| {
449 let request = &record.request;
450 let mut subtypes = vec![fixed];
451 subtypes.extend(request.operation_system.as_ref().map(|system| {
452 Coded::new(
453 system.clone(),
454 request.operation.clone(),
455 request.operation.clone(),
456 )
457 }));
458 subtypes
459 };
460 let profile = |plain: &'static str, with_patient: &'static str| {
461 if patient { with_patient } else { plain }
462 };
463 match record.action {
464 Action::Query => EventKind {
465 profile: profile(
466 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Query",
467 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientQuery",
468 ),
469 event_type: REST,
470 subtypes: subtypes(SEARCH),
471 action: "E",
472 client: SOURCE_ROLE,
473 server: DESTINATION_ROLE,
474 },
475 Action::Read => EventKind {
476 profile: profile(
477 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Read",
478 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientRead",
479 ),
480 event_type: REST,
481 subtypes: subtypes(READ),
482 action: "R",
483 client: DESTINATION_ROLE,
484 server: SOURCE_ROLE,
485 },
486 Action::Create => EventKind {
487 profile: profile(
488 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Create",
489 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientCreate",
490 ),
491 event_type: REST,
492 subtypes: subtypes(CREATE),
493 action: "C",
494 client: SOURCE_ROLE,
495 server: DESTINATION_ROLE,
496 },
497 Action::Update => EventKind {
498 profile: profile(
499 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Update",
500 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientUpdate",
501 ),
502 event_type: REST,
503 subtypes: subtypes(UPDATE),
504 action: "U",
505 client: SOURCE_ROLE,
506 server: DESTINATION_ROLE,
507 },
508 Action::Delete => EventKind {
509 profile: if record.outcome == Outcome::Success {
510 profile(
511 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.Delete",
512 "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientDelete",
513 )
514 } else {
515 ""
516 },
517 event_type: REST,
518 subtypes: subtypes(DELETE),
519 action: "D",
520 client: APPLICATION,
521 server: CUSTODIAN,
522 },
523 }
524}
525
526fn entities(record: &AccessRecord, ids: &ObjectIds) -> Vec<Entity> {
529 let request = &record.request;
530 let correlation = request
531 .correlation
532 .iter()
533 .map(|correlation| Detail::new(detail::CORRELATION, correlation.clone()));
534 let mut entities: Vec<Entity> = request
537 .id
538 .iter()
539 .map(|id| {
540 Entity::Described(Described {
541 what: Some(What::Identifier {
542 system: None,
543 value: id.clone(),
544 }),
545 kind: REQUEST_ID,
546 role: None,
547 name: None,
548 description: None,
549 details: correlation.clone().collect(),
550 security_labels: Vec::new(),
551 })
552 })
553 .collect();
554 match record.action {
555 Action::Query => entities.push(Entity::Query(
556 request
557 .query
558 .clone()
559 .unwrap_or_else(|| SecretString::from(request.operation.clone())),
560 )),
561 Action::Read | Action::Create | Action::Update | Action::Delete => {
562 entities.push(Entity::Described(Described {
563 what: Some(data(request.resource.as_deref(), &request.operation, ids)),
564 kind: SYSTEM_OBJECT,
565 role: Some(DOMAIN_RESOURCE),
566 name: Some(request.operation.clone()),
567 description: None,
568 details: Vec::new(),
569 security_labels: Vec::new(),
570 }));
571 }
572 }
573 entities.extend(subject_entities(record));
574 let mut details = categories(&record.categories);
575 if let Some(delivered) = record.delivered {
576 details.push(Detail::new(detail::DELIVERED, delivered.to_string()));
577 }
578 if let Some(name) = &request.stored_query {
579 details.push(Detail::new(detail::STORED_QUERY, name.clone()));
580 }
581 if request.id.is_none() {
582 details.extend(correlation);
583 }
584 if let Some(count) = record.origin_count {
585 details.push(Detail::new(detail::ORIGIN_COUNT, count.to_string()));
586 }
587 details.extend(retention_details(&record.retention));
588 entities.push(other(
589 None,
590 "ehds-categories",
591 details,
592 record.labels.categories(),
593 ));
594 for origin in &record.origins {
595 let labels = origin
596 .key()
597 .map_or(&[][..], |key| record.labels.origin(key));
598 entities.push(origin_entity(origin, labels));
599 }
600 if let Some(relayed) = &record.accessor.relayed {
601 entities.push(other(None, "ehds-relayed", relayed_details(relayed), &[]));
602 }
603 entities.extend(record.subject.population.as_ref().map(population_entity));
604 if let Some(emergency) = &record.emergency {
605 entities.push(emergency_entity(emergency));
606 }
607 entities.extend(record.inserted.map(inserted::entity));
608 entities.extend(record.transformations.iter().map(transformation::entity));
609 entities
610}
611
612fn data(resource: Option<&str>, operation: &str, ids: &ObjectIds) -> What {
616 let Some(resource) = resource else {
619 return What::Identifier {
620 system: None,
621 value: operation.to_owned(),
622 };
623 };
624 match ids {
625 ObjectIds::References if is_literal_reference(resource) => What::Reference {
626 reference: resource.to_owned(),
627 resource_type: None,
628 },
629 ObjectIds::References => What::Identifier {
630 system: None,
631 value: resource.to_owned(),
632 },
633 ObjectIds::Identifiers { system } => What::Identifier {
634 system: system.as_ref().map(|system| system.as_str().to_owned()),
635 value: resource.to_owned(),
636 },
637 }
638}
639
640fn retention_details(retention: &Retention) -> Vec<Detail> {
644 let period = retention.period();
645 let mut details: Vec<Detail> = period
646 .years()
647 .map(|years| Detail::new(detail::RETENTION_YEARS, years.get().to_string()))
648 .into_iter()
649 .collect();
650 details.push(Detail::new(detail::RETENTION_PERIOD, period.code()));
651 details.extend(
652 retention
653 .ends()
654 .map(|ends| Detail::new(detail::RETENTION_ENDS, ends.to_string())),
655 );
656 details.extend([
657 Detail::new(detail::RETENTION_GROUND, retention.ground().code()),
658 Detail::new(detail::RETENTION_FLOOR, retention.floor().code()),
659 ]);
660 details
661}
662
663fn subject_entities(record: &AccessRecord) -> Vec<Entity> {
667 let mut entities: Vec<Entity> = patients(record)
668 .into_iter()
669 .map(|patient| {
670 let uri = patient
671 .namespace
672 .as_deref()
673 .filter(|namespace| is_uri(namespace));
674 Entity::Patient {
675 system: uri.map(str::to_owned),
676 value: patient.value.clone(),
677 namespace: patient.namespace.clone().filter(|_| uri.is_none()),
678 }
679 })
680 .collect();
681 entities.extend(record.subject.ehrs.iter().map(|ehr| {
682 other(
683 Some(ehr.ehr_id.clone()),
684 "ehr",
685 vec![
686 Detail::new(detail::ENDPOINT, ehr.endpoint.clone()),
687 Detail::new(detail::PATIENT_LOOKUP, ehr.patient.code()),
688 ],
689 &[],
690 )
691 }));
692 entities
693}
694
695fn origin_entity(origin: &Origin, labels: &[Label]) -> Entity {
699 let mut details: Vec<Detail> = origin
700 .status
701 .iter()
702 .map(|status| Detail::new(detail::STATUS, status.clone()))
703 .collect();
704 details.extend(
705 origin
706 .node
707 .iter()
708 .map(|node| Detail::new(detail::NODE, node.clone())),
709 );
710 details.extend(
711 origin
712 .system_id
713 .iter()
714 .map(|system_id| Detail::new(detail::SYSTEM_ID, system_id.clone())),
715 );
716 details.extend(
717 origin
718 .rows
719 .map(|rows| Detail::new(detail::ROWS, rows.to_string())),
720 );
721 if let Some(version) = &origin.version {
722 details.extend(
723 version
724 .named()
725 .map(|named| Detail::new(detail::WRITTEN_VERSION, named)),
726 );
727 details.extend(
728 version
729 .unread()
730 .map(|why| Detail::new(detail::WRITTEN_VERSION_UNREAD, why)),
731 );
732 }
733 if let Some(created) = &origin.created_ehr {
734 details.extend(
735 created
736 .named()
737 .map(|ehr_id| Detail::new(detail::CREATED_EHR, ehr_id)),
738 );
739 details.extend(
740 created
741 .unread()
742 .map(|why| Detail::new(detail::CREATED_EHR_UNREAD, why)),
743 );
744 }
745 details.extend(origin.categories.iter().flat_map(categories));
746 other(origin.key().map(str::to_owned), "origin", details, labels)
747}
748
749fn emergency_entity(emergency: &Emergency) -> Entity {
752 let mut details = vec![Detail::new(detail::EMERGENCY_ACCESS, "true")];
753 details.extend(emergency.purposes().iter().map(|purpose| {
754 Detail::new(
755 detail::EMERGENCY_PURPOSE,
756 coded(purpose.system.as_deref(), &purpose.code),
757 )
758 }));
759 details.extend(
760 emergency
761 .consent_set_aside()
762 .iter()
763 .map(|member| Detail::new(detail::CONSENT_SET_ASIDE, member.clone())),
764 );
765 Entity::Described(Described {
766 what: None,
767 kind: OTHER,
768 role: None,
769 name: Some(EMERGENCY_ENTITY.to_owned()),
770 description: Some(EMERGENCY_DESCRIPTION.to_owned()),
771 details,
772 security_labels: Vec::new(),
773 })
774}
775
776fn security(labels: &[Label]) -> Vec<SecurityLabel> {
778 labels
779 .iter()
780 .map(|label| SecurityLabel {
781 system: label.system().to_owned(),
782 version: label.version().map(str::to_owned),
783 code: label.code().to_owned(),
784 })
785 .collect()
786}
787
788fn population_entity(population: &Population) -> Entity {
791 let mut details = vec![
792 Detail::new(detail::POPULATION, "true"),
793 Detail::new(detail::POPULATION_REASON, population.reason.code()),
794 ];
795 details.extend(
796 population
797 .ehrs
798 .map(|ehrs| Detail::new(detail::POPULATION_EHRS, ehrs.to_string())),
799 );
800 details.push(Detail::new(
801 detail::POPULATION_LIMIT,
802 population.limit.to_string(),
803 ));
804 for (name, ids) in [
805 (detail::TEMPLATE_ID, &population.templates),
806 (detail::ARCHETYPE_ID, &population.archetypes),
807 ] {
808 details.extend(ids.iter().map(|id| Detail::new(name, id.clone())));
809 }
810 Entity::Described(Described {
811 what: None,
812 kind: OTHER,
813 role: None,
814 name: Some(POPULATION_ENTITY.to_owned()),
815 description: Some(POPULATION_DESCRIPTION.to_owned()),
816 details,
817 security_labels: Vec::new(),
818 })
819}
820
821fn coded(system: Option<&str>, code: &str) -> String {
823 match system {
824 Some(system) => format!("{system}|{code}"),
825 None => code.to_owned(),
826 }
827}
828
829fn relayed_details(relayed: &Relayed) -> Vec<Detail> {
832 let professional = &relayed.professional;
833 let provider = &relayed.provider;
834 let mut details = vec![
835 Detail::new(detail::CONTACT_POINT, relayed.contact_point.clone()),
836 Detail::new(detail::ASSERTED, "true"),
837 Detail::new(detail::COUNTRY_CODE, relayed.country_code.clone()),
838 Detail::new(detail::FAMILY_NAME, professional.family_name.clone()),
839 Detail::new(detail::GIVEN_NAME, professional.given_name.clone()),
840 Detail::new(detail::HP_IDENTIFIER, professional.identifier.clone()),
841 Detail::new(
842 detail::HP_ISSUING_AUTHORITY,
843 professional.issuing_authority.clone(),
844 ),
845 ];
846 details.extend(
847 professional
848 .roles
849 .iter()
850 .map(|role| Detail::new(detail::HP_ROLE, coded(role.system.as_deref(), &role.code))),
851 );
852 details.extend([
853 Detail::new(detail::PROVIDER_IDENTIFIER, provider.identifier.clone()),
854 Detail::new(
855 detail::PROVIDER_ISSUING_AUTHORITY,
856 provider.issuing_authority.clone(),
857 ),
858 Detail::new(detail::PROVIDER_NAME, provider.name.clone()),
859 Detail::new(detail::PROVIDER_ADDRESS, provider.address.clone()),
860 ]);
861 details
862}
863
864fn patients(record: &AccessRecord) -> Vec<&PatientIdentifier> {
868 let found = record
869 .subject
870 .ehrs
871 .iter()
872 .flat_map(|ehr| match &ehr.patient {
873 PatientLookup::Found(found) => found.as_slice(),
874 _ => &[],
875 });
876 let mut named: Vec<&PatientIdentifier> = Vec::new();
877 for patient in record.subject.patient.iter().chain(found) {
878 let seen = named.iter().any(|kept: &&PatientIdentifier| {
879 kept.namespace == patient.namespace
880 && kept.value.expose_secret() == patient.value.expose_secret()
881 });
882 if !seen {
883 named.push(patient);
884 }
885 }
886 named
887}
888
889fn is_uri(namespace: &str) -> bool {
892 Url::parse(namespace).is_ok()
893}
894
895fn other(value: Option<String>, name: &str, details: Vec<Detail>, labels: &[Label]) -> Entity {
898 Entity::Described(Described {
899 what: value.map(|value| What::Identifier {
900 system: None,
901 value,
902 }),
903 kind: OTHER,
904 role: None,
905 name: Some(name.to_owned()),
906 description: None,
907 details,
908 security_labels: security(labels),
909 })
910}
911
912fn categories(classified: &Classification) -> Vec<Detail> {
914 let mut details = Vec::new();
915 let mut versions = BTreeSet::new();
916 for (category, bases) in classified.categories() {
917 details.push(Detail::new(detail::CATEGORY, category.token()));
918 for basis in bases {
919 details.push(Detail::new(
920 detail::BASIS,
921 format!("{}:{}", category.token(), basis.code()),
922 ));
923 }
924 if let Some(version) = category.version() {
925 versions.insert(format!("{}|{version}", category.system()));
926 }
927 }
928 details.extend(
929 versions
930 .into_iter()
931 .map(|version| Detail::new(detail::CATEGORY_VERSION, version)),
932 );
933 if classified.is_no_category() {
934 details.push(Detail::new(detail::NO_CATEGORY, "true"));
935 }
936 let kinds = classified.no_category_kinds();
937 details.extend(
938 kinds
939 .iter()
940 .map(|kind| Detail::new(detail::NO_CATEGORY_KIND, kind.code())),
941 );
942 if !kinds.is_empty() {
943 details.push(Detail::new(
944 detail::NO_CATEGORY_BASIS,
945 Basis::ResourceKind.code(),
946 ));
947 }
948 if let Some(unclassified) = classified.unclassified() {
949 details.push(Detail::new(detail::UNCLASSIFIED, unclassified.code()));
950 }
951 let counts = classified.evidence_counts();
952 for (name, count_name, ids, count) in [
953 (
954 detail::TEMPLATE_ID,
955 detail::TEMPLATE_ID_COUNT,
956 classified.templates(),
957 counts.templates,
958 ),
959 (
960 detail::ARCHETYPE_ID,
961 detail::ARCHETYPE_ID_COUNT,
962 classified.archetypes(),
963 counts.archetypes,
964 ),
965 (
966 detail::VERSION_UID,
967 detail::VERSION_UID_COUNT,
968 classified.versions(),
969 counts.versions,
970 ),
971 (
972 detail::UNMAPPED_ID,
973 detail::UNMAPPED_ID_COUNT,
974 classified.unmapped(),
975 counts.unmapped,
976 ),
977 ] {
978 details.extend(ids.iter().map(|id| Detail::new(name, id.clone())));
979 if count > ids.len() {
980 details.push(Detail::new(count_name, count.to_string()));
981 }
982 }
983 if let Some(digest) = classified.digest() {
984 details.push(Detail::new(detail::MAP_DIGEST, digest));
985 }
986 details
987}