AuditEvent Profile¶
| Canonical | ../StructureDefinition/nexus-emr-core-auditevent |
|---|---|
| Status | draft (experimental) · 1.26.0 |
| Base | AuditEvent (constraint) |
| Source | FSH · JSON |
Nexus EMR profile for AuditEvent -- the access and activity log: who acted, on which record, when, through what interaction, and with what outcome.
FSH
The authored source of truth: resources/StructureDefinition-nexus-emr-core-auditevent.fsh (edit on GitHub). One FSH file may define several related artifacts.
// NexusEmrCoreAuditEvent.fsh
// FIRST PASS. The access and activity log: who did what, to which record, when, from what
// interaction, and whether it succeeded.
//
// WHAT THIS ANSWERS, AND WHAT ANSWERS IT INSTEAD. Three questions in this estate look alike and
// are answered by three different mechanisms. Reading the wrong one is the most common mistake
// available here, so the boundaries are stated before the constraints:
//
// * "WAS A MACHINE INVOLVED IN PRODUCING THIS CONTENT?" -- the `AIAST` security label on the
// resource that holds the content. One comparison, no second query. See the AI attribution
// pattern in the handbook.
// * "HOW DID THIS RECORD COME TO BE -- from what inputs, under whose supervision?" -- the
// Provenance profile, for AI-mediated and cross-boundary writes, plus the `meta-updated-by`
// extension, which names the user account that wrote a given version so a version list can be
// rendered without a Provenance per row.
// * "WHO TOUCHED THIS RECORD, AND WHEN?" -- this profile. An audit entry is a statement about an
// ACCESS, not about the content: it is written for reads as well as writes, it is written
// whether or not the interaction changed anything, and it is not part of the record it
// describes.
//
// The distinction that matters most: Provenance describes AUTHORSHIP and is written selectively,
// so its absence is inconclusive. An audit entry describes an INTERACTION and is written for every
// one that reaches the FHIR API. A privacy report over access to personal health information is
// this resource's question and cannot be answered from Provenance.
//
// WHERE THE ESTATE'S OWN ACCESS LOG LIVES, AND WHY THIS PROFILE STILL EXISTS. The platform records
// its access log in a dedicated, append-only, tenant-scoped store fed asynchronously from the API
// tier, and serves it through its own administrative search API rather than as FHIR. That store is
// not reachable at `/AuditEvent`, and a consumer should not expect a FHIR search to return the
// estate's access history today.
//
// This profile is therefore two things. It is the shape of an AuditEvent WRITTEN THROUGH the FHIR
// API, which the store accepts like any other resource type. And it is the declared mapping from
// what the access log actually records onto AuditEvent elements, so that a reporting criterion or
// an exported disclosure log has a FHIR element to name rather than a private column. The mapping
// is stated element by element below; each entry says what the log holds and what it does not.
//
// FIRST-PASS SCOPE. Constrained here: the elements the log guarantees on every entry -- the actor,
// the moment, the interaction, its outcome, and the record it concerned. Deliberately NOT
// constrained: the event-type and event-subtype vocabulary, purpose of use, the requesting network
// address, and the payload of the interaction. Each is named below with what would settle it.
// Read the absence of a constraint here as "not yet decided", not as "free to invent".
Alias: $NexusEmrCoreAuditEvent = ../StructureDefinition/nexus-emr-core-auditevent
// The point of an audit entry is to say WHO. Every write path into the platform's access log
// requires the acting identity and supplies it from the request's security context, so an entry
// naming no actor is a malformed one rather than a legitimately anonymous event.
//
// TWO ACCEPTABLE FORMS, because the identity that is always available is not a reference. The
// platform holds an identity-provider subject for the acting user; resolving it to a Practitioner
// needs an identity directory this guide does not describe, and not every actor has a Practitioner
// resource at all. So `altId` satisfies this rule exactly as `who` does -- the same call the
// `meta-updated-by` extension makes, for the same reason.
//
// Warning rather than error: an entry recorded for an unauthenticated or rejected request may
// legitimately have no identity to name, and a validation error would make such an entry
// unstoreable at precisely the moment it is most worth keeping.
//
// Written as `agent.where(...)` rather than the `agent(...)` shorthand so the expression stays
// inside the subset a runtime validator can emit; an invariant outside that subset renders on the
// published page and then checks nothing.
Invariant: auditevent-names-an-actor
Description: "An AuditEvent SHOULD name who acted -- at least one agent carrying a resolvable `who` reference or an `altId` user identifier. An entry naming no actor answers none of the questions an access log exists to answer"
Severity: #warning
Expression: "agent.where(who.exists() or altId.exists()).exists()"
// ===========================================================================
// Profile Definition
// ===========================================================================
Profile: NexusEmrCoreAuditEvent
Parent: AuditEvent
Id: nexus-emr-core-auditevent
Title: "AuditEvent Profile"
Description: """
Nexus EMR profile for AuditEvent -- the access and activity log: who acted, on which record, when,
through what interaction, and with what outcome.
Distinct from Provenance, which describes how a record came to be and is written selectively, and
from the `AIAST` security label, which says only whether a machine produced the content. An audit
entry describes an INTERACTION rather than authorship, is written for reads as well as writes, and
is not part of the record it describes. A report over access to personal health information is this
resource's question.
First pass: constrains the actor, the moment, the interaction, its outcome and the record concerned.
The event-type vocabulary, purpose of use and network attribution are not yet constrained. Closed to
SMART apps pending an exposure decision.
"""
* ^url = $NexusEmrCoreAuditEvent // Canonical URL from alias
* ^status = #draft
* ^experimental = true
* obeys auditevent-names-an-actor
* . ^short = "Nexus EMR AuditEvent Core"
* . ^definition = """
A record of an interaction with the estate: the actor, the moment, the interaction performed, its
outcome, and the records it concerned.
Audit entries are append-only by intent. An entry describes something that happened; correcting it
by rewriting it destroys the property that makes it worth keeping. Where an entry is wrong, record
a further entry.
"""
// THE effective date for this profile (see the EffectiveDate / NoEffectiveDate RuleSets):
// `period.start` where the entry describes an activity with a duration, falling back to `recorded`,
// which R4 makes 1..1 so this always resolves. Unlike a routing envelope or a subscription, an audit
// entry genuinely names a moment -- it is the only thing it is about -- so this profile declares a
// date rather than pinning itself empty.
//
// A CAUTION THAT BELONGS WITH THE DECLARATION: see `recorded` below. The platform's access log
// stamps its entry as it is persisted, not as the interaction happened, and the two differ by the
// queue depth between them. For ordering interactions within a session that difference is
// immaterial; for reconciling an audit entry against an external timestamp it is not.
* insert EffectiveDate([[period.start | recorded]])
* insert Salient(action, action, code)
* insert SalientComment([[The interaction performed, as R4's `audit-event-action` codes. See the element note: the platform records a transmission action that these five codes do not express.]])
* insert Salient(outcome, outcome, code)
* insert SalientComment([[Present only when the outcome is known. An entry recording an outcome the platform could not determine carries no code at all -- treat absence as unknown, never as success.]])
* insert Salient(date, [[period.start | recorded]], dateTime)
* insert SalientComment([[When the entry was recorded, falling back from the activity period where one is given. Not necessarily the instant of the interaction -- see the `recorded` note.]])
* insert Salient(performed_by, [[agent.altId.first()]], string)
* insert SalientComment([[The identity-provider subject of the acting user, as the platform holds it. An opaque account identifier, not a FHIR id and not a Practitioner reference. Resolving it to a person needs the identity directory, which this guide does not describe.]])
// SMART on FHIR exposure -- deliberately NOT declared. The permission family exists on the platform,
// so declaring exposure here WOULD resolve and WOULD grant it; that is exactly why the decision is
// taken rather than inherited. An access log is the record of who reached the chart, and granting a
// third-party application read over it hands that application the reading history of every user in
// the tenant -- a wider disclosure than any single clinical resource it might be granted alongside.
// The permission is held today by administrative and auditor roles inside the estate, not by apps.
//
// What would decide it: a named consumer, the interactions it needs, and a ruling on whether an
// application's grant over an audit trail is scoped to the patients it may already read or to the
// tenant. Until then this type is absent from the server capability statement, and no SMART scope
// naming it is granted.
//
// Write and delete are a separate question and a colder one. An audit trail an application can
// rewrite or destroy is not an audit trail.
* text 0..1 MS // Narrative element with human-readable summary
* text ^short = "Narrative"
* text ^definition = "A human-readable narrative summary of the NexusEmrCoreAuditEvent resource for human interpretation."
// -----------------------------------------
// What happened -- type, subtype, action
// -----------------------------------------
* type MS
* type ^short = "What kind of event this is"
* type ^comment = """
Required by R4 (1..1) and NOT yet fixed by this guide, though two values are recommended and both
come from the standard vocabulary rather than from a local one.
`http://terminology.hl7.org/CodeSystem/audit-event-type#rest` for an entry describing a FHIR API
interaction, which is what a projection of the platform's access log writes for the great majority of
entries. `http://dicom.nema.org/resources/ontology/DCM#110106` (Export) for an entry describing
information LEAVING the estate -- a packet released, a report downloaded, a referral transmitted --
because `action` cannot express that distinction and a disclosure report has to be able to find those
entries. Recommended rather than required: nothing writes an AuditEvent through this API today, so
fixing a value would constrain a producer that does not exist. Do not invent a local vocabulary here.
"""
* subtype 0..* MS
* subtype ^short = "The narrower activity, where the event is not simply a REST interaction"
* subtype ^comment = """
Where an entry describes a named platform activity rather than an interaction with a chart record --
assembling a document packet, launching a billing handoff, running an eligibility check, exchanging
credentials, transmitting a referral -- the activity belongs here rather than on `entity`.
NO VOCABULARY IS PUBLISHED FOR THAT SET. The platform's log carries its own activity names, and this
guide does not yet declare a code system for them: the majority of the values name FHIR resource
types, which already have a published code system, and minting a second one that restates them would
be a permanent commitment to a duplicate. What would settle it is the residue -- the values that name
a platform activity rather than a resource type -- published as a code system in its own right.
Until then, read `subtype` liberally and do not assume a system URI.
"""
* action 0..1 MS
* action ^short = "create | read | update | delete | execute"
* action ^comment = """
R4 binds this to five codes (`C`, `R`, `U`, `D`, `E`) and this guide adds none.
TWO THINGS A READER MUST KNOW. First, every interaction that is not a create, an update or a delete
is recorded as a READ -- searches, version reads and history reads included. `R` therefore means
"the record was disclosed to someone", which is what a privacy report wants, and does not mean a
single-resource GET. A search that returned twelve records yields TWELVE read entries, one per
record returned, rather than one entry for the search: the question an access log has to answer is
which records were disclosed, not how many queries were run. Count records, not entries, and expect
a busy chart view to produce many.
Second, the platform distinguishes a TRANSMISSION -- a referral sent to an external party, a report
exported or downloaded -- from an ordinary read, and R4's five codes do not express that distinction.
Until it is settled, a transmission projected here appears as `R`, and the fact that it left the
estate is carried on `type`/`subtype` rather than on `action`. A consumer answering "what was
disclosed outside the clinic" must therefore not read `action` alone. Settling it means either
assigning the standard export event type on `type` as a required convention, or an extension; this
guide has done neither yet.
"""
// -----------------------------------------
// When
// -----------------------------------------
* recorded MS
* recorded ^short = "When the entry was recorded"
* recorded ^comment = "R4 makes this 1..1, and it is the one element every entry carries. It is the moment the entry was PERSISTED, which is not exactly the moment of the interaction: entries reach the log asynchronously and the stamp is applied on arrival. The difference is small and it is not zero. Order interactions by it freely; reconcile it against an external timestamp with care, and prefer `period` where an entry carries one."
* period 0..1 MS
* period ^short = "When the activity itself occurred"
* period ^comment = "Preferred over `recorded` as THE moment where it is present, for the reason above. The platform's access log does not populate it today, so it is optional and its absence is expected rather than a defect."
// -----------------------------------------
// Outcome
// -----------------------------------------
* outcome 0..1 MS
* outcome ^short = "0 | 4 | 8 | 12 -- whether the interaction succeeded"
* outcome ^comment = """
The platform records three outcomes -- succeeded, failed, and could not be determined -- against
R4's four severity-graded codes. The mapping this guide states: a success is `0`; a failure is `4`
(minor failure), because the platform does not grade failures and claiming a serious or major
failure would assert something it did not observe; an undetermined outcome carries NO code.
READ AN ABSENT `outcome` AS UNKNOWN, NEVER AS SUCCESS. It is the single most consequential misread
available on this resource: an access whose outcome could not be determined is exactly the access a
privacy review wants surfaced, and defaulting it to success buries it.
ONE CLASS OF ENTRY CARRIES A SUCCESS OUTCOME THAT IS NOT A CLAIM ABOUT THE RESULT. An entry recorded
for an INVOKED OPERATION is written when the invocation is accepted, before the operation has
resolved, and records that the attempt was made. Its outcome is not evidence that the operation
succeeded. Entries for record interactions are written on the response and their outcome does reflect
it.
"""
* outcomeDesc 0..1 MS
* outcomeDesc ^short = "Free-text detail about the event"
* outcomeDesc ^comment = "The platform's log carries an optional free-text detail on any entry, not only on a failure. R4 scopes this element to describing the outcome, which is narrower than what is written; a consumer should treat the text as descriptive of the event and should not assume it is present only on failures, nor that it is machine-readable."
// -----------------------------------------
// Purpose of the event -- NOT constrained, and this is where emergency access would go
// -----------------------------------------
* purposeOfEvent 0..* MS
* purposeOfEvent ^short = "Why the interaction took place"
* purposeOfEvent ^definition = """
The reason the interaction occurred, bound extensibly by R4 to the standard purpose-of-use
vocabulary (treatment, payment, operations, emergency treatment, and the rest).
**This guide does not yet constrain it, and emergency access is the reason to say so explicitly.**
Where a clinician overrides a patient's restriction on their record to obtain information in an
emergency, this element -- together with `agent.purposeOfUse` and `agent.policy` -- is where base R4
records that the access was an override and under what authority. That is the shape a reader should
expect if and when such a record appears.
**No such record is produced in this estate today.** Nothing implements an emergency-access
override, and this guide therefore constrains nothing about one: no code is fixed, no element is
made mandatory, and no invariant asserts that an override carries a justification. Read the absence
of `purposeOfEvent` on an entry as "not recorded", not as "routine access".
What would settle it: the override itself becoming a product behaviour, at which point the question
this guide has to answer is whether the override is a coded purpose on the access entry, as here, or
a separate record of its own with the access entry pointing at it. The two differ in whether the
justification is queryable independently of the access it authorised.
"""
// -----------------------------------------
// Agent -- WHO acted
// -----------------------------------------
* agent MS
* agent ^short = "Who acted"
* agent ^definition = """
The actor or actors involved in the interaction. R4 makes this `1..*`.
The platform's access log records exactly ONE actor per entry: the identity that made the request.
Entries carrying a second agent -- a recipient of a disclosure, a system acting on a user's behalf --
are permitted by R4 and are not produced today.
"""
* agent.who 0..1 MS
* agent.who ^short = "The actor, where it resolves to a resource"
* agent.who only Reference(NexusEmrCorePractitioner or NexusEmrCorePractitionerRole or NexusEmrCorePatient or NexusEmrCoreRelatedPerson or NexusEmrCoreOrganization or NexusEmrCoreAppDevice or NexusEmrCoreEmrDevice)
* agent.who ^comment = "Left OPTIONAL deliberately. The identity the platform holds for an actor is an identity-provider subject, not a resource reference, and resolving it needs an identity directory this guide does not describe; not every actor has a Practitioner resource at all. Where a producer CAN resolve the actor it should write both this and `altId`. Per the Nexus EMR logical-reference model the reference MAY be identifier-only."
* agent.altId 0..1 MS
* agent.altId ^short = "The acting user's identity-provider subject"
* agent.altId ^definition = """
The opaque account identifier the identity provider issues for the acting user (for example
`auth0|abc123`). This is the identity the platform actually holds on every entry, and it is the
element to match on when correlating an audit entry with the `meta-updated-by` extension on a
resource version -- the two carry the same kind of value for the same reason.
NOT a FHIR id and NOT a Practitioner reference. Resolving it to a person needs the identity
directory, which this guide does not describe.
"""
* agent.name 0..1 MS
* agent.name ^short = "Human-readable label for the actor"
* agent.name ^comment = "A display label -- the actor's username, falling back to their email address where no username is held. FOR DISPLAY ONLY. It is not stable, it is not unique, and it must never be used for an authorization or matching decision; `altId` is the identity."
* agent.requestor MS
* agent.requestor ^short = "Whether this agent initiated the interaction"
* agent.requestor ^comment = "R4 makes this 1..1. The single agent on a platform-produced entry is the requester, so `true` is the expected value; an entry naming a second, non-initiating agent sets it `false` on that agent."
* agent.policy 0..* MS
* agent.policy ^short = "The authority the interaction was performed under"
* agent.policy ^comment = "Where an interaction was authorised by something nameable -- a consent directive, a provincial consent-override transaction -- its identifier belongs here as a URI. Not produced today; named so that a producer of such a record has a declared element rather than an invented one."
* agent.network 0..1 MS
* agent.network ^short = "Where the request came from"
* agent.network ^comment = "NOT populated. The platform's access log does not record the requesting network address on ordinary entries, so a consumer must not expect one and must not treat its absence as evidence about the origin of a request. Constraining it would assert a guarantee that does not exist. What would settle it is the address being captured at the API tier; the element is named here so that it lands in the right place when it is."
// -----------------------------------------
// Source -- WHICH system recorded the entry
// -----------------------------------------
* source MS
* source ^short = "The system that recorded the entry"
* source ^comment = "R4 makes `source` 1..1 and `source.observer` 1..1, so any conformant instance must name an observer. The platform's access log does not record which service produced an entry: entries arrive from every service in the estate on one shared channel and carry no producer identity. A projection therefore has to supply the observer from the endpoint doing the projecting rather than read it from the entry. Treat `source.observer` as naming the reporting endpoint, not the service where the interaction occurred."
* source.site 0..1 MS
* source.site ^short = "The logical source location"
* source.site ^comment = "Not populated. The estate is multi-tenant and an entry is scoped to its tenant, but tenancy is carried out of band rather than on the resource, so this element does not receive it. A projection serving more than one tenant must scope its results rather than expecting a reader to filter on this."
// -----------------------------------------
// Entity -- WHAT the interaction concerned
// -----------------------------------------
* entity 0..* MS
* entity ^short = "The records the interaction concerned"
* entity ^definition = """
What the interaction was about. For an interaction with a chart record this is the record itself;
an entry may name more than one, and a single interaction that touched several records names each.
The platform's log carries the target of the interaction plus a set of correlation identifiers
gathered from the enclosing operation, so that the entries written for one transaction can be
retrieved together. Projected here, each of those is an `entity`.
COVERAGE IS THE WHOLE TYPE SPACE, not the set this guide profiles. Every R4 resource type reaching
the FHIR API is classified and logged, including types this specification does not model. A
consumer should not assume that the types appearing in an access log are the types described here.
"""
* entity ^comment = "Optional (`0..*`) because entries describing an activity rather than a record -- a credential exchange, a report run -- legitimately concern no resource."
* entity.what 0..1 MS
* entity.what ^short = "The record concerned"
* entity.what ^comment = "Per the Nexus EMR logical-reference model this MAY be identifier-only, and in a projection of the platform's log it usually IS: the log stores the target's identifier rather than a resolvable reference. A consumer treating a reference with no literal `reference` as absent will read most entries as concerning nothing. NOTE also that the log records a target identifier only where the record's id takes the platform's identifier form; entries for records whose id does not take that form are recorded WITHOUT a target, and the interaction is then reconstructable only from the entry's other elements."
* entity.type 0..1 MS
* entity.type ^short = "What kind of thing the record is"
* entity.type ^comment = "Bound extensibly by R4 to a four-value vocabulary (person, system object, organization, other), which is coarser than the resource type a reader usually wants. Where `entity.what` is a typed reference the resource type is readable there and is the better answer; this element says only which of the four kinds it falls into."
* entity.detail 0..* MS
* entity.detail ^short = "Named values carrying detail about the record concerned"
* entity.detail ^comment = "Where an entry needs to carry a measured value about what it describes -- how many pages a disclosure withheld, which parameters a report ran with -- this is the element for it, as a named type with its value. No detail names are fixed by this guide, so a consumer must read the `type` string rather than assume a position."