Consent
A node checks consent itself, and a deployment may add a pre-filter that drops members before dispatch. This page covers both, and the Dutch pre-filter, Mitz.
Each node checks consent before it releases data, whatever the gateway did first (N26, N27). The gateway never decides on release itself, and a node it dispatches to is not thereby cleared: a localization or consent service that named the node only means nothing upstream ruled it out (§14.3).
A node’s own refusal. ITS-REST defines no consent signal, so the gateway
does not infer one from a status code. A node’s answer is reported
consent-denied only when it is a 403 whose ITS-REST Error body carries a
code that the registry lists for that endpoint in consent_refusal_codes
(The registry). Every other refusal is
node-error. The list is empty by default, so until you name the codes a
node uses, its consent refusal fails the query 424 as any node error does.
This key is FerroFED’s own design, because no specification defines the
signal. A refusing node contributes no rows, never fails the query in either
completeness mode, clears meta.federation.complete, and its record carries
the latency_ms of the request it refused (§11.3, N40).
The optional Step-1 pre-filter. A deployment with a consent service may
drop members before dispatch (N27a). The pre-filter runs after localization
and before resolution. Each member it denies is reported consent-denied with
no latency_ms, is never resolved and never sent a request, and any ehr_id
the client session cached for it is dropped. A member it does not deny is
asked, and its node decides. When the consent service cannot answer, Step 1
carries no consent signal, which is the state of a deployment with no consent
service at all, so every candidate is asked and each node checks consent
itself (§13.2.1, N27a). This pass-to-node policy is FerroFED’s own design.
The outage is never silent: the answer to a query carries it as
meta.federation.consent.error, mirroring localization.error of §14.1
(The client contract), the pre-filter’s
state on GET {base}/operator/dependencies turns down or failing
(Health probes), and each call is counted in
ferrofed_consent_prefilter_requests_total (Metrics). A call
the pre-filter could not put to its service at all, for the patient’s
namespace or for missing caller claims, is counted as not-asked with that
reason and leaves the health state as it was, since the service saw
nothing. The client’s answer is the one a pre-filter that found nothing
gives. The pre-filter applies to every patient route: a federated query and the read
of an EHR by subject
(Follow-ups).
OPTIONS {base}/ declares a configured pre-filter under federation.consent,
with its mode, that policy and disclose; a deployment with no pre-filter
declares nothing there. A deployment under Regulation (EU) 2025/327 Art 8
sets [federation.consent] disclose = false, so an answer never shows an
exclusion (Withholding consent exclusions).
An emergency request. A caller asserts an emergency access through the
purpose of use of its verified token, matched against
[[access_log.emergency_purpose]]
(Emergency access). Whether the pre-filter’s
exclusion applies to such a request is your choice, under
[federation.consent] emergency:
[federation.consent]
emergency = "apply" # or "pass-to-node"
"apply", the default: the pre-filter applies to every request, an emergency one included. A member it denies is not asked."pass-to-node": for a request whose token declares an emergency purpose, the pre-filter is still asked, and every member it denies is asked all the same. Its node decides on its own consent, restrictions and the purpose the request conveys (N26, N27). The access record names each such member in itsehds-emergency-accessentity (Emergency access), whatever the node answered; a request whose record cannot be stored is refused503 access-unrecorded. The answer, the status and the log line say nothing of it (Art 8). The setting-aside holds for that one request: the gateway learns no resolution binding and noehr_idroute from it, so the next request is filtered again. A request without an emergency purpose is filtered as before.
The gateway never decides on its own that an emergency justifies setting a
consent decision aside. Regulation (EU) 2025/327 leaves whether the vital
interests of the data subject override a restriction or an opt-out to the
Member State (Art 8, Art 10(2), Art 11(5)), and release is the node’s
decision (N26, N27). The pre-filter is optional (N27a), so whether it
applies to an emergency request is a deployment setting, and the default
keeps it for every purpose. "pass-to-node" needs at least one
[[access_log.emergency_purpose]], or the configuration is refused.
OPTIONS {base}/ declares the setting as federation.consent.emergency.
For development, rows under [[dev.consent_denied]] beside the
cross-reference are a static pre-filter, accepted only under
profile = "development" and declared as development-static:
[[dev.consent_denied]]
namespace = "urn:oid:2.999.1.1"
value = "ffd-test-0001"
member = "node-b" # this patient's consent denies asking node-b
In the Netherlands the pre-filter is Mitz, below. Set [[dev.consent_denied]]
rows or [nl_gf.mitz], never both: both refuse the configuration.
Dutch consent: [nl_gf.mitz]
Mitz keeps the consent Dutch patients record and answers one closed
question about it, the gesloten autorisatievraag (Annex B §B.6): may this
data holder make this patient’s data of these categories available to this
data user, for this purpose? The gateway asks it once per data holder among
the candidates, after localization and before resolution. A member whose
holder Mitz denies for every category asked is consent-denied and never
asked. Every other member is asked, and its node checks consent itself:
Mitz is a filter in front of the gate, not the gate (§14.3, N27).
The wire is the VZVZ Implementatiehandleiding Open en gesloten
autorisatievraag 3.8.2: a SOAP 1.2 request carrying one XACML 3.0
XACMLAuthzDecisionQuery, over mutual TLS, with an X-Request-Id on every
request. VZVZ states no licence for the document, so the repository pins it
by sha256 and does not ship it; scripts/vendor/mitz.sh fetches it for
reading.
profile = "production"
[nl_gf.mitz]
url = "https://mitz.example.org/geslotenautorisatievraag"
client_identity_file = "/run/secrets/mitz-client.pem" # mutual TLS
trust_roots_file = "/etc/ferrofed/mitz-roots.pem" # optional
credentials = { bearer_token_file = "/run/secrets/mitz-token" } # optional
namespaces = ["urn:oid:2.999.1"] # client namespaces that stand for the BSN
purpose = "TREAT" # TREAT or COC
data_categories = ["GGC002"] # the Mitz data categories asked about
timeout_ms = 1000 # one round of questions, within the query's budget
[nl_gf.mitz.holders] # each member's care provider
"node-a" = { type = "V6" } # the URA from [nl_gf.nvi.custodians] or the directory
"node-b" = { type = "V6", ura = "ura-test-0002" }
[[auth.issuer]] # the issuer of your callers' tokens
issuer = "https://issuer.example.org"
jwks_uri = "https://issuer.example.org/jwks"
[auth.issuer.requester] # the claims of its tokens that name the requester
professional = "uzi_number" # the professional's UZI number
role = "uzi_role" # the professional's UZI role code
organisation = "ura" # the organisation's URA
organisation_type = "organisation_type"
timeout_ms bounds one round of questions, and it is a part of
federation.overall_timeout_ms: with pdqm.timeout_ms and the localizer’s
budget it must end before the overall budget, or the configuration is
refused, naming the keys (§11.5). OPTIONS {base}/ declares it as
timeout.consent_ms.
What a deployment must provide:
- The BSN. Mitz is asked by BSN. A client names the patient in a BSN
system (
http://fhir.nl/fhir/NamingSystem/bsn, or the BSN’s OID asurn:oid:2.16.840.1.113883.2.4.6.3or dotted) or in onenamespaceslists. A patient named by the pseudonymised BSN, as the NVI requires, cannot be asked about: the pre-filter then carries no consent signal and every candidate is asked, and the call is counted asnot-askedwith the reasonnamespace(Metrics). The pseudonym’s system is never accepted innamespaces. The BSN reaches Mitz and nothing else: never a node, a log line or an error. - A holder per member. Every registry member needs a
type, and one URA from itsura,[nl_gf.nvi.custodians]or a directory that publishes URAs; where more than one gives it they must agree. A member with no holder, a holder with no URA, or a holder naming no member refuses the configuration. - The requester, in the caller’s token. The question names the
professional who asks, by UZI number and role, and their organisation, by
URA and type. That is always the verified caller: Mitz records the
professional and decides on their role, so the gateway never asks for
anyone else. Map, per trusted issuer, the four token claims that carry
them under
[auth.issuer.requester](Client authentication); no specification the gateway binds names these claims, so each is configured and none has a default. A caller whose token does not carry all four is not asked about: Mitz is not called, no member is filtered, and each node checks consent itself (N27). The call is counted asnot-askedwith the reasoncaller-claims. A token that carries them in a form the question does not take, such as a UZI number that is not alphanumeric, is treated the same way, with the reasoncaller-claims-invalid, and so is a patient value the question does not take as a BSN, withpatient-value. Neither is reported as a Mitz outage, since Mitz is never called, and no claim value appears in a label, a log line or an error. - TLS. The
urlmust behttpsoutsideprofile = "development".credentialstakes a bearer token or basic credentials, never an OAuth 2.0 grant. Whether a gateway may ask Mitz at all is a matter of admission to the Mitz afsprakenstelsel.
The question carries the purpose purpose configures, TREAT or COC,
the only values Mitz 3.8.2 admits (§3.2.4.2), never the caller’s purpose
of use. Mitz has no emergency consultation situation, and the Wabvpz has
no emergency exception to its Art 15a, so config check notes
emergency = "pass-to-node" beside [nl_gf.mitz]: a member Mitz denies is
then asked under a legal basis you hold, and its node applies the rules
that bind it.
Mitz answers Permit or Deny per category. Indeterminate, a fault, a
status other than 200, silence past timeout_ms and an answer that does
not hold to the question are no decision: the members of that holder are
asked, and the failure is carried in meta.federation.consent.error. Mitz
3.8.2 §3.2.4.6 tells the data holder’s own system to treat Indeterminate
under explicit consent as a Deny; the node is that system, and its own
Mitz check decides (N27). When
Mitz denies one holder and fails for another, the denied members are
consent-denied, the others are asked, and the failure is still carried.
OPTIONS {base}/ declares the pre-filter as "nl-gf-mitz".