Skip to main content

ehds_logging/balp/
mod.rs

1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! An access record written as an IHE BALP `AuditEvent` (feature `balp`),
5//! through the audit types of `ihe-atna`.
6//!
7//! The recording system received the request, so the record is the server's
8//! side of the `RESTful` pattern its action names (BALP 1.1.4): a query is
9//! `IHE.BasicAudit.Query`, a read `IHE.BasicAudit.Read`, a create
10//! `IHE.BasicAudit.Create`, an update `IHE.BasicAudit.Update` and a delete
11//! `IHE.BasicAudit.Delete`, each its `Patient` variant when the record names
12//! one patient, as the implementation guide lists them: the one the request
13//! named, or the one the identity service found behind the `ehr_id` it
14//! reached. Its `entity:patient` slice holds one patient, so a record that
15//! names several writes each and claims the plain pattern. The Delete pattern
16//! fixes `outcome` to `0`, so a failed delete claims no profile. The person
17//! and the client are the token's (BALP 1.1.4 §3:5.7.5.4), with the provider
18//! as an agent of its own, the request id is `entity:transaction`, and the
19//! query `entity:query`.
20//!
21//! The person's agent, `agent:user`, alone carries what Annex II 3.2(b) asks
22//! about the natural person: the professional's name as `who.display` and
23//! their identifier as an `ihe-otherId` extension typed `NPI`, as BALP
24//! 1.1.4 §3:5.7.5.4 maps the IHE IUA `subject_name` and
25//! `national_provider_identifier`; the assurance level of the
26//! authentication, when one was established, as an `ihe-assuranceLevel`
27//! extension coded `low`, `substantial` or `high` (Regulation (EU) No
28//! 910/2014 Art 8(2)); and who acts, `person` or `client`, as its `role`.
29//!
30//! The data a read, create, update or delete accessed is `entity:data`,
31//! named by `Request::resource` as the recording system declares its ids
32//! ([`ObjectIds`]): `what.reference` for a literal reference to a resource
33//! it serves, and `what.identifier` for any other id, such as an openEHR
34//! version uid, since FHIR R4 names an object no FHIR server makes available
35//! by `Reference.identifier` (references.html §2.3.0.2, §2.3.0.3). BALP
36//! 1.1.4 requires `entity:data.what` and admits either.
37//!
38//! The policies the access was authorised under, such as a data permit
39//! (Regulation (EU) 2025/327 Art 68), are `agent:user` `policy` entries
40//! after the token's `jti`: FHIR R4 defines `AuditEvent.agent.policy` as
41//! "the policy or plan that authorized the activity being recorded", and "a
42//! single activity may have multiple applicable policies". BALP 1.1.4's
43//! OAuth patterns fix that `policy` to the `jti` alone, so a record with a
44//! policy beside the token claims no `OAUTHaccessTokenUse` profile. A DICOM
45//! PS3.15 audit message has no element for a policy (A.5.1.1), so the
46//! permit reaches an Audit Record Repository in the `AuditEvent` the FHIR
47//! Feed sends.
48//!
49//! What Annex II 3.2 adds to a BALP record has no element of its own (BALP
50//! defines none for a data category), so it rides in entities of type `4`,
51//! which no slice of a pattern is discriminated by: one for the categories
52//! and the record's retention, one per origin and one per `ehr_id`, each
53//! with its `detail` entries named as [`detail`] lists them (no
54//! specification governs the names: our own design). The professional and
55//! the provider a national contact point relays (Implementing Regulation
56//! (EU) 2026/2099 Annex Tables 1 and 2) ride the same way, in one entity
57//! `ehds-relayed` that names the contact point and marks them `asserted`,
58//! and a correlation identifier the client sent is a `detail` of the request
59//! id's entity.
60//!
61//! The record's security labels ([`crate::label`], Annex II 3.4) are each
62//! the `securityLabel` of the entity it belongs to: the categories' on the
63//! `ehds-categories` entity and each origin's on its `origin` entity, which
64//! the `RESTful` ATNA mapping keeps as the DICOM
65//! `ParticipantObjectSensitivity` in both directions (`RESTful` ATNA Rev. 3.6
66//! Tables 3.20.4.2.2.1-1 and 3.81.4.2.2.1-1), as BALP puts the labels of the
67//! data on its entity (`IHE.BasicAudit.Read`, `entity:data.securityLabel`).
68//! Each label is written there alone: the `ehds-categories` entity is the
69//! category record of Annex II 3.2(c), each `origin` entity the record of an
70//! origin, and `meta`, which neither mapping carries, would hold a second
71//! copy on every record, so the record writes no `meta.security`.
72//!
73//! An emergency access (Regulation (EU) 2025/327 Art 11(5)) keeps its
74//! purposes of use where every purpose is written, in `agent:user`
75//! `purposeOfUse` and in `purposeOfEvent`, and is marked in one entity of
76//! its own, `ehds-emergency-access`, whose `description` states the mark in words and
77//! whose `detail` entries name the purposes that marked it, so a reader of
78//! the record need not know which codes the deployment maps, and each member
79//! whose consent pre-filter denial was set aside for it.
80//!
81//! A population access, a query over many patients' data whose record names
82//! none of them, is marked in one entity `ehds-population`, whose
83//! `description` says in words that the query ran over each origin the
84//! record names and that the record confirms no read of any one person's
85//! record, and whose `detail` entries name why no `ehr_id` is named and the
86//! template and archetype ids the query read (Regulation (EU) 2025/327 Art
87//! 9(1)). BALP 1:52.4.1.6 recommends one record per patient "driven by a
88//! Policy"; this record names the scope, never a person.
89//!
90//! Each semantic transformation the access applied is one entity of its own
91//! ([`transformation`]), in the same `AuditEvent` as the access, so it is
92//! kept and found with it (eHealth Network General Guidelines Release 3.1
93//! Art 6(3)).
94//!
95//! # Examples
96//!
97//! A record of a read, handed to the BALP sink, which writes it as an
98//! `AuditEvent` and gives it to an `ihe-atna` recorder. The sink names the
99//! recording system by `endpoint`: the address its requests arrive at, its
100//! base URL or, when it knows only that, its host or IP address, written as
101//! the network address of its own agent.
102//!
103//! ```
104//! use std::collections::BTreeMap;
105//! use std::sync::Arc;
106//!
107//! use ehds_logging::balp::BalpSink;
108//! use ehds_logging::classify::{Basis, Evidence, RootObject};
109//! use ehds_logging::label::LabelPolicy;
110//! use ehds_logging::map::{CategoryMap, Declared};
111//! use ehds_logging::record::{
112//!     AccessRecord, Accessor, Acting, Action, DataSubject, Outcome, Professional, Request,
113//! };
114//! use ehds_logging::retention::RetentionPolicy;
115//! use ehds_logging::sink::AccessSink;
116//! use ihe_atna::balp::{AuditError, AuditRecorder, Exchange, NetworkAddress};
117//!
118//! /// A recorder that keeps nothing, where a deployment spools for its
119//! /// Audit Record Repository.
120//! struct Recorder;
121//!
122//! #[async_trait::async_trait]
123//! impl AuditRecorder for Recorder {
124//!     async fn record(&self, _exchange: Exchange) -> Result<(), AuditError> {
125//!         Ok(())
126//!     }
127//! }
128//!
129//! # #[tokio::main(flavor = "current_thread")]
130//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
131//! let templates = BTreeMap::from([(
132//!     "Example Lab Report.v1".to_owned(),
133//!     Declared::Codes(vec!["Laboratory-Reports".to_owned()]),
134//! )]);
135//! let map = CategoryMap::declare(&[], &templates, &BTreeMap::new())?;
136//! let read = RootObject {
137//!     template_id: Some("Example Lab Report.v1".to_owned()),
138//!     ..RootObject::default()
139//! };
140//! let categories = map.classify(&Evidence::reached(Basis::Returned, vec![read]));
141//! let recorded = jiff::Timestamp::now();
142//! let record = AccessRecord {
143//!     action: Action::Read,
144//!     recorded,
145//!     outcome: Outcome::Success,
146//!     outcome_description: None,
147//!     accessor: Accessor {
148//!         issuer: Some("https://idp.example.org".to_owned()),
149//!         subject: "Qz7-clinician".to_owned(),
150//!         client_id: Some("ward-app".to_owned()),
151//!         audience: None,
152//!         provider: None,
153//!         acting: Some(Acting::Person),
154//!         roles: Vec::new(),
155//!         token_id: None,
156//!         assurance: None,
157//!         professional: Professional::default(),
158//!         alt_id: None,
159//!         purposes: Vec::new(),
160//!         policies: Vec::new(),
161//!         relayed: None,
162//!     },
163//!     subject: DataSubject::default(),
164//!     retention: RetentionPolicy::default().retain(recorded, &categories, []),
165//!     labels: LabelPolicy::default().label(&categories, []),
166//!     categories,
167//!     delivered: Some(1),
168//!     origins: Vec::new(),
169//!     origin_count: None,
170//!     request: Request {
171//!         id: Some("Qz7-request".to_owned()),
172//!         operation: "composition_get".to_owned(),
173//!         operation_system: None,
174//!         resource: None,
175//!         query: None,
176//!         stored_query: None,
177//!         client_address: None,
178//!         correlation: None,
179//!     },
180//!     emergency: None,
181//!     inserted: None,
182//!     transformations: Vec::new(),
183//! };
184//! let sink = BalpSink::new(Arc::new(Recorder), NetworkAddress::host("cdr.example.org"));
185//! sink.store(record).await?;
186//! # Ok(())
187//! # }
188//! ```
189
190pub 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
216/// The name of the entity that marks an emergency access.
217pub const EMERGENCY_ENTITY: &str = "ehds-emergency-access";
218
219/// The `description` of the entity that marks an emergency access.
220pub 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
224/// The name of the entity that marks a population access.
225pub const POPULATION_ENTITY: &str = "ehds-population";
226
227/// The `description` of the entity that marks a population access.
228pub 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
233/// The sink that writes each record as a BALP `AuditEvent` and hands it to
234/// an `ihe-atna` audit recorder, such as one that spools it for an ATNA
235/// Audit Record Repository.
236pub struct BalpSink {
237    recorder: Arc<dyn AuditRecorder>,
238    endpoint: NetworkAddress,
239    ids: ObjectIds,
240}
241
242/// What the ids a recording system passes as `Request::resource` are, which
243/// decides how `entity:data` names the data a read, create, update or delete
244/// accessed.
245///
246/// FHIR R4 `Reference.reference` holds a location a FHIR server resolves, a
247/// relative `[type]/[id]` or an absolute URL, and `Reference.identifier`
248/// names an object no FHIR server makes available (references.html
249/// §2.3.0.2, §2.3.0.3). The record writes no other value as a reference, so
250/// an id that is no literal reference is an identifier whatever is declared.
251#[derive(Debug, Clone, Default, PartialEq, Eq)]
252#[non_exhaustive]
253pub enum ObjectIds {
254    /// The ids are literal references to the FHIR resources the recording
255    /// system serves, written as `what.reference`. An id that is no literal
256    /// reference ([`ihe_atna::balp::is_literal_reference`]) is written as
257    /// `what.identifier` with no `system`.
258    #[default]
259    References,
260    /// The ids are identifiers the recording system issues in the namespace
261    /// `system`, written as `what.identifier` whatever their form.
262    Identifiers {
263        /// `what.identifier.system`: the namespace, an absolute URI (FHIR R4
264        /// `Identifier.system`), or `None` when the system names none.
265        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    /// The sink that records through `recorder`, naming the recording system
277    /// by `endpoint`, the address its requests arrive at: the base URL it
278    /// serves, or its host or IP address when it knows only that, written as
279    /// the network address of the recording system's agent.
280    #[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    /// The sink whose records name the data a read, create, update or delete
290    /// accessed as `ids` declares the recording system's ids; a sink built
291    /// by [`new`](Self::new) takes them as [`ObjectIds::References`].
292    #[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
322/// The place of one record [`BalpSink`] took: the recorder's own place, or
323/// none when the recorder holds no bounded store.
324struct 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/// The BALP exchange `record` is written as, the recording system named by
352/// `endpoint`, the address its requests arrive at, with its ids taken as
353/// [`ObjectIds::References`].
354#[must_use]
355pub fn exchange(record: &AccessRecord, endpoint: &NetworkAddress) -> Exchange {
356    exchange_with(record, endpoint, &ObjectIds::default())
357}
358
359/// The BALP exchange `record` is written as, the recording system named by
360/// `endpoint`, the address its requests arrive at, whose ids `ids` declares.
361#[must_use]
362pub fn exchange_with(
363    record: &AccessRecord,
364    endpoint: &NetworkAddress,
365    ids: &ObjectIds,
366) -> Exchange {
367    let accessor = &record.accessor;
368    // NOTE: no specification governs the acting codes: our own design; BALP 1.1.4 names no element
369    // for who acts, and §3:5.7.5.4 writes the IUA subject_role at agent[user].role beside it.
370    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        // NOTE: no specification governs these codes: our own design; BALP 1.1.4 leaves the
400        // assuranceLevel vocabulary open.
401        .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        // NOTE: RESTful ATNA Rev. 3.6 Table 3.81.4.2.2.1-1 maps `entity.securityLabel`
430        // and no `meta`, so every label rides on its entity alone (#108, #111).
431        security: Vec::new(),
432    }
433}
434
435/// A code no system defines.
436fn code(code: &str) -> Code {
437    Code {
438        system: None,
439        code: code.to_owned(),
440    }
441}
442
443/// What the pattern of `record`'s action fixes.
444fn kind(record: &AccessRecord) -> EventKind {
445    let patient = patients(record).len() == 1;
446    // NOTE: every BALP 1.1.4 RESTful pattern slices subtype with rules open, so the operation
447    // follows the pattern's restful-interaction code.
448    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
526/// The entities of `record`: the request id, the query or the resource,
527/// the patient, each `ehr_id`, the categories, and each origin.
528fn 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    // NOTE: BALP 1.1.4 entity:transaction is 0..1 and needs what.identifier.value, so a request
535    // with no id writes none, and its correlation id rides on the categories entity.
536    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
612/// The `what` of `entity:data`: `resource` as `ids` declares it, or, for a
613/// request that addressed none, the operation as an identifier no namespace
614/// issues.
615fn data(resource: Option<&str>, operation: &str, ids: &ObjectIds) -> What {
616    // NOTE: BALP 1.1.4 IHE.BasicAudit.Read requires entity:data.what (1..1), so a request that
617    // addressed no resource names its operation, which no FHIR server resolves (R4 §2.3.0.3).
618    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
640/// The `detail` entries of how long a record is kept: its years, for a
641/// period in years, its period, its end date, unless it is kept without end,
642/// what called for the period, and the ground of its floor.
643fn 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
663/// The entities of whose data `record` concerned: each patient it names, by
664/// an identifier whose namespace is its system when it is a URI and a detail
665/// when it is not, and each `ehr_id` it reached.
666fn 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
695/// The entity of `origin`, labelled `labels`: how it answered, its node, its
696/// `system_id`, its rows, the version a write left there, the `ehr_id` an
697/// EHR create left there and its categories.
698fn 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
749/// The entity that marks an emergency access, naming the purposes that
750/// marked it and each member whose consent pre-filter denial was set aside.
751fn 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
776/// The security labels `labels` are written as.
777fn 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
788/// The entity that marks a population access, with why it names no
789/// `ehr_id` and the template and archetype ids the query read.
790fn 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
821/// `<system>|<code>`, or `<code>` when no system is named.
822fn coded(system: Option<&str>, code: &str) -> String {
823    match system {
824        Some(system) => format!("{system}|{code}"),
825        None => code.to_owned(),
826    }
827}
828
829/// The `detail` entries of what a national contact point relayed, marked
830/// as its assertion.
831fn 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
864/// Every patient `record` names, each once: the one the request named, and
865/// each one the identity service found behind an `ehr_id` the access
866/// reached.
867fn 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
889/// Whether `namespace` is an absolute URI (RFC 3986 §4.3), which FHIR R4
890/// `Identifier.system` takes.
891fn is_uri(namespace: &str) -> bool {
892    Url::parse(namespace).is_ok()
893}
894
895/// An entity of type `4` named `name`, identified by `value` when given,
896/// labelled `labels`.
897fn 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
912/// The `detail` entries of `classified`.
913fn 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}