@@ -15,6 +15,12 @@ This file documents conventions for the demo Symfony 7.4 application.
1515
1616Located in ` src/Controller/ ` . Use ` #[Route] ` attribute for routing. Extend ` AbstractController ` .
1717
18+ - ` SdcController ` (` /sdc ` ) — the SDC Populate/Extract playground: pick or paste a Questionnaire, fill in
19+ its form (repeats, ` enableWhen ` , quantity, ` answerValueSet ` code-checking all supported), Populate it
20+ from launch-context JSON, and Extract into a transaction Bundle. A curated example gallery and a
21+ "View QuestionnaireResponse JSON" toggle are on the page itself. See "External FHIR/Terminology
22+ Servers" below for its optional live-server env vars.
23+
1824## FHIR Services (via FHIRBundle)
1925
2026Inject by type hint — all autowired:
@@ -30,6 +36,78 @@ Inject by type hint — all autowired:
3036
3137` config/packages/fhir.yaml ` — configures default version, output/cache directories, validation, and FHIRPath cache.
3238
39+ ## External FHIR/Terminology Servers (SDC Playground)
40+
41+ The ` /sdc ` page is offline-first by default. Two env vars opt it into live server connectivity:
42+
43+ - ` FHIR_SERVER_URL ` — when set, ` application/x-fhir-query ` ` itemPopulationContext ` /` variable ` directives
44+ in ` $populate ` resolve against this server instead of being skipped with an informational issue.
45+ - ` FHIR_TERMINOLOGY_SERVER_URL ` — when set, an ` answerValueSet ` -bound choice item's "Check code" action
46+ validates against this server instead of showing a "no terminology server configured" note.
47+
48+ Both default to empty string (offline). Set them via a real environment variable or ` .env.local `
49+ (gitignored) — ** not** ` demo/.env ` , which is off-limits to agent edits in this repo; the empty defaults
50+ are declared instead as committed ` parameters: ` in ` config/services.yaml ` using Symfony's
51+ ` env(NAME): 'default' ` convention, so no ` .env ` edit is ever required to keep the demo offline by default.
52+
53+ Server base URLs are ** operator-configured only** — there is deliberately no "enter a FHIR server URL"
54+ field anywhere in the UI. A public URL-entry field would turn the demo host into an open SSRF proxy
55+ against whatever network it runs on. See ` src/Sdc/ExternalClientFactory.php ` and the
56+ ` FHIRHttpClientInterface ` /` FHIRTerminologyClientInterface ` factory wiring in ` config/services.yaml ` .
57+
58+ Overriding these interfaces is app-wide, not scoped to ` /sdc ` : setting ` FHIR_SERVER_URL ` also makes
59+ ` /fhirpath ` 's ` resolve() ` /` memberOf() ` go live, and ` FHIR_TERMINOLOGY_SERVER_URL ` also makes ` /validate ` 's
60+ terminology binding checks go live — matching ` FHIRBundle ` 's own documented override pattern.
61+
62+ ### FHIR server authentication (M06)
63+
64+ Two independent, composable authentication mechanisms for the ` FHIR_SERVER_URL ` connection. The
65+ * destination* is always operator-configured only (env var/` .env.local ` , never a request-time input); the
66+ * credential values* can additionally come from a visitor's own session — see "Session-scoped credential
67+ entry (M07)" below.
68+
69+ - ** OAuth 2.0 client credentials grant** — ` FHIR_SERVER_OAUTH_TOKEN_URL ` , ` FHIR_SERVER_OAUTH_CLIENT_ID ` ,
70+ ` FHIR_SERVER_OAUTH_CLIENT_SECRET ` . The token URL is required to enable OAuth at all; the client id and
71+ secret must either both be present (from env vars, a visitor's session, or one of each) or both absent
72+ — a mismatched pair fails loudly at container-resolution time. Token-URL-alone (no id/secret anywhere
73+ yet) is a valid state: OAuth is enabled but not yet authenticated for this request. The library fetches
74+ and caches the bearer token itself (`Ardenexal\FHIRTools\Component\HttpClient\OAuth\
75+ OAuthClientCredentialsTokenProvider` / ` OAuthHttpClient`).
76+ - ** A manual header** — ` FHIR_SERVER_AUTH_HEADER_NAME ` / ` FHIR_SERVER_AUTH_HEADER_VALUE ` , attached
77+ verbatim to every request (` StaticHeaderHttpClient ` ). Covers a hand-obtained `Authorization: Bearer
78+ <token >` or an ` X-Api-Key`-style header. Same shape as OAuth: the name enables the mechanism, the value
79+ can arrive later (env or session).
80+
81+ Both mechanisms can be configured together as long as they don't both target the ` Authorization ` header
82+ (that combination fails loudly too — see ` ExternalClientFactory::assertAuthConfigurationIsConsistent() ` ).
83+ The client secret and header value are never logged, never displayed, and never appear in any error
84+ panel — verified end-to-end in ` demo/tests/Controller/SdcOAuthSecretLeakageTest.php ` . The ` /sdc ` status
85+ badge shows * which* mechanism is active (e.g. "configured (OAuth)") but never the credential itself.
86+
87+ ### Session-scoped credential entry (M07)
88+
89+ ` /sdc ` has a small form (shown only when the operator has enabled OAuth or a manual header — i.e. when
90+ ` FHIR_SERVER_OAUTH_TOKEN_URL ` or ` FHIR_SERVER_AUTH_HEADER_NAME ` is set) letting a visitor enter their own
91+ OAuth client id/secret, or their own header value, for ** their session only** — never persisted to disk,
92+ never logged, never echoed back into the form. This lets someone test the demo as different users
93+ without editing env vars and restarting the server. ` ExternalClientFactory ` checks the current visitor's
94+ session first (` sdc_oauth_client_id ` /` sdc_oauth_client_secret ` /` sdc_auth_header_value ` ) and falls back to
95+ the env-var value when no session override is present — see `App\Controller\SdcController::
96+ setCredentials()` / ` clearCredentials()` and ` ExternalClientFactory`'s class docblock.
97+
98+ ** Accepted risk, deliberately chosen — not a hardening recommendation.** The destination FHIR server and
99+ OAuth token URL/IdP stay operator-configured; only the credential * values* are visitor-enterable. This
100+ still means ** any visitor who can reach ` /sdc ` can authenticate as whoever they claim, against whatever
101+ server the operator has configured** — there is no verification that a submitted client id/secret
102+ "belongs" to the person submitting it. This tradeoff was raised explicitly (against a safer
103+ "operator-pre-configured named profiles" alternative) and the free-text session-entry approach was
104+ chosen anyway, on the basis that this is suited to a local/trusted-access demo. ** Do not expose ` /sdc `
105+ on a network you don't trust without adding real access control first** (see ` backlog.md ` ).
106+
107+ Session isolation between visitors, and that a submitted secret never leaks into any rendered page or
108+ error, are both covered by ` demo/tests/Controller/SdcSessionCredentialIsolationTest.php ` and
109+ ` SdcSessionCredentialLeakageTest.php ` .
110+
33111## Useful Commands
34112
35113``` bash
0 commit comments