diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 2ed8ce3c..d61f52f0 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -1,5 +1,12 @@ = Changelog `vzd-cli` +== Version 3.0.0 + +- Unterstützung FHIR VZD FdV und Search Schnittstellen, s. `vzd-cli fhir` +- Neues Befehl für übergreifenden login: `vzd-cli login` +- Tägliche Überprüfung der Updates ist jetzt abschaltbar `vzd-cli config set updates.enabled true|false` +- Diverse Bugfixes + == Version 2.6.2 - TelematikID ist jetzt optional in `vzd-cli admin log` diff --git a/Makefile b/Makefile index 20a8e1ad..0103ee0a 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ ktlint: dist: ktlint ./gradlew clean build -x test - java -jar ./vzd-cli/build/libs/vzd-cli-all.jar admin tu login + java -jar ./vzd-cli/build/libs/vzd-cli-all.jar login tu ./run_tests.sh release: diff --git a/README.adoc b/README.adoc index 39f8e49a..d10ed6ab 100644 --- a/README.adoc +++ b/README.adoc @@ -31,7 +31,9 @@ Options: Commands: config Manage configuration update Updates this software + login Logins into all configured APIs admin CLI for DirectoryAdministration API + fhir CLI for FHIR Directory apo CLI for ApoVZD API gui Starts HTTP Server with GUI pers Process gematik SMC-B/HBA Exports @@ -68,6 +70,8 @@ vzd-cli config set httpProxy.enabled true * Directory Admin API * Apo API (ApoVZD) +* FHIR FDV Search API +* FHIR Search API (nur mit einem speziellen Token, s. https://directory-beta.ccs.gematik.solutions) Für die Nutzung dieser APIs sind jeweils entsprechende Geheimnisse bzw. Credentials erforderlich. Diese werden von der gematik *den bereichtigten Organistionen* bereitgestellt. @@ -113,6 +117,22 @@ vzd-cli apo config set apiKeys.test vzd-cli apo config set apiKeys.prod ---- + +=== (Neu) FHIR FDV Search API Credentials + +Zugriff auf FHIR FDV Search API wird mittels OAuth2 Client Credentials Flow geschützt. Die berechtige Dienstanbieter erhalten die Client ID und Client Secret von der gematik. + +[source,bash] +---- +# Secret für die Referenzumgebung speichern +# es folgt eine Vault Passwortabfrage für persönliche Vault +vzd-cli fhir fdv-vault store -e ru -c -s + +# Secret für die Produktivumgebung speichern +# es folgt eine Vault Passwortabfrage für persönliche Vault +vzd-cli fhir fdv-vault store -e pu -c -s +---- + == Erste Schritte Befor die Directory Admin API genutzt werden kann, muss eine Anmeldung erfolgen. @@ -122,10 +142,10 @@ Die Anmeldung muss alle 6 Stunden wiederholt werden. ---- # Anmelden in die Referenzumgebung (ru) # es folgt eine Vault-Passwortabfrage -vzd-cli admin ru login +vzd-cli login ru # Anmelden in die Referenzumgebung (pu) # es folgt eine Vault-Passwortabfrage -vzd-cli admin pu login +vzd-cli login pu ---- Für vollautomatisierte Nutzung des `vzd-cli`, auch bei der Anmeldung, wird das setzten der Umgebungsvariable `VAULT_PASSWORD` empfohlen. @@ -156,6 +176,16 @@ vzd-cli admin ru search Müller Berlin vzd-cli admin ru show 1-SMC-B-Testkarte-883110000117729 ---- +.*(Neu) Beispiel:* Suche nach einträgen in der FHIR FDV Search API in der Produktivumgebung (`pu`) +[source,bash] +---- +vzd-cli fhir pu fdv search healthcare-service -t +vzd-cli fhir pu fdv search practitioner-role -t +# oder in Kurzform +vzd-cli fhir pu fdv search hs -t +vzd-cli fhir pu fdv search pr -t +---- + == Übergreifende Befehle === `vzd-cli config` @@ -575,3 +605,44 @@ vzd-cli apo prod show 3-1234567890 == `vzd-cli gui`: Directory GUI Durch den Befehl `vzd-cli gui` wird ein HTTP Server gestartet und ein neuer Browser-Tab mit GUI geöffnet. + +== (Neu) `vzd-cli fhir`: FHIR Directory APIs + +=== `vzd-cli fhir fdv search` + +Suche nach Einträgen in der FHIR FDV Search API. + +.*Beispiel:* Suche nach Einträgen mit TelematikID +[source,bash] +---- +vzd-cli fhir pu fdv search hs -t 1-1234567890 +---- + +=== `vzd-cli fhir fdv-vault` + +Befehle zur Verwaltung von OAuth2 Geheimnissen für die FHIR FDV Search API. Aufbau ist analog zu `vzd-cli admin vault`. + +=== `vzd-cli fhir token` + +Lesen und Setzen eines `ACCESS_TOKEN` für die FHIR Search API. + +[source,bash] +---- +# Setzen des ACCESS_TOKEN für Referenzumgebung (ru) +vzd-cli fhir ru token -s +# Lesen des ACCESS_TOKEN für Referenzumgebung (ru) +vzd-cli fhir ru token +---- + +=== `vzd-cli fhir search` + +Suche nach Einträgen in der FHIR Search API. + +.*Beispiel:* Suche nach Einträgen mit TelematikID +[source,bash] +---- +# Suche nach HealthcareService Einträgen +vzd-cli fhir ru search hs -t 1-2234567890 +# Suche nach PractitionerRole Einträgen +vzd-cli fhir ru search pr -t 1-1234567890 +---- \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 99df926a..0c6039e9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,9 +1,9 @@ plugins { // Support convention plugins written in Kotlin. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build. base - kotlin("jvm") version "1.8.10" apply false + kotlin("jvm") version "1.9.22" apply false `kotlin-dsl` - kotlin("plugin.serialization") version "1.8.10" apply false + kotlin("plugin.serialization") version "1.9.22" apply false id("org.jlleitschuh.gradle.ktlint") version "11.6.0" apply false } @@ -13,5 +13,5 @@ repositories { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") } diff --git a/buildSrc/src/main/kotlin/de.gematik.directory.common-conventions.gradle.kts b/buildSrc/src/main/kotlin/de.gematik.directory.common-conventions.gradle.kts index 1948f102..07674e9e 100644 --- a/buildSrc/src/main/kotlin/de.gematik.directory.common-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/de.gematik.directory.common-conventions.gradle.kts @@ -1,10 +1,3 @@ - -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -tasks.withType { - kotlinOptions.jvmTarget = "11" -} - plugins { id("org.jetbrains.kotlin.jvm") } @@ -20,7 +13,6 @@ dependencies { testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") } - tasks.named("test") { // Use JUnit Platform for unit tests. useJUnitPlatform() diff --git a/directory-lib/build.gradle.kts b/directory-lib/build.gradle.kts index 0d0b85a8..8fba0e19 100644 --- a/directory-lib/build.gradle.kts +++ b/directory-lib/build.gradle.kts @@ -1,7 +1,11 @@ plugins { id("de.gematik.directory.library-conventions") - kotlin("plugin.serialization") version "1.8.20" + kotlin("plugin.serialization") version "1.9.22" +} + +kotlin { + jvmToolchain(11) } java { diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Auth.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/Auth.kt similarity index 97% rename from directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Auth.kt rename to directory-lib/src/main/kotlin/de/gematik/ti/directory/Auth.kt index 1816e9e7..3dfadc48 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Auth.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/Auth.kt @@ -1,5 +1,6 @@ -package de.gematik.ti.directory.admin +package de.gematik.ti.directory +import de.gematik.ti.directory.admin.AdminResponseException import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.* diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/AdminEnvironment.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/DirectoryEnvironment.kt similarity index 53% rename from directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/AdminEnvironment.kt rename to directory-lib/src/main/kotlin/de/gematik/ti/directory/DirectoryEnvironment.kt index e92a1490..b37f965a 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/AdminEnvironment.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/DirectoryEnvironment.kt @@ -1,7 +1,7 @@ -package de.gematik.ti.directory.admin +package de.gematik.ti.directory @Suppress("ktlint:standard:enum-entry-name-case") -enum class AdminEnvironment { +enum class DirectoryEnvironment { ru, tu, pu, diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Client.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Client.kt index ce646a76..85fa02ef 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Client.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Client.kt @@ -1,15 +1,14 @@ package de.gematik.ti.directory.admin +import de.gematik.ti.directory.DirectoryAuthPlugin +import de.gematik.ti.directory.DirectoryAuthPluginConfig import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* -import io.ktor.client.plugins.auth.* -import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* -import io.ktor.client.plugins.observer.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Config.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Config.kt index 3c01dbb6..c505576d 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Config.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Config.kt @@ -1,5 +1,6 @@ package de.gematik.ti.directory.admin +import de.gematik.ti.directory.DirectoryEnvironment import de.gematik.ti.directory.DirectoryException import kotlinx.serialization.Serializable @@ -31,7 +32,7 @@ class ConfigException(message: String, cause: Throwable? = null) : DirectoryExce data class Config( val environments: Map, ) { - fun environment(env: AdminEnvironment) = environments[env.name] ?: throw ConfigException("Unknown environment: ${env.name}") + fun environment(env: DirectoryEnvironment) = environments[env.name] ?: throw ConfigException("Unknown environment: ${env.name}") } @Serializable diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Model.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Model.kt index b14a6f39..e91ceec4 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Model.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/admin/Model.kt @@ -132,6 +132,7 @@ data class FAD1( @SerialName("KOM-LE_Version") var komleVersion: String? = null, var komLeData: List? = null, + var kimData: List? = null, ) @Serializable @@ -213,3 +214,32 @@ data class InnerError( val attributeName: String, val attributeError: String, ) + +/** + * kimData: + * type: array + * items: + * type: object + * properties: + * mail: + * type: string + * description: 'E-Mail-Adresse' + * version: + * type: string + * example: 1.5+ + * description: 'Die höchste Version der KIM Clientmodule für diese KIM-Mail-Adresse' + * appTags: + * type: array + * items: + * type: string + * example: + * - eEB;V1.0 + * - DALE-UV;Einsendung;V1.0 + * description: 'Anwendungskennzeichen, welche diese KIM-Mail-Adresse verarbeiten kann' + */ +@Serializable +data class KIMData( + val mail: String, + val version: String, + val appTags: List? = null, +) diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/DirectoryEntryExtension.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/DirectoryEntryExtension.kt index 891f1b53..019d6786 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/DirectoryEntryExtension.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/DirectoryEntryExtension.kt @@ -100,7 +100,7 @@ fun elaborateSpecialization(specialization: String): Coding { } } else if (OrganisationSpecializationRegex.matches(specialization)) { OrganisationSpecializationRegex.matchEntire(specialization)?.let { - HealthcareServiceSpecialtyVS.resolveCode("urn:oid:${it.groupValues[1]}", it.groupValues[2]) + HealthcareServiceTypeVS.resolveCode("urn:oid:${it.groupValues[1]}", it.groupValues[2]) } } else { null diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/ElaborateModel.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/ElaborateModel.kt index 2248c781..691adbf3 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/ElaborateModel.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/ElaborateModel.kt @@ -81,6 +81,7 @@ data class ElaborateKIMAddress( val mail: String, val version: String, val provider: ElaborateKIMProvider?, + val appTags: List? = null, ) @Serializable diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/KIMExtension.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/KIMExtension.kt index 88e19870..2a5c9ecf 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/KIMExtension.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/elaborate/KIMExtension.kt @@ -12,10 +12,15 @@ fun DirectoryEntry.infereKIMAddresses(): List? { fad1.dn.ou?.first()?.let { fad -> ElaborateKIMProvider(fad, fad) } + val kimAddress = + fad1.kimData?.firstOrNull { kimData -> + kimData.mail == it.mail + } ElaborateKIMAddress( it.mail, it.version, provider, + kimAddress?.appTags, ) } ?: emptyList() } diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/BundleExtension.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/BundleExtension.kt new file mode 100644 index 00000000..8eb564fc --- /dev/null +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/BundleExtension.kt @@ -0,0 +1,71 @@ +package de.gematik.ti.directory.fhir + +import org.hl7.fhir.r4.model.* + +fun Bundle.filterByType(resourceType: ResourceType): List { + return entry.filter { it.resource.resourceType == resourceType }.map { it.resource } +} + +fun Bundle.filterPractitionerRoles(): List { + return filterByType(ResourceType.PractitionerRole).map { resource -> + val practitionerRole = resource as PractitionerRole + val practitioner = findResource(practitionerRole.practitioner)?.resource?.let { it as Practitioner } + + FHIRDirectoryEntry( + telematikID = practitioner?.identifier?.firstOrNull { it.system == "https://gematik.de/fhir/sid/telematik-id" }?.value, + displayName = practitioner?.name?.firstOrNull()?.text, + resourceType = ResourceType.PractitionerRole, + practitionerRole = practitionerRole, + practitioner = practitioner, + location = + practitionerRole.location?.mapNotNull { + findResource(it)?.resource + }?.map { + it as Location + }?.ifEmpty { null }, + endpoint = + practitionerRole.endpoint?.mapNotNull { + findResource(it)?.resource + }?.map { + it as Endpoint + }?.ifEmpty { null }, + ) + } +} + +fun Bundle.filterHealthcareServices(): List { + return filterByType(ResourceType.HealthcareService).map { resource -> + val healthcareService = resource as HealthcareService + val organization = findResource(healthcareService.providedBy)?.resource?.let { it as Organization } + FHIRDirectoryEntry( + telematikID = organization?.identifier?.firstOrNull { it.system == "https://gematik.de/fhir/sid/telematik-id" }?.value, + displayName = organization?.name, + resourceType = ResourceType.HealthcareService, + healthcareService = healthcareService, + organization = organization, + location = + healthcareService.location?.mapNotNull { + findResource(it)?.resource + }?.map { + it as Location + }?.ifEmpty { null }, + endpoint = + healthcareService.endpoint?.mapNotNull { + findResource(it)?.resource + }?.map { + it as Endpoint + }?.ifEmpty { null }, + ) + } +} + +fun Bundle.toDirectoryEntries(): List { + return filterPractitionerRoles() + filterHealthcareServices() +} + +fun Bundle.findResource(reference: Reference): Bundle.BundleEntryComponent? { + val id = IdType(reference.reference) + return this.entry.find { + it.resource?.fhirType() == id.resourceType && it.resource?.idElement?.idPart == id.idPart + } +} diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/Client.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/Client.kt new file mode 100644 index 00000000..3a4a8a82 --- /dev/null +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/Client.kt @@ -0,0 +1,250 @@ +package de.gematik.ti.directory.fhir + +import ca.uhn.fhir.context.FhirContext +import de.gematik.ti.directory.DirectoryAuthPlugin +import de.gematik.ti.directory.DirectoryAuthPluginConfig +import de.gematik.ti.directory.DirectoryEnvironment +import de.gematik.ti.directory.DirectoryException +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import mu.KotlinLogging +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.OperationOutcome +import org.hl7.fhir.r4.model.ResourceType + +val FHIR_R4 = FhirContext.forR4() + +private val JSON = + Json { + ignoreUnknownKeys = true + prettyPrint = true + } + +val DefaultConfig = + Config( + environments = + mapOf( + "tu" to + EnvironmentConfig( + search = + SearchConfig( + apiURL = "https://fhir-directory-test.vzd.ti-dienste.de/search", + ), + fdv = + FdvConfig( + apiURL = "https://fhir-directory-test.vzd.ti-dienste.de/fdv/search", + authenticationEndpoint = "https://auth-test.vzd.ti-dienste.de:9443/auth/realms/Service-Authenticate/protocol/openid-connect/token", + authorizationEndpoint = "https://fhir-directory-test.vzd.ti-dienste.de/service-authenticate", + ), + ), + "ru" to + EnvironmentConfig( + search = + SearchConfig( + apiURL = "https://fhir-directory-ref.vzd.ti-dienste.de/search", + ), + fdv = + FdvConfig( + apiURL = "https://fhir-directory-ref.vzd.ti-dienste.de/fdv/search", + authenticationEndpoint = "https://auth-ref.vzd.ti-dienste.de:9443/auth/realms/Service-Authenticate/protocol/openid-connect/token", + authorizationEndpoint = "https://fhir-directory-ref.vzd.ti-dienste.de/service-authenticate", + ), + ), + "pu" to + EnvironmentConfig( + search = + SearchConfig( + apiURL = "https://fhir-directory.vzd.ti-dienste.de/search", + ), + fdv = + FdvConfig( + apiURL = "https://fhir-directory.vzd.ti-dienste.de/fdv/search", + authenticationEndpoint = "https://auth.vzd.ti-dienste.de:9443/auth/realms/Service-Authenticate/protocol/openid-connect/token", + authorizationEndpoint = "https://fhir-directory.vzd.ti-dienste.de/service-authenticate", + ), + ), + ), + ) + +class ConfigException(message: String, cause: Throwable? = null) : DirectoryException(message, cause) + +@Serializable +data class Config( + val environments: Map, +) { + fun environment(env: DirectoryEnvironment) = environments[env.name] ?: throw ConfigException("Unknown environment: ${env.name}") +} + +@Serializable +data class EnvironmentConfig( + val search: SearchConfig, + val fdv: FdvConfig, +) + +@Serializable +data class SearchConfig( + val apiURL: String, +) + +@Serializable +data class FdvConfig( + val apiURL: String, + val authenticationEndpoint: String, + val authorizationEndpoint: String, +) + +enum class SearchResource(val resourceType: ResourceType) { + PractitionerRole(ResourceType.PractitionerRole), + HealthcareService(ResourceType.HealthcareService), +} + +class SearchQuery(val resource: SearchResource, val params: MutableMap> = mutableMapOf()) { + fun addParam( + key: String, + value: String + ) { + if (params.containsKey(key)) { + params[key] = params[key]!!.plus(value) + } else { + params[key] = listOf(value) + } + } +} + +class Client(block: Configurator.() -> Unit = {}) { + private val configurator: Configurator = Configurator() + private val envConfig: EnvironmentConfig + private val httpClientFdv: HttpClient get() { + val httpClient by lazy { createHttpClient(envConfig.fdv.apiURL, configurator.authFdvBlock) } + return httpClient + } + private val httpClientSearch: HttpClient get() { + val httpClient by lazy { createHttpClient(envConfig.search.apiURL, configurator.authSearchBlock) } + return httpClient + } + val logger = KotlinLogging.logger {} + + class Configurator { + var envConfig: EnvironmentConfig? = null + var httpProxyURL: String? = null + internal var authFdvBlock: DirectoryAuthPluginConfig.() -> Unit = {} + internal var authSearchBlock: DirectoryAuthPluginConfig.() -> Unit = {} + + fun authFdv(block: DirectoryAuthPluginConfig.() -> Unit) { + authFdvBlock = block + } + + fun authSearch(block: DirectoryAuthPluginConfig.() -> Unit) { + authSearchBlock = block + } + } + + fun createHttpClient( + defaultURL: String, + authBlock: DirectoryAuthPluginConfig.() -> Unit + ): HttpClient { + return HttpClient(CIO) { + engine { + configurator.httpProxyURL?.let { + logger.debug { "Using proxy: $it" } + proxy = ProxyBuilder.http(it) + } + } + + expectSuccess = false + + val l = logger + + install(HttpTimeout) { + requestTimeoutMillis = 1000 * 60 * 60 + } + + install(Logging) { + logger = Logger.DEFAULT + if (l.isDebugEnabled) { + level = LogLevel.ALL + } else if (l.isInfoEnabled) { + level = LogLevel.INFO + } + } + + install(DirectoryAuthPlugin) { + authBlock(this) + } + + install(ContentNegotiation) { + json(JSON) + } + defaultRequest { + url(defaultURL) + } + } + } + + init { + block(configurator) + this.envConfig = configurator.envConfig ?: throw ConfigException("No environment configured") + } + + suspend fun search(query: SearchQuery): Bundle { + logger.debug { "Searching ${query.resource.name} with query: ${query.params}" } + val response = + httpClientSearch.get("/search/${query.resource.name}") { + query.params.forEach { (key, values) -> + values.forEach { value -> + parameter(key, value) + } + } + } + return handleSearchResponse(response) + } + + private suspend fun handleSearchResponse(response: HttpResponse): Bundle { + val body = response.body() + val parser = FHIR_R4.newJsonParser() + + if (response.status != HttpStatusCode.OK) { + var exc: DirectoryException? + + try { + val outcome = parser.parseResource(OperationOutcome::class.java, body) + exc = DirectoryException(outcome.issue.joinToString { it.diagnostics }) + } catch (e: Exception) { + if (response.status == HttpStatusCode.Unauthorized) { + exc = DirectoryException("Unauthorized. Please use `vzd-cli login` first.") + } else { + exc = DirectoryException("Search failed: ${response.status} $body") + } + } + + throw exc!! + } + val bundle = parser.parseResource(Bundle::class.java, body) + logger.debug { "Got search response bundle with ${bundle.total} resources." } + return bundle + } + + suspend fun searchFdv(query: SearchQuery): Bundle { + logger.debug { "Searching ${query.resource.name} with query: ${query.params}" } + val response = + httpClientFdv.get("/fdv/search/${query.resource.name}") { + query.params.forEach { (key, values) -> + values.forEach { value -> + parameter(key, value) + } + } + } + return handleSearchResponse(response) + } +} diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/FHIRDirectoryEntry.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/FHIRDirectoryEntry.kt new file mode 100644 index 00000000..6d4600cf --- /dev/null +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/FHIRDirectoryEntry.kt @@ -0,0 +1,21 @@ +package de.gematik.ti.directory.fhir + +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import org.hl7.fhir.r4.model.* + +/** + * Combined entry for all resource types + */ +@Serializable +data class FHIRDirectoryEntry( + val telematikID: String?, + val displayName: String?, + val resourceType: ResourceType, + val organization: @Contextual Organization? = null, + val healthcareService: @Contextual HealthcareService? = null, + val practitioner: @Contextual Practitioner? = null, + val practitionerRole: @Contextual PractitionerRole? = null, + val location: List<@Contextual Location>? = null, + val endpoint: List<@Contextual Endpoint>? = null, +) diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/SerializerModule.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/SerializerModule.kt new file mode 100644 index 00000000..da2329c5 --- /dev/null +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/SerializerModule.kt @@ -0,0 +1,47 @@ +package de.gematik.ti.directory.fhir + +import ca.uhn.fhir.context.FhirContext +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.modules.SerializersModule +import org.hl7.fhir.r4.model.* + +val FHIRSerializerModule = + SerializersModule { + contextual(Organization::class, ResourceSerializer()) + contextual(HealthcareService::class, ResourceSerializer()) + contextual(Practitioner::class, ResourceSerializer()) + contextual(PractitionerRole::class, ResourceSerializer()) + contextual(Location::class, ResourceSerializer()) + contextual(Endpoint::class, ResourceSerializer()) + } + +val FhirContextR4 = FhirContext.forR4() + +/** + * Generic Serializer für FHIR Resource + */ +class ResourceSerializer : KSerializer where R : BaseResource { + override val descriptor: SerialDescriptor = JsonObject.serializer().descriptor + + override fun serialize( + encoder: Encoder, + value: R, + ) { + // first serialize the resource inti json string + val parser = FhirContextR4.newJsonParser() + val json = parser.encodeResourceToString(value) + // now parse the json string into a JsonObject + val surrogate: JsonObject = Json.decodeFromString(json) + // now serialize the JsonObject using kotlinx.serialization + encoder.encodeSerializableValue(JsonObject.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): R { + throw NotImplementedError() + } +} diff --git a/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/SimpleValueSet.kt b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/SimpleValueSet.kt index 1faa81cc..b8201f65 100644 --- a/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/SimpleValueSet.kt +++ b/directory-lib/src/main/kotlin/de/gematik/ti/directory/fhir/SimpleValueSet.kt @@ -33,5 +33,5 @@ private fun loadSimpleValueSet(name: String): SimpleValueSet { return json.decodeFromString(SimpleValueSet::class.java.getResource("/de.gematik.fhir.directory/ValueSet-$name.json")!!.readText()) } -val HealthcareServiceSpecialtyVS = loadSimpleValueSet("HealthcareServiceSpecialtyVS") +val HealthcareServiceTypeVS = loadSimpleValueSet("HealthcareServiceTypeVS") val PractitionerQualificationVS = loadSimpleValueSet("PractitionerQualificationVS") diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointDirectoryConnectionType.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointDirectoryConnectionType.json index ae0bf53d..84b3aa03 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointDirectoryConnectionType.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointDirectoryConnectionType.json @@ -1,12 +1,11 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", "name": "EndpointDirectoryConnectionType", "id": "EndpointDirectoryConnectionType", "title": "Codes for Endpoint.connectionType", "description": "CodeSystem TI specific connection types assigned to the Endpoints", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/CodeSystem/EndpointDirectoryConnectionType", "concept": [ { @@ -14,8 +13,12 @@ "display": "TI-Messenger Endpoint" }, { - "code": "tim-domain", - "display": "TI-Messenger domain name" + "code": "tim-fa", + "display": "TI-Messenger Funktionsaccount" + }, + { + "code": "tim-bot", + "display": "TI-Messenger Chatbot" }, { "code": "kim-1.0", @@ -36,9 +39,14 @@ { "code": "erp-supported", "display": "eRP Endpoint" + }, + { + "code": "ident-eingangsabruf", + "display": "Ident Eingangsdatenabruf" } ], "publisher": "gematik GmbH", + "version": "0.11.2", "caseSensitive": false, - "count": 7 + "count": 9 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointDirectoryPayloadType.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointDirectoryPayloadType.json index 4bf99c33..8cc12c28 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointDirectoryPayloadType.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointDirectoryPayloadType.json @@ -1,12 +1,11 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", "name": "EndpointDirectoryPayloadType", "id": "EndpointDirectoryPayloadType", "title": "Codes for Endpoint.payloadType", "description": "CodeSystem TI specific payload types assigned to the Endpoints\n\nCodes are maintained by gematik.\nThe codes are used to declare which processes are supported by an entity with the corresponding entry in the gematik Directory.\nNew codes can be requested at gematik. There must exist a specification for each code so that developers can find out how to implement the process.", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/CodeSystem/EndpointDirectoryPayloadType", "concept": [ { @@ -15,6 +14,7 @@ } ], "publisher": "gematik GmbH", + "version": "0.11.2", "caseSensitive": false, "count": 1 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointVisibilityCS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointVisibilityCS.json new file mode 100644 index 00000000..0b14fdea --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-EndpointVisibilityCS.json @@ -0,0 +1,19 @@ +{ + "resourceType": "CodeSystem", + "status": "active", + "content": "complete", + "name": "EndpointVisibilityCS", + "id": "EndpointVisibilityCS", + "title": "EndpointVisibilityCS", + "description": "EndpointVisibilityCS", + "url": "https://gematik.de/fhir/directory/CodeSystem/EndpointVisibilityCS", + "concept": [ + { + "code": "hide-versicherte", + "display": "Eintrag nicht Versicherten anzeigen" + } + ], + "publisher": "gematik GmbH", + "version": "0.11.2", + "count": 1 +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-HolderCS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-HolderCS.json index 20f772b2..683160d6 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-HolderCS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-HolderCS.json @@ -1,12 +1,11 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", "name": "HolderCS", "id": "HolderCS", "title": "Codes for identity authorities (Holder)", "description": "Code System for identity authories in the TI, which verify and control the identities of practitioners and organisations", - "version": "0.9.1", "url": "https://gematik.de/fhir/directory/CodeSystem/HolderCS", "concept": [ { @@ -475,6 +474,7 @@ } ], "publisher": "gematik GmbH", + "version": "0.11.2", "caseSensitive": false, "count": 116 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationProfessionOID.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationProfessionOID.json index c0944839..9fd3de15 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationProfessionOID.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationProfessionOID.json @@ -1,12 +1,11 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", "name": "OrganizationProfessionOID", "id": "OrganizationProfessionOID", "title": "CodeSystem for ProfessionOID of Institutions", "description": "The codes for Organizations based on Profession OIDs defined in [gemSpec_OID](https://fachportal.gematik.de/fachportal-import/files/gemSpec_OID_V3.10.0.pdf)", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/CodeSystem/OrganizationProfessionOID", "concept": [ { @@ -221,6 +220,10 @@ "code": "1.2.276.0.76.4.281", "display": "Ernährungstherapeutische Praxis" }, + { + "code": "1.2.276.0.76.4.282", + "display": "DIGA-Hersteller und Anbieter" + }, { "code": "1.2.276.0.76.4.284", "display": "Betriebsstätte Weitere Kostenträger im Gesundheitswesen" @@ -232,9 +235,30 @@ { "code": "1.2.276.0.76.4.286", "display": "KIM-Hersteller und -Anbieter" + }, + { + "code": "1.2.276.0.76.4.292", + "display": "NCPeH Fachdienst" + }, + { + "code": "1.2.276.0.76.4.295", + "display": "TIM-Hersteller und -Anbieter" + }, + { + "code": "1.2.276.0.76.4.303", + "display": "Ombudsstelle eines Kostenträgers" + }, + { + "code": "1.2.276.0.76.4.304", + "display": "Betriebsstätte Augenoptiker und Hörakustiker" + }, + { + "code": "1.2.276.0.76.4.306", + "display": "Betriebsstätte Orthopädieschuhmacher und Orthopädietechniker" } ], "publisher": "gematik GmbH", + "version": "0.11.2", "caseSensitive": false, - "count": 56 + "count": 62 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationProviderType.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationProviderType.json index 1be543cb..e9b49806 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationProviderType.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationProviderType.json @@ -1,11 +1,10 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", "name": "OrganizationProviderType", "id": "OrganizationProviderType", "description": "CodeSystem of TI Service Provider types as to be found in the gematik Directory", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/CodeSystem/OrganizationProviderType", "concept": [ { @@ -26,6 +25,7 @@ } ], "publisher": "gematik GmbH", + "version": "0.11.2", "caseSensitive": false, "count": 4 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationVisibilityCS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationVisibilityCS.json new file mode 100644 index 00000000..74b7e7e6 --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-OrganizationVisibilityCS.json @@ -0,0 +1,19 @@ +{ + "resourceType": "CodeSystem", + "status": "active", + "content": "complete", + "name": "OrganizationVisibilityCS", + "id": "OrganizationVisibilityCS", + "title": "OrganizationVisibilityCS", + "description": "OrganizationVisibilityCS", + "url": "https://gematik.de/fhir/directory/CodeSystem/OrganizationVisibilityCS", + "concept": [ + { + "code": "hide-erezeptApp", + "display": "Eintrag nicht in eRezeptApp darstellen" + } + ], + "publisher": "gematik GmbH", + "version": "0.11.2", + "count": 1 +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-Origin.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-Origin.json new file mode 100644 index 00000000..0d7bf00c --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-Origin.json @@ -0,0 +1,23 @@ +{ + "resourceType": "CodeSystem", + "status": "active", + "content": "complete", + "name": "Origin", + "id": "Origin", + "description": "CodeSystem which identifies the origin of a resource in the FHIR Directory", + "url": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "concept": [ + { + "code": "ldap", + "display": "Synchronized from LDAP Directory" + }, + { + "code": "owner", + "display": "Provided by Identity Owner" + } + ], + "publisher": "gematik GmbH", + "version": "0.11.2", + "caseSensitive": false, + "count": 2 +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyHealthcareServiceTypeCS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyHealthcareSpecialityCS.json similarity index 68% rename from directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyHealthcareServiceTypeCS.json rename to directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyHealthcareSpecialityCS.json index 000d5dcc..1dc6f79d 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyHealthcareServiceTypeCS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyHealthcareSpecialityCS.json @@ -1,13 +1,12 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", - "name": "PharmacyHealthcareServiceTypeCS", - "id": "PharmacyHealthcareServiceTypeCS", - "title": "HealthcareService Type", - "description": "Type codes of HealthcareServices", - "version": "1.0", - "url": "https://gematik.de/fhir/directory/CodeSystem/PharmacyHealthcareServiceTypeCS", + "name": "PharmacyHealthcareSpecialityCS", + "id": "PharmacyHealthcareSpecialityCS", + "title": "HealthcareService Speciality", + "description": "Speciality codes of HealthcareServices", + "url": "https://gematik.de/fhir/directory/CodeSystem/PharmacyHealthcareSpecialityCS", "concept": [ { "code": "10", @@ -31,6 +30,7 @@ } ], "publisher": "gematik GmbH", + "version": "0.11.2", "caseSensitive": false, "count": 5 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyTypeCS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyTypeCS.json index 05595d4e..a4d750a7 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyTypeCS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PharmacyTypeCS.json @@ -1,32 +1,28 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", "name": "PharmacyTypeCS", "id": "PharmacyTypeCS", "title": "Pharmacy type", "description": "Pharmacy type codes of accessability for patients", - "version": "1.0", "url": "https://gematik.de/fhir/directory/CodeSystem/PharmacyTypeCS", "concept": [ { - "code": "10", + "code": "offizin-apotheke", "display": "Offizin-Apotheke" }, { - "code": "20", + "code": "krankenhausversorgende-apotheke", "display": "Krankenhausversorgende Apotheke" }, { - "code": "30", - "display": "Heimversorgende Apotheke" - }, - { - "code": "40", - "display": "Versandapotheke" + "code": "bundeswehrapotheke", + "display": "Bundeswehrapotheke" } ], "publisher": "gematik GmbH", + "version": "1.0", "caseSensitive": true, - "count": 4 + "count": 3 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PractitionerProfessionOID.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PractitionerProfessionOID.json index 14d1aea4..53c5a554 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PractitionerProfessionOID.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-PractitionerProfessionOID.json @@ -1,12 +1,11 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", "name": "PractitionerProfessionOID", "id": "PractitionerProfessionOID", - "title": "CodeSystem for Practitioner's ProfessioiOID", + "title": "CodeSystem for Practitioner's ProfessionOID", "description": "The codes for Practitioners based on Profession OIDs defined in [gemSpec_OID](https://fachportal.gematik.de/fachportal-import/files/gemSpec_OID_V3.10.0.pdf)", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/CodeSystem/PractitionerProfessionOID", "concept": [ { @@ -149,9 +148,18 @@ { "code": "1.2.276.0.76.4.277", "display": "Ernährungstherapeut/-in" + }, + { + "code": "1.2.276.0.76.4.305", + "display": "Orthopädieschuhmacher/-in und Orthopädietechniker/-in" + }, + { + "code": "1.2.276.0.76.4.308", + "display": "Augenoptiker/-in, Optometrist/-in und Hörakustiker/-in" } ], "publisher": "gematik GmbH", + "version": "0.11.2", "caseSensitive": false, - "count": 35 + "count": 37 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-Region.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-Region.json index fe03bd80..553f3f78 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-Region.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/CodeSystem-Region.json @@ -1,12 +1,11 @@ { "resourceType": "CodeSystem", - "status": "draft", + "status": "active", "content": "complete", "name": "Region", "id": "Region", "title": "Codes for regions in german healthcare system.", "description": "Additionally to german Bundeslander there are 2 sub-provinces \n`Nordrhein` and `Westfalen-Lippe`.", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/CodeSystem/Region", "concept": [ { @@ -83,6 +82,7 @@ } ], "publisher": "gematik GmbH", + "version": "0.11.2", "caseSensitive": false, "count": 18 } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/Endpoint-EndpointExample.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/Endpoint-EndpointExample.json index 63309699..39e3512c 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/Endpoint-EndpointExample.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/Endpoint-EndpointExample.json @@ -2,10 +2,25 @@ "resourceType": "Endpoint", "id": "EndpointExample", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "owner" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/EndpointDirectory" ] }, + "extension": [ + { + "url": "https://gematik.de/fhir/directory/StructureDefinition/EndpointVisibility", + "valueCoding": { + "code": "hide-versicherte", + "system": "https://gematik.de/fhir/directory/CodeSystem/EndpointVisibilityCS" + } + } + ], "status": "active", "connectionType": { "code": "tim", diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/Endpoint-OrganizationExample001-Endpoint-TIM.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/Endpoint-OrganizationExample001-Endpoint-TIM.json index 3145b5c1..38089b45 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/Endpoint-OrganizationExample001-Endpoint-TIM.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/Endpoint-OrganizationExample001-Endpoint-TIM.json @@ -2,6 +2,12 @@ "resourceType": "Endpoint", "id": "OrganizationExample001-Endpoint-TIM", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "owner" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/EndpointDirectory" ] diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/HealthcareService-HealthcareServiceExample.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/HealthcareService-HealthcareServiceExample.json index 819a88b5..9b7e9e6d 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/HealthcareService-HealthcareServiceExample.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/HealthcareService-HealthcareServiceExample.json @@ -2,6 +2,12 @@ "resourceType": "HealthcareService", "id": "HealthcareServiceExample", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "ldap" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/HealthcareServiceDirectory" ] @@ -9,6 +15,48 @@ "providedBy": { "reference": "Organization/OrganizationExample" }, + "category": [ + { + "coding": [ + { + "code": "PRA", + "system": "http://ihe-d.de/CodeSystems/PatientBezogenenGesundheitsversorgung", + "display": "Arztpraxis" + } + ] + } + ], + "type": [ + { + "coding": [ + { + "code": "MZKH", + "system": "urn:oid:1.3.6.1.4.1.19376.3.276.1.5.4", + "display": "Zahnmedizin" + } + ] + }, + { + "coding": [ + { + "code": "ORAL", + "system": "urn:oid:1.3.6.1.4.1.19376.3.276.1.5.4", + "display": "Oralchirurgie" + } + ] + } + ], + "specialty": [ + { + "coding": [ + { + "code": "92", + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "display": "Paediatric Dentistry" + } + ] + } + ], "location": [ { "reference": "Location/LocationExample" @@ -72,5 +120,16 @@ { "reference": "Endpoint/EndpointExample" } + ], + "characteristic": [ + { + "coding": [ + { + "code": "DELEGATOR", + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "display": "eRX Token Receiver" + } + ] + } ] } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/Location-LocationExample.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/Location-LocationExample.json index 9ddadef4..c58a853b 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/Location-LocationExample.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/Location-LocationExample.json @@ -2,6 +2,12 @@ "resourceType": "Location", "id": "LocationExample", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "ldap" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/LocationDirectory" ] @@ -31,5 +37,8 @@ "closingTime": "18:00:00" } ], - "availabilityExceptions": "An Feiertagen geschlossen" + "availabilityExceptions": "An Feiertagen geschlossen", + "managingOrganization": { + "reference": "Organization/OrganizationExample001" + } } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/Organization-OrganizationExample.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/Organization-OrganizationExample.json index a1bcdda2..1bacb7ff 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/Organization-OrganizationExample.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/Organization-OrganizationExample.json @@ -2,13 +2,19 @@ "resourceType": "Organization", "id": "OrganizationExample", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "ldap" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/OrganizationDirectory" ] }, "identifier": [ { - "system": "http://fhir.de/StructureDefinition/identifier-telematik-id", + "system": "https://gematik.de/fhir/sid/telematik-id", "value": "2-2.58.00000040" } ], diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/Organization-OrganizationExample001.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/Organization-OrganizationExample001.json index 8189e883..511d95f5 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/Organization-OrganizationExample001.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/Organization-OrganizationExample001.json @@ -2,6 +2,12 @@ "resourceType": "Organization", "id": "OrganizationExample001", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "ldap" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/OrganizationDirectory" ] @@ -12,6 +18,16 @@ "value": "9-2.58.00000040" } ], + "extension": [ + { + "url": "https://gematik.de/fhir/directory/StructureDefinition/OrganizationVisibility", + "valueCoding": { + "code": "hide-erezeptApp", + "system": "https://gematik.de/fhir/directory/CodeSystem/OrganizationVisibilityCS" + } + } + ], + "active": true, "type": [ { "coding": [ @@ -23,7 +39,6 @@ } ], "name": "gematik GmbH", - "active": true, "alias": [ "gematik" ], diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/Practitioner-TIPractitionerExample001.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/Practitioner-TIPractitionerExample001.json index 384f869c..e3483b91 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/Practitioner-TIPractitionerExample001.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/Practitioner-TIPractitionerExample001.json @@ -2,6 +2,12 @@ "resourceType": "Practitioner", "id": "TIPractitionerExample001", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "ldap" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/PractitionerDirectory" ] @@ -16,6 +22,7 @@ "value": "123456789" } ], + "active": true, "qualification": [ { "code": { diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/Practitioner-TIPractitionerExampleDentist.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/Practitioner-TIPractitionerExampleDentist.json index eef6e7a0..4df701ff 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/Practitioner-TIPractitionerExampleDentist.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/Practitioner-TIPractitionerExampleDentist.json @@ -2,6 +2,12 @@ "resourceType": "Practitioner", "id": "TIPractitionerExampleDentist", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "ldap" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/PractitionerDirectory" ] diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/PractitionerRole-PractitionerRoleExample.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/PractitionerRole-PractitionerRoleExample.json index 13f2dc76..6e026f49 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/PractitionerRole-PractitionerRoleExample.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/PractitionerRole-PractitionerRoleExample.json @@ -2,6 +2,12 @@ "resourceType": "PractitionerRole", "id": "PractitionerRoleExample", "meta": { + "tag": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin", + "code": "ldap" + } + ], "profile": [ "https://gematik.de/fhir/directory/StructureDefinition/PractitionerRoleDirectory" ] diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-EndpointDirectory.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-EndpointDirectory.json index 455a0542..d5ab4106 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-EndpointDirectory.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-EndpointDirectory.json @@ -1,36 +1,14 @@ { "resourceType": "StructureDefinition", "id": "EndpointDirectory", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-category", - "valueString": "Base.Entities" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category", - "valueCode": "business" - } - ], "url": "https://gematik.de/fhir/directory/StructureDefinition/EndpointDirectory", - "version": "0.9.0", + "version": "0.11.2", "name": "EndpointDirectory", "title": "Endpoint in gematik Directory", - "status": "draft", + "status": "active", "publisher": "gematik GmbH", "description": "Endpoints for applications in the gematik Directory", "fhirVersion": "4.0.1", - "mapping": [ - { - "identity": "rim", - "uri": "http://hl7.org/v3", - "name": "RIM Mapping" - }, - { - "identity": "w5", - "uri": "http://hl7.org/fhir/fivews", - "name": "FiveWs Pattern Mapping" - } - ], "kind": "resource", "abstract": false, "type": "Endpoint", @@ -38,6 +16,60 @@ "derivation": "constraint", "differential": { "element": [ + { + "id": "Endpoint.id", + "path": "Endpoint.id", + "mustSupport": true + }, + { + "id": "Endpoint.meta.tag", + "path": "Endpoint.meta.tag", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "system" + } + ], + "rules": "open" + }, + "min": 1, + "mustSupport": true + }, + { + "id": "Endpoint.meta.tag:Origin", + "path": "Endpoint.meta.tag", + "sliceName": "Origin", + "min": 1, + "max": "1", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/OriginVS" + } + }, + { + "id": "Endpoint.meta.tag:Origin.system", + "path": "Endpoint.meta.tag.system", + "min": 1, + "patternUri": "https://gematik.de/fhir/directory/CodeSystem/Origin" + }, + { + "id": "Endpoint.extension:endpointVisibility", + "path": "Endpoint.extension", + "sliceName": "endpointVisibility", + "min": 0, + "max": "*", + "type": [ + { + "code": "Extension", + "profile": [ + "https://gematik.de/fhir/directory/StructureDefinition/EndpointVisibility" + ] + } + ], + "mustSupport": true + }, { "id": "Endpoint.status", "path": "Endpoint.status", @@ -48,9 +80,15 @@ "path": "Endpoint.connectionType", "mustSupport": true }, + { + "id": "Endpoint.connectionType.system", + "path": "Endpoint.connectionType.system", + "mustSupport": true + }, { "id": "Endpoint.connectionType.code", "path": "Endpoint.connectionType.code", + "mustSupport": true, "binding": { "strength": "extensible", "valueSet": "https://gematik.de/fhir/directory/ValueSet/EndpointConnectionTypeVS" diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-EndpointVisibility.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-EndpointVisibility.json new file mode 100644 index 00000000..080f2786 --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-EndpointVisibility.json @@ -0,0 +1,57 @@ +{ + "resourceType": "StructureDefinition", + "id": "EndpointVisibility", + "url": "https://gematik.de/fhir/directory/StructureDefinition/EndpointVisibility", + "version": "0.11.2", + "name": "EndpointVisibility", + "title": "EndpointVisibility", + "status": "active", + "publisher": "gematik GmbH", + "description": "Visibility of an Endpoint in the FHIR-VZD. This Extensions includes codes of use-cases im which this Endpoint SHALL not be visible.", + "fhirVersion": "4.0.1", + "kind": "complex-type", + "abstract": false, + "context": [ + { + "expression": "Endpoint", + "type": "element" + } + ], + "type": "Extension", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension", + "derivation": "constraint", + "differential": { + "element": [ + { + "id": "Extension", + "path": "Extension", + "short": "EndpointVisibility", + "definition": "Visibility of an Endpoint in the FHIR-VZD. This Extensions includes codes of use-cases im which this Endpoint SHALL not be visible." + }, + { + "id": "Extension.extension", + "path": "Extension.extension", + "max": "0" + }, + { + "id": "Extension.url", + "path": "Extension.url", + "fixedUri": "https://gematik.de/fhir/directory/StructureDefinition/EndpointVisibility" + }, + { + "id": "Extension.value[x]", + "path": "Extension.value[x]", + "type": [ + { + "code": "Coding" + } + ], + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/EndpointVisibilityVS" + } + } + ] + } +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-HealthcareServiceDirectory.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-HealthcareServiceDirectory.json index 7fa09524..cb1bea71 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-HealthcareServiceDirectory.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-HealthcareServiceDirectory.json @@ -1,36 +1,14 @@ { "resourceType": "StructureDefinition", "id": "HealthcareServiceDirectory", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-category", - "valueString": "Base.Entities" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category", - "valueCode": "business" - } - ], "url": "https://gematik.de/fhir/directory/StructureDefinition/HealthcareServiceDirectory", - "version": "0.9.0", + "version": "0.11.2", "name": "HealthcareServiceDirectory", "title": "HealthcareService in gematik Directory", - "status": "draft", + "status": "active", "publisher": "gematik GmbH", "description": "Defines the data structure for medical, regulatory and technical\norganisations specific for german Healthcare and Telematics Infrastructure.", "fhirVersion": "4.0.1", - "mapping": [ - { - "identity": "rim", - "uri": "http://hl7.org/v3", - "name": "RIM Mapping" - }, - { - "identity": "w5", - "uri": "http://hl7.org/fhir/fivews", - "name": "FiveWs Pattern Mapping" - } - ], "kind": "resource", "abstract": false, "type": "HealthcareService", @@ -43,19 +21,70 @@ "path": "HealthcareService.id", "mustSupport": true }, + { + "id": "HealthcareService.meta.tag", + "path": "HealthcareService.meta.tag", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "system" + } + ], + "rules": "open" + }, + "min": 1, + "mustSupport": true + }, + { + "id": "HealthcareService.meta.tag:Origin", + "path": "HealthcareService.meta.tag", + "sliceName": "Origin", + "min": 1, + "max": "1", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/OriginVS" + } + }, + { + "id": "HealthcareService.meta.tag:Origin.system", + "path": "HealthcareService.meta.tag.system", + "min": 1, + "patternUri": "https://gematik.de/fhir/directory/CodeSystem/Origin" + }, { "id": "HealthcareService.providedBy", "path": "HealthcareService.providedBy", "min": 1, "mustSupport": true }, + { + "id": "HealthcareService.category", + "path": "HealthcareService.category", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "http://ihe-d.de/ValueSets/IHEXDShealthcareFacilityTypeCodePatientRelatedHealthcare" + } + }, + { + "id": "HealthcareService.type", + "path": "HealthcareService.type", + "mustSupport": true, + "binding": { + "strength": "extensible", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/HealthcareServiceTypeVS" + } + }, { "id": "HealthcareService.specialty", "path": "HealthcareService.specialty", "mustSupport": true, "binding": { - "strength": "extensible", - "valueSet": "https://gematik.de/fhir/directory/ValueSet/HealthcareServiceSpecialtyVS" + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/HealthcareSpecialityTypeVS" } }, { @@ -93,6 +122,15 @@ "path": "HealthcareService.serviceProvisionCode", "mustSupport": true }, + { + "id": "HealthcareService.characteristic", + "path": "HealthcareService.characteristic", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/healthcareservice-characteristic-vs" + } + }, { "id": "HealthcareService.communication", "path": "HealthcareService.communication", diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-LocationDirectory.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-LocationDirectory.json index 607c1857..6fc42a28 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-LocationDirectory.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-LocationDirectory.json @@ -1,36 +1,14 @@ { "resourceType": "StructureDefinition", "id": "LocationDirectory", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-category", - "valueString": "Base.Entities" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category", - "valueCode": "business" - } - ], "url": "https://gematik.de/fhir/directory/StructureDefinition/LocationDirectory", - "version": "0.9.0", + "version": "0.11.2", "name": "LocationDirectory", "title": "Location in gematik Directory", - "status": "draft", + "status": "active", "publisher": "gematik GmbH", "description": "Defines the data structure for medical, regulatory and technical\norganisations specific for german Healthcare and Telematics Infrastructure.", "fhirVersion": "4.0.1", - "mapping": [ - { - "identity": "rim", - "uri": "http://hl7.org/v3", - "name": "RIM Mapping" - }, - { - "identity": "w5", - "uri": "http://hl7.org/fhir/fivews", - "name": "FiveWs Pattern Mapping" - } - ], "kind": "resource", "abstract": false, "type": "Location", @@ -43,6 +21,39 @@ "path": "Location.id", "mustSupport": true }, + { + "id": "Location.meta.tag", + "path": "Location.meta.tag", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "system" + } + ], + "rules": "open" + }, + "min": 1, + "mustSupport": true + }, + { + "id": "Location.meta.tag:Origin", + "path": "Location.meta.tag", + "sliceName": "Origin", + "min": 1, + "max": "1", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/OriginVS" + } + }, + { + "id": "Location.meta.tag:Origin.system", + "path": "Location.meta.tag.system", + "min": 1, + "patternUri": "https://gematik.de/fhir/directory/CodeSystem/Origin" + }, { "id": "Location.name", "path": "Location.name", @@ -122,6 +133,16 @@ "path": "Location.position.altitude", "mustSupport": true }, + { + "id": "Location.managingOrganization", + "path": "Location.managingOrganization", + "mustSupport": true + }, + { + "id": "Location.partOf", + "path": "Location.partOf", + "mustSupport": true + }, { "id": "Location.hoursOfOperation", "path": "Location.hoursOfOperation", diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-OrganizationDirectory.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-OrganizationDirectory.json index 92eaf14e..17ca947f 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-OrganizationDirectory.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-OrganizationDirectory.json @@ -1,46 +1,14 @@ { "resourceType": "StructureDefinition", "id": "OrganizationDirectory", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-category", - "valueString": "Base.Entities" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category", - "valueCode": "business" - } - ], "url": "https://gematik.de/fhir/directory/StructureDefinition/OrganizationDirectory", - "version": "0.9.0", + "version": "0.11.2", "name": "OrganizationDirectory", "title": "Organization in gematik Directory", - "status": "draft", + "status": "active", "publisher": "gematik GmbH", "description": "Defines the data structure for medical, regulatory and technical \norganisations specific for german Healthcare and Telematics Infrastructure.", "fhirVersion": "4.0.1", - "mapping": [ - { - "identity": "v2", - "uri": "http://hl7.org/v2", - "name": "HL7 v2 Mapping" - }, - { - "identity": "rim", - "uri": "http://hl7.org/v3", - "name": "RIM Mapping" - }, - { - "identity": "servd", - "uri": "http://www.omg.org/spec/ServD/1.0/", - "name": "ServD" - }, - { - "identity": "w5", - "uri": "http://hl7.org/fhir/fivews", - "name": "FiveWs Pattern Mapping" - } - ], "kind": "resource", "abstract": false, "type": "Organization", @@ -53,6 +21,55 @@ "path": "Organization.id", "mustSupport": true }, + { + "id": "Organization.meta.tag", + "path": "Organization.meta.tag", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "system" + } + ], + "rules": "open" + }, + "min": 1, + "mustSupport": true + }, + { + "id": "Organization.meta.tag:Origin", + "path": "Organization.meta.tag", + "sliceName": "Origin", + "min": 1, + "max": "1", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/OriginVS" + } + }, + { + "id": "Organization.meta.tag:Origin.system", + "path": "Organization.meta.tag.system", + "min": 1, + "patternUri": "https://gematik.de/fhir/directory/CodeSystem/Origin" + }, + { + "id": "Organization.extension:organizationVisibility", + "path": "Organization.extension", + "sliceName": "organizationVisibility", + "min": 0, + "max": "*", + "type": [ + { + "code": "Extension", + "profile": [ + "https://gematik.de/fhir/directory/StructureDefinition/OrganizationVisibility" + ] + } + ], + "mustSupport": true + }, { "id": "Organization.identifier", "path": "Organization.identifier", @@ -132,17 +149,50 @@ ], "mustSupport": true }, + { + "id": "Organization.active", + "path": "Organization.active", + "mustSupport": true + }, { "id": "Organization.type", "path": "Organization.type", + "slicing": { + "discriminator": [ + { + "type": "pattern", + "path": "$this" + } + ], + "rules": "open" + }, "min": 1, - "max": "1", + "mustSupport": true + }, + { + "id": "Organization.type:providerType", + "path": "Organization.type", + "sliceName": "providerType", + "min": 0, + "max": "*", "mustSupport": true, "binding": { "strength": "required", "valueSet": "https://gematik.de/fhir/directory/ValueSet/OrganizationTypeVS" } }, + { + "id": "Organization.type:profession", + "path": "Organization.type", + "sliceName": "profession", + "min": 0, + "max": "*", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/OrganizationProfessionOIDTypeVS" + } + }, { "id": "Organization.name", "path": "Organization.name", @@ -154,6 +204,51 @@ "path": "Organization.alias", "mustSupport": true }, + { + "id": "Organization.address", + "path": "Organization.address", + "mustSupport": true + }, + { + "id": "Organization.address.use", + "path": "Organization.address.use", + "mustSupport": true + }, + { + "id": "Organization.address.text", + "path": "Organization.address.text", + "mustSupport": true + }, + { + "id": "Organization.address.line", + "path": "Organization.address.line", + "mustSupport": true + }, + { + "id": "Organization.address.city", + "path": "Organization.address.city", + "mustSupport": true + }, + { + "id": "Organization.address.state", + "path": "Organization.address.state", + "mustSupport": true + }, + { + "id": "Organization.address.postalCode", + "path": "Organization.address.postalCode", + "mustSupport": true + }, + { + "id": "Organization.address.country", + "path": "Organization.address.country", + "mustSupport": true + }, + { + "id": "Organization.partOf", + "path": "Organization.partOf", + "mustSupport": true + }, { "id": "Organization.contact", "path": "Organization.contact", diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-OrganizationVisibility.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-OrganizationVisibility.json new file mode 100644 index 00000000..4201e9d1 --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-OrganizationVisibility.json @@ -0,0 +1,57 @@ +{ + "resourceType": "StructureDefinition", + "id": "OrganizationVisibility", + "url": "https://gematik.de/fhir/directory/StructureDefinition/OrganizationVisibility", + "version": "0.11.2", + "name": "OrganizationVisibility", + "title": "OrganizationVisibility", + "status": "active", + "publisher": "gematik GmbH", + "description": "Visibility of an Organization in the FHIR-VZD. This Extensions includes codes of use-cases im which this Organization SHALL not be visible.", + "fhirVersion": "4.0.1", + "kind": "complex-type", + "abstract": false, + "context": [ + { + "expression": "Organization", + "type": "element" + } + ], + "type": "Extension", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension", + "derivation": "constraint", + "differential": { + "element": [ + { + "id": "Extension", + "path": "Extension", + "short": "OrganizationVisibility", + "definition": "Visibility of an Organization in the FHIR-VZD. This Extensions includes codes of use-cases im which this Organization SHALL not be visible." + }, + { + "id": "Extension.extension", + "path": "Extension.extension", + "max": "0" + }, + { + "id": "Extension.url", + "path": "Extension.url", + "fixedUri": "https://gematik.de/fhir/directory/StructureDefinition/OrganizationVisibility" + }, + { + "id": "Extension.value[x]", + "path": "Extension.value[x]", + "type": [ + { + "code": "Coding" + } + ], + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/OrganizationVisibilityVS" + } + } + ] + } +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-PractitionerDirectory.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-PractitionerDirectory.json index 718965ad..85d9137b 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-PractitionerDirectory.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-PractitionerDirectory.json @@ -1,45 +1,13 @@ { "resourceType": "StructureDefinition", "id": "PractitionerDirectory", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-category", - "valueString": "Base.Individuals" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category", - "valueCode": "individual" - } - ], "url": "https://gematik.de/fhir/directory/StructureDefinition/PractitionerDirectory", - "version": "0.9.0", + "version": "0.11.2", "name": "PractitionerDirectory", "title": "Practitioner in gematik Directory", - "status": "draft", + "status": "active", "publisher": "gematik GmbH", "fhirVersion": "4.0.1", - "mapping": [ - { - "identity": "v2", - "uri": "http://hl7.org/v2", - "name": "HL7 v2 Mapping" - }, - { - "identity": "rim", - "uri": "http://hl7.org/v3", - "name": "RIM Mapping" - }, - { - "identity": "servd", - "uri": "http://www.omg.org/spec/ServD/1.0/", - "name": "ServD" - }, - { - "identity": "w5", - "uri": "http://hl7.org/fhir/fivews", - "name": "FiveWs Pattern Mapping" - } - ], "kind": "resource", "abstract": false, "type": "Practitioner", @@ -52,6 +20,39 @@ "path": "Practitioner.id", "mustSupport": true }, + { + "id": "Practitioner.meta.tag", + "path": "Practitioner.meta.tag", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "system" + } + ], + "rules": "open" + }, + "min": 1, + "mustSupport": true + }, + { + "id": "Practitioner.meta.tag:Origin", + "path": "Practitioner.meta.tag", + "sliceName": "Origin", + "min": 1, + "max": "1", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/OriginVS" + } + }, + { + "id": "Practitioner.meta.tag:Origin.system", + "path": "Practitioner.meta.tag.system", + "min": 1, + "patternUri": "https://gematik.de/fhir/directory/CodeSystem/Origin" + }, { "id": "Practitioner.identifier", "path": "Practitioner.identifier", @@ -72,7 +73,7 @@ "path": "Practitioner.identifier", "sliceName": "TelematikID", "min": 1, - "max": "*", + "max": "1", "type": [ { "code": "Identifier", @@ -193,6 +194,7 @@ { "id": "Practitioner.qualification.code", "path": "Practitioner.qualification.code", + "mustSupport": true, "binding": { "strength": "extensible", "valueSet": "https://gematik.de/fhir/directory/ValueSet/PractitionerQualificationVS" diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-PractitionerRoleDirectory.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-PractitionerRoleDirectory.json index 8e0284b3..91b9292e 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-PractitionerRoleDirectory.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/StructureDefinition-PractitionerRoleDirectory.json @@ -1,45 +1,13 @@ { "resourceType": "StructureDefinition", "id": "PractitionerRoleDirectory", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-category", - "valueString": "Base.Individuals" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category", - "valueCode": "individual" - } - ], "url": "https://gematik.de/fhir/directory/StructureDefinition/PractitionerRoleDirectory", - "version": "0.9.0", + "version": "0.11.2", "name": "PractitionerRoleDirectory", "title": "PractitionerRole in gematik Directory", - "status": "draft", + "status": "active", "publisher": "gematik GmbH", "fhirVersion": "4.0.1", - "mapping": [ - { - "identity": "v2", - "uri": "http://hl7.org/v2", - "name": "HL7 v2 Mapping" - }, - { - "identity": "rim", - "uri": "http://hl7.org/v3", - "name": "RIM Mapping" - }, - { - "identity": "servd", - "uri": "http://www.omg.org/spec/ServD/1.0/", - "name": "ServD" - }, - { - "identity": "w5", - "uri": "http://hl7.org/fhir/fivews", - "name": "FiveWs Pattern Mapping" - } - ], "kind": "resource", "abstract": false, "type": "PractitionerRole", @@ -52,17 +20,60 @@ "path": "PractitionerRole.id", "mustSupport": true }, + { + "id": "PractitionerRole.meta.tag", + "path": "PractitionerRole.meta.tag", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "system" + } + ], + "rules": "open" + }, + "min": 1, + "mustSupport": true + }, + { + "id": "PractitionerRole.meta.tag:Origin", + "path": "PractitionerRole.meta.tag", + "sliceName": "Origin", + "min": 1, + "max": "1", + "mustSupport": true, + "binding": { + "strength": "required", + "valueSet": "https://gematik.de/fhir/directory/ValueSet/OriginVS" + } + }, + { + "id": "PractitionerRole.meta.tag:Origin.system", + "path": "PractitionerRole.meta.tag.system", + "min": 1, + "patternUri": "https://gematik.de/fhir/directory/CodeSystem/Origin" + }, { "id": "PractitionerRole.practitioner", "path": "PractitionerRole.practitioner", "min": 1, "mustSupport": true }, + { + "id": "PractitionerRole.organization", + "path": "PractitionerRole.organization", + "mustSupport": true + }, { "id": "PractitionerRole.location", "path": "PractitionerRole.location", "mustSupport": true }, + { + "id": "PractitionerRole.healthcareService", + "path": "PractitionerRole.healthcareService", + "mustSupport": true + }, { "id": "PractitionerRole.endpoint", "path": "PractitionerRole.endpoint", diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-AddressStateVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-AddressStateVS.json index 57fd1285..33320d3e 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-AddressStateVS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-AddressStateVS.json @@ -1,12 +1,12 @@ { "resourceType": "ValueSet", - "status": "draft", + "status": "active", "name": "AddressStateVS", "id": "AddressStateVS", "description": "ValueSet for `Address.state`", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/ValueSet/AddressStateVS", "publisher": "gematik GmbH", + "version": "0.11.2", "compose": { "include": [ { diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointConnectionTypeVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointConnectionTypeVS.json index 875c47ca..bb7f7274 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointConnectionTypeVS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointConnectionTypeVS.json @@ -1,12 +1,12 @@ { "resourceType": "ValueSet", - "status": "draft", + "status": "active", "name": "EndpointConnectionTypeVS", "id": "EndpointConnectionTypeVS", "description": "ValueSet for `Endpoint.connectionType`", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/ValueSet/EndpointConnectionTypeVS", "publisher": "gematik GmbH", + "version": "0.11.2", "compose": { "include": [ { diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointPayloadTypeVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointPayloadTypeVS.json index 1f196016..012a23af 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointPayloadTypeVS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointPayloadTypeVS.json @@ -1,12 +1,12 @@ { "resourceType": "ValueSet", - "status": "draft", + "status": "active", "name": "EndpointPayloadTypeVS", "id": "EndpointPayloadTypeVS", "description": "ValueSet for `Endpoint.payloadType`", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/ValueSet/EndpointPayloadTypeVS", "publisher": "gematik GmbH", + "version": "0.11.2", "compose": { "include": [ { diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointVisibilityVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointVisibilityVS.json new file mode 100644 index 00000000..0fa2199d --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-EndpointVisibilityVS.json @@ -0,0 +1,18 @@ +{ + "resourceType": "ValueSet", + "status": "active", + "name": "EndpointVisibilityVS", + "id": "EndpointVisibilityVS", + "title": "EndpointVisibilityVS", + "description": "EndpointVisibilityVS", + "url": "https://gematik.de/fhir/directory/ValueSet/EndpointVisibilityVS", + "publisher": "gematik GmbH", + "version": "0.11.2", + "compose": { + "include": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/EndpointVisibilityCS" + } + ] + } +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareServiceSpecialtyVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareServiceTypeVS.json similarity index 97% rename from directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareServiceSpecialtyVS.json rename to directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareServiceTypeVS.json index b65bccc9..92d334cc 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareServiceSpecialtyVS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareServiceTypeVS.json @@ -1,12 +1,12 @@ { "resourceType": "ValueSet", - "status": "draft", - "name": "HealthcareServiceSpecialtyVS", - "id": "HealthcareServiceSpecialtyVS", - "description": "ValueSet for `HealthcareService.specialty`", - "version": "0.9.0", - "url": "https://gematik.de/fhir/directory/ValueSet/HealthcareServiceSpecialtyVS", + "status": "active", + "name": "HealthcareServiceTypeVS", + "id": "HealthcareServiceTypeVS", + "description": "ValueSet for `HealthcareService.type`", + "url": "https://gematik.de/fhir/directory/ValueSet/HealthcareServiceTypeVS", "publisher": "gematik GmbH", + "version": "0.11.2", "compose": { "include": [ { @@ -403,6 +403,9 @@ "display": "Psychotherapie" } ] + }, + { + "system": "https://gematik.de/fhir/directory/CodeSystem/PharmacyTypeCS" } ] } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareSpecialityTypeVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareSpecialityTypeVS.json new file mode 100644 index 00000000..243c58d1 --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-HealthcareSpecialityTypeVS.json @@ -0,0 +1,21 @@ +{ + "resourceType": "ValueSet", + "status": "active", + "name": "HealthcareServiceSpecialityVS", + "id": "HealthcareSpecialityTypeVS", + "title": "ValueSet of HealthcareService specialities", + "description": "HealthcareService specialities", + "url": "https://gematik.de/fhir/directory/ValueSet/HealthcareSpecialityTypeVS", + "publisher": "gematik GmbH", + "version": "0.11.2", + "compose": { + "include": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/PharmacyHealthcareSpecialityCS" + }, + { + "system": "http://terminology.hl7.org/CodeSystem/service-type" + } + ] + } +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationProfessionOIDTypeVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationProfessionOIDTypeVS.json new file mode 100644 index 00000000..c5079a13 --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationProfessionOIDTypeVS.json @@ -0,0 +1,17 @@ +{ + "resourceType": "ValueSet", + "status": "active", + "name": "OrganizationProfessionOIDTypeVS", + "id": "OrganizationProfessionOIDTypeVS", + "description": "ValueSet for `Organization.type`", + "url": "https://gematik.de/fhir/directory/ValueSet/OrganizationProfessionOIDTypeVS", + "publisher": "gematik GmbH", + "version": "0.11.2", + "compose": { + "include": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/OrganizationProfessionOID" + } + ] + } +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationTypeVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationTypeVS.json index e8f4ea81..e3d7967d 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationTypeVS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationTypeVS.json @@ -1,17 +1,14 @@ { "resourceType": "ValueSet", - "status": "draft", + "status": "active", "name": "OrganizationTypeVS", "id": "OrganizationTypeVS", "description": "ValueSet for `Organization.type`", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/ValueSet/OrganizationTypeVS", "publisher": "gematik GmbH", + "version": "0.11.2", "compose": { "include": [ - { - "system": "https://gematik.de/fhir/directory/CodeSystem/OrganizationProfessionOID" - }, { "system": "https://gematik.de/fhir/directory/CodeSystem/OrganizationProviderType" } diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationVisibilityVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationVisibilityVS.json new file mode 100644 index 00000000..1164c6c1 --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OrganizationVisibilityVS.json @@ -0,0 +1,18 @@ +{ + "resourceType": "ValueSet", + "status": "active", + "name": "OrganizationVisibilityVS", + "id": "OrganizationVisibilityVS", + "title": "OrganizationVisibilityVS", + "description": "OrganizationVisibilityVS", + "url": "https://gematik.de/fhir/directory/ValueSet/OrganizationVisibilityVS", + "publisher": "gematik GmbH", + "version": "0.11.2", + "compose": { + "include": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/OrganizationVisibilityCS" + } + ] + } +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OriginVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OriginVS.json new file mode 100644 index 00000000..d41647ff --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-OriginVS.json @@ -0,0 +1,17 @@ +{ + "resourceType": "ValueSet", + "status": "active", + "name": "OriginVS", + "id": "OriginVS", + "description": "ValueSet for `meta.tag[Origin]`", + "url": "https://gematik.de/fhir/directory/ValueSet/OriginVS", + "publisher": "gematik GmbH", + "version": "0.11.2", + "compose": { + "include": [ + { + "system": "https://gematik.de/fhir/directory/CodeSystem/Origin" + } + ] + } +} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PharmacyHealthcareServiceTypeVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PharmacyHealthcareServiceTypeVS.json deleted file mode 100644 index 18861532..00000000 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PharmacyHealthcareServiceTypeVS.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "resourceType": "ValueSet", - "status": "draft", - "name": "PharmacyHealthcareServiceTypeVS", - "id": "PharmacyHealthcareServiceTypeVS", - "title": "ValueSet of HealthcareService types", - "description": "Types of accessibilities available", - "version": "1.0", - "url": "https://gematik.de/fhir/directory/ValueSet/PharmacyHealthcareServiceTypeVS", - "publisher": "gematik GmbH", - "compose": { - "include": [ - { - "system": "https://gematik.de/fhir/directory/CodeSystem/PharmacyHealthcareServiceTypeCS" - }, - { - "system": "http://terminology.hl7.org/CodeSystem/service-type" - } - ] - } -} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PharmacyTypeVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PharmacyTypeVS.json deleted file mode 100644 index 4791f8c1..00000000 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PharmacyTypeVS.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "resourceType": "ValueSet", - "status": "draft", - "name": "PharmacyTypeVS", - "id": "PharmacyTypeVS", - "title": "ValueSet of Pharmacy types", - "description": "Types of accessibilities available", - "version": "1.0", - "url": "https://gematik.de/fhir/directory/ValueSet/PharmacyTypeVS", - "publisher": "gematik GmbH", - "compose": { - "include": [ - { - "system": "https://gematik.de/fhir/directory/CodeSystem/PharmacyTypeCS" - } - ] - } -} diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PractitionerQualificationVS.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PractitionerQualificationVS.json index 535e832c..994061a5 100644 --- a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PractitionerQualificationVS.json +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-PractitionerQualificationVS.json @@ -1,12 +1,12 @@ { "resourceType": "ValueSet", - "status": "draft", + "status": "active", "name": "PractitionerQualificationVS", "id": "PractitionerQualificationVS", "description": "ValueSet for `Practitoner.qualification`", - "version": "0.9.0", "url": "https://gematik.de/fhir/directory/ValueSet/PractitionerQualificationVS", "publisher": "gematik GmbH", + "version": "0.11.2", "compose": { "include": [ { diff --git a/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-healthcareservice-characteristic-vs.json b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-healthcareservice-characteristic-vs.json new file mode 100644 index 00000000..e5ad0680 --- /dev/null +++ b/directory-lib/src/main/resources/de.gematik.fhir.directory/ValueSet-healthcareservice-characteristic-vs.json @@ -0,0 +1,24 @@ +{ + "resourceType": "ValueSet", + "status": "active", + "name": "HealthCareServiceCharacteristicVS", + "id": "healthcareservice-characteristic-vs", + "title": "HealthCareServiceCharacteristicVS", + "description": "HealthCareServiceCharacteristicVS", + "url": "https://gematik.de/fhir/directory/ValueSet/healthcareservice-characteristic-vs", + "publisher": "gematik GmbH", + "version": "0.11.2", + "compose": { + "include": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "concept": [ + { + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + } + ] + } +} diff --git a/directory-lib/src/test/kotlin/de/gematik/ti/directory/admin/TestAuth.kt b/directory-lib/src/test/kotlin/de/gematik/ti/directory/admin/TestAuth.kt index af880b88..4a6e5a12 100644 --- a/directory-lib/src/test/kotlin/de/gematik/ti/directory/admin/TestAuth.kt +++ b/directory-lib/src/test/kotlin/de/gematik/ti/directory/admin/TestAuth.kt @@ -1,5 +1,6 @@ package de.gematik.ti.directory.admin +import de.gematik.ti.directory.DirectoryEnvironment import io.kotest.core.spec.style.FeatureSpec import io.kotest.matchers.comparables.shouldBeGreaterThan import io.kotest.matchers.shouldBe @@ -10,7 +11,7 @@ class TestAuth : FeatureSpec({ scenario("Static token") { val adminClient = Client { - apiURL = DefaultConfig.environment(AdminEnvironment.tu).apiURL + apiURL = DefaultConfig.environment(DirectoryEnvironment.tu).apiURL auth { accessToken { System.getenv("TEST_ACCESS_TOKEN") @@ -23,7 +24,7 @@ class TestAuth : FeatureSpec({ scenario("Renew token") { val adminClient = Client { - apiURL = DefaultConfig.environment(AdminEnvironment.tu).apiURL + apiURL = DefaultConfig.environment(DirectoryEnvironment.tu).apiURL auth { accessToken { if (firstRun) { diff --git a/directory-lib/src/test/kotlin/de/gematik/ti/directory/fhir/TestClient.kt b/directory-lib/src/test/kotlin/de/gematik/ti/directory/fhir/TestClient.kt new file mode 100644 index 00000000..81fbfd1f --- /dev/null +++ b/directory-lib/src/test/kotlin/de/gematik/ti/directory/fhir/TestClient.kt @@ -0,0 +1,17 @@ +package de.gematik.ti.directory.fhir + +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe +import org.hl7.fhir.r4.model.IdType +import org.hl7.fhir.r4.model.Reference + +class TestClient : FeatureSpec({ + feature("Bundle") { + scenario("References") { + val ref = Reference("Practitioner/123") + val id = IdType(ref.reference) + id.idPart shouldBe "123" + id.resourceType shouldBe "Practitioner" + } + } +}) diff --git a/directory-lib/src/test/kotlin/de/gematik/ti/directory/fhir/TestSimpleFHIR.kt b/directory-lib/src/test/kotlin/de/gematik/ti/directory/fhir/TestSimpleFHIR.kt index 4f052992..def0e55e 100644 --- a/directory-lib/src/test/kotlin/de/gematik/ti/directory/fhir/TestSimpleFHIR.kt +++ b/directory-lib/src/test/kotlin/de/gematik/ti/directory/fhir/TestSimpleFHIR.kt @@ -16,7 +16,7 @@ class TestSimpleFHIR : FeatureSpec({ val specialization = "urn:psc:1.3.6.1.4.1.19376.3.276.1.5.4:ALLG" val regex = Regex("^urn:psc:([0-9\\.]+):(.*)$") regex.matchEntire(specialization)?.let { - HealthcareServiceSpecialtyVS.resolveCode( + HealthcareServiceTypeVS.resolveCode( "urn:oid:" + it.groupValues[1], it.groupValues[2], )?.display shouldBe "Allgemeinmedizin" diff --git a/gradle.properties b/gradle.properties index c954f6fd..3d4f179e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.6.2 +version=3.0.0 ktorVersion=2.2.2 kotestVersion=5.5.4 hapiVersion=6.1.3 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba77..7f93135c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c30b486a..b82aa23a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d42..0adc8e1a 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -197,6 +198,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in diff --git a/settings.gradle.kts b/settings.gradle.kts index 360e4099..22f350d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,2 @@ rootProject.name = "vzd-cli" -include("vzd-cli", "directory-lib", "directory-bff") +include("vzd-cli", "directory-lib") diff --git a/vzd-cli/additionalScripts/vzd-gui.bat b/vzd-cli/additionalScripts/vzd-gui.bat index a76779be..26ce00cb 100755 --- a/vzd-cli/additionalScripts/vzd-gui.bat +++ b/vzd-cli/additionalScripts/vzd-gui.bat @@ -1,4 +1,4 @@ @echo off set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. -%DIRNAME%\vzd-cli.bat gui +"%DIRNAME%\vzd-cli.bat" gui diff --git a/vzd-cli/build.gradle.kts b/vzd-cli/build.gradle.kts index 0a23161a..0948f61e 100644 --- a/vzd-cli/build.gradle.kts +++ b/vzd-cli/build.gradle.kts @@ -1,13 +1,18 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +val ktorVersion: String by project val bcVersion: String by project plugins { id("de.gematik.directory.app-conventions") - kotlin("plugin.serialization") version "1.8.10" + kotlin("plugin.serialization") version "1.9.22" id("com.github.johnrengelman.shadow") version "8.1.1" } +kotlin { + jvmToolchain(11) +} + java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -36,7 +41,7 @@ dependencies { // implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") // YAML support for kotlinx serialisation - implementation("net.mamoe.yamlkt:yamlkt:0.12.0") + implementation("net.mamoe.yamlkt:yamlkt:0.13.0") // CSV support implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.7.0") @@ -60,7 +65,6 @@ dependencies { // implementation("org.apache.opennlp:opennlp-uima:2.0.0") // Ktor server (for GUI BFF) - val ktorVersion: String by project implementation("io.ktor:ktor-server-core:$ktorVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") @@ -75,6 +79,9 @@ dependencies { // Validation framework implementation("io.konform:konform-jvm:0.4.0") + // jackson yaml, the version must match the transitive dependency from hapi + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.2") + // Bouncy castle fpr crypto and certificates processing shadow("org.bouncycastle:bcprov-jdk15on:$bcVersion") shadow("org.bouncycastle:bcpkix-jdk15on:$bcVersion") diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/Cli.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/Cli.kt index 64e3eb65..1e4c2b35 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/Cli.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/Cli.kt @@ -15,6 +15,7 @@ import de.gematik.ti.directory.DirectoryException import de.gematik.ti.directory.admin.AdminResponseException import de.gematik.ti.directory.apo.ApoCli import de.gematik.ti.directory.cli.admin.AdminCli +import de.gematik.ti.directory.cli.fhir.FhirCli import de.gematik.ti.directory.cli.gui.GuiCommand import de.gematik.ti.directory.cli.pers.PersCommand import de.gematik.ti.directory.cli.util.VaultException @@ -108,7 +109,9 @@ class Cli : CliktCommand(name = "vzd-cli") { subcommands( ConfigCommand(), UpdateCommand(), + LoginCommand(), AdminCli(), + FhirCli(), ApoCli(), GuiCommand(), PersCommand(), @@ -151,12 +154,14 @@ class Cli : CliktCommand(name = "vzd-cli") { currentContext.obj = CliContext(globalAPI) try { - val version = runBlocking { globalAPI.dailyUpdateCheck() } - if (version > BuildConfig.APP_VERSION) { - echo( - "Update is available: $version (current: ${BuildConfig.APP_VERSION}). Please update using `vzd-cli update`", - err = true, - ) + if (globalAPI.config.updates.enabled) { + val version = runBlocking { globalAPI.dailyUpdateCheck() } + if (version > BuildConfig.APP_VERSION) { + echo( + "Update is available: $version (current: ${BuildConfig.APP_VERSION}). Please update using `vzd-cli update`", + err = true, + ) + } } } catch (e: Exception) { // ignore error when checking for update diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/ConfigCommands.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/ConfigCommands.kt index 0d9c1b18..668797bb 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/ConfigCommands.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/ConfigCommands.kt @@ -8,7 +8,7 @@ import com.github.ajalt.clikt.parameters.types.choice import net.mamoe.yamlkt.Yaml import java.net.URL -private val YAML = Yaml { encodeDefaultValues = false } +private val YAML = Yaml { encodeDefaultValues = true } class ConfigCommand : CliktCommand(name = "config", help = "Manage configuration") { init { @@ -34,6 +34,7 @@ val SET_PROPERTIES = }, "httpProxy.enabled" to { config: GlobalConfig, value: String -> config.httpProxy.enabled = value.toBoolean() }, "updates.preReleasesEnabled" to { config: GlobalConfig, value: String -> config.updates.preReleasesEnabled = value.toBoolean() }, + "updates.enabled" to { config: GlobalConfig, value: String -> config.updates.enabled = value.toBoolean() }, ) class ConfigSetCommand : CliktCommand( diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/GlobalConfig.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/GlobalConfig.kt index 95c98914..6ac2f2b1 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/GlobalConfig.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/GlobalConfig.kt @@ -16,6 +16,7 @@ data class UpdatesConfig( var preReleasesEnabled: Boolean, var lastCheck: Long, var latestRelease: String, + var enabled: Boolean = true, ) @Serializable @@ -38,6 +39,7 @@ internal class GlobalConfigFileStore(customConfigPath: Path? = null) : FileObjec preReleasesEnabled = false, lastCheck = -1, latestRelease = BuildConfig.APP_VERSION, + enabled = true, ), ) }, diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/LoginCommand.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/LoginCommand.kt new file mode 100644 index 00000000..888f9073 --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/LoginCommand.kt @@ -0,0 +1,57 @@ +package de.gematik.ti.directory.cli + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.prompt +import de.gematik.ti.directory.DirectoryEnvironment +import de.gematik.ti.directory.cli.admin.AdminAPI +import de.gematik.ti.directory.cli.fhir.FhirAPI +import mu.KotlinLogging + +private val logger = KotlinLogging.logger {} + +class LoginCommand : CliktCommand(name = "login", help = "Logins into all configured APIs") { + private val environments by argument().multiple(default = listOf("pu", "ru", "tu")) + private val password by option( + "--password", + "-p", + help = "Password for protection of the Vault", + envvar = "VAULT_PASSWORD", + ).prompt("Enter Vault Password", hideInput = true) + + override fun run() { + catching { + val globalAPI = GlobalAPI() + val adminAPI = AdminAPI(globalAPI) + val adminVault = adminAPI.openVault(password) + adminVault.list().forEach { + if (environments.contains(it.variant)) { + try { + adminAPI.login(DirectoryEnvironment.valueOf(it.variant), it.name, it.secret) + echo("Logged in as ${it.name} to Admin API (${it.variant})") + } catch (e: Exception) { + echo("Failed to login as ${it.name} to Admin API (${it.variant})") + logger.debug(e) { "Stacktrace of previous error" } + } + } + } + + val fhirAPI = FhirAPI(globalAPI) + val fhirVault = fhirAPI.openVaultFdv(password) + + fhirVault.list().forEach { + if (environments.contains(it.variant)) { + try { + fhirAPI.loginFdv(DirectoryEnvironment.valueOf(it.variant), it.name, it.secret) + echo("Logged in as ${it.name} to FHIR FDV API (${it.variant})") + } catch (e: Exception) { + echo("Failed to login as ${it.name} to FHIR FDV API (${it.variant})") + logger.debug(e) { "Stacktrace of previous error" } + } + } + } + } + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/VaultCommands.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/VaultCommands.kt similarity index 66% rename from vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/VaultCommands.kt rename to vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/VaultCommands.kt index d52e79be..5c03b58d 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/VaultCommands.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/VaultCommands.kt @@ -1,4 +1,4 @@ -package de.gematik.ti.directory.cli.admin +package de.gematik.ti.directory.cli import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktError @@ -8,28 +8,31 @@ import com.github.ajalt.clikt.parameters.options.prompt import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.path -import de.gematik.ti.directory.cli.catching +import de.gematik.ti.directory.cli.admin.AdminEnvironment import de.gematik.ti.directory.cli.util.KeyStoreVault import de.gematik.ti.directory.cli.util.KeyStoreVaultProvider import mu.KotlinLogging private val logger = KotlinLogging.logger {} -class VaultCommand : CliktCommand(name = "vault", help = "Manage OAuth credentials in the Vault") { +class VaultCommand(serviceName: String, commandName: String = "vault", commandHelp: String = "Manage OAuth credentials in the Vault") : CliktCommand( + name = commandName, + help = commandHelp, +) { init { subcommands( - VaultResetCommand(), - VaultListCommand(), - VaultStoreCommand(), - VaultExportCommand(), - VaultImportCommand(), + VaultListCommand(serviceName), + VaultStoreCommand(serviceName), + VaultExportCommand(serviceName), + VaultImportCommand(serviceName), + VaultClearCommand(serviceName), ) } override fun run() = Unit } -abstract class AbstractVaultCommand(name: String, help: String) : CliktCommand(name = name, help = help) { +abstract class AbstractVaultCommand(val serviceName: String, name: String, help: String) : CliktCommand(name = name, help = help) { protected val password by option( "--password", "-p", @@ -41,11 +44,11 @@ abstract class AbstractVaultCommand(name: String, help: String) : CliktCommand(n fun openOrCreateVault(): KeyStoreVault { return password?.let { - vaultProvider.open(it) + vaultProvider.open(it, serviceName) } ?: run { if (vaultProvider.exists()) { val promptPassword = prompt("Enter Vault password", hideInput = true) ?: throw CliktError() - vaultProvider.open(promptPassword) + vaultProvider.open(promptPassword, serviceName) } else { logger.info { "Creating new vault" } val newPassword = @@ -54,40 +57,51 @@ abstract class AbstractVaultCommand(name: String, help: String) : CliktCommand(n hideInput = true, requireConfirmation = true, ) ?: throw CliktError() - vaultProvider.open(newPassword) + vaultProvider.open(newPassword, serviceName) } } } } -class VaultResetCommand : AbstractVaultCommand(name = "purge", help = "Remove Vault") { +class VaultListCommand(serviceName: String) : AbstractVaultCommand( + serviceName = serviceName, + name = "list", + help = "List configured OAuth2 credentials", +) { override fun run() = catching { - if (confirm("Are you sure you want to delete ALL secrets stored in the vault?") == true) { - vaultProvider.purge() - echo("Vault purged.") - } else { - echo("Abort. Vault left intact.") + if (!vaultProvider.exists()) { + throw CliktError("Vault does not exist.") + } + val vault = openOrCreateVault() + echo("Env ClientID Secret") + echo("=== ==================== ======") + vault.list().forEach { + echo("%-3s %-20s ******".format(it.variant, it.name)) } } } -class VaultListCommand : AbstractVaultCommand(name = "list", help = "List configured OAuth2 credentials") { +class VaultClearCommand(serviceName: String) : AbstractVaultCommand( + serviceName = serviceName, + name = "clear", + help = "Clears the credentials of this service", +) { override fun run() = catching { if (!vaultProvider.exists()) { throw CliktError("Vault does not exist.") } val vault = openOrCreateVault() - echo("Env ClientID Secret") - echo("=== ==================== ======") - vault.list().forEach { - echo("%-3s %-20s ******".format(it.variant, it.name)) - } + vault.clear() } } -class VaultStoreCommand : AbstractVaultCommand(name = "store", help = "Store OAuth2 client credentials") { +class VaultStoreCommand(serviceName: String) : AbstractVaultCommand( + serviceName = serviceName, + name = "store", + help = "Store OAuth2 client credentials", +) { private val env by option( "-e", "--env", @@ -107,7 +121,11 @@ class VaultStoreCommand : AbstractVaultCommand(name = "store", help = "Store OAu } } -class VaultExportCommand : AbstractVaultCommand(name = "export", help = "Export Vault to a file for backup or transfer.") { +class VaultExportCommand(serviceName: String) : AbstractVaultCommand( + serviceName = serviceName, + name = "export", + help = "Export Vault to a file for backup or transfer.", +) { private val output by option("-o", "--output").path(canBeDir = false).required() private val transferPassword by option("-t", "--transfer-password") .prompt("Enter Vault transfer password", hideInput = true) @@ -116,7 +134,7 @@ class VaultExportCommand : AbstractVaultCommand(name = "export", help = "Export catching { logger.info { "Exporting Vault to $output" } val transferVaultProvider = KeyStoreVaultProvider(customVaultPath = output) - val transferVault = transferVaultProvider.open(transferPassword) + val transferVault = transferVaultProvider.open(transferPassword, serviceName) val vault = openOrCreateVault() vault.list().forEach { @@ -126,7 +144,11 @@ class VaultExportCommand : AbstractVaultCommand(name = "export", help = "Export } } -class VaultImportCommand : AbstractVaultCommand(name = "import", help = "Import credentials from another Vault") { +class VaultImportCommand(serviceName: String) : AbstractVaultCommand( + serviceName = serviceName, + name = "import", + help = "Import credentials from another Vault", +) { private val input by option("-i", "--input").path(canBeDir = false, mustBeReadable = true).required() private val transferPassword by option("-t", "--transfer-password") .prompt("Enter TRANSFER Vault password", hideInput = true) @@ -134,7 +156,7 @@ class VaultImportCommand : AbstractVaultCommand(name = "import", help = "Import override fun run() = catching { logger.info { "Importing Vault from $input" } - val transferVault = KeyStoreVaultProvider(customVaultPath = input).open(transferPassword) + val transferVault = KeyStoreVaultProvider(customVaultPath = input).open(transferPassword, serviceName) val vault = openOrCreateVault() transferVault.list().forEach { @@ -143,3 +165,17 @@ class VaultImportCommand : AbstractVaultCommand(name = "import", help = "Import } } } + +class VaultPurgeCommand() : CliktCommand(name = "purge", help = "Remove Vault") { + protected val vaultProvider = KeyStoreVaultProvider() + + override fun run() = + catching { + if (confirm("Are you sure you want to delete ALL secrets stored in the vault?") == true) { + vaultProvider.purge() + echo("Vault purged.") + } else { + echo("Abort. Vault left intact.") + } + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/AdminAPI.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/AdminAPI.kt index f1ab367c..e61818fe 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/AdminAPI.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/AdminAPI.kt @@ -1,5 +1,6 @@ package de.gematik.ti.directory.cli.admin +import de.gematik.ti.directory.ClientCredentialsAuthenticator import de.gematik.ti.directory.DirectoryAuthException import de.gematik.ti.directory.admin.* import de.gematik.ti.directory.cli.GlobalAPI @@ -16,7 +17,9 @@ import java.nio.file.Path private val logger = KotlinLogging.logger {} -typealias AdminEnvironment = de.gematik.ti.directory.admin.AdminEnvironment +const val SERVICE_NAME = "urn:gematik:directory:admin" + +typealias AdminEnvironment = de.gematik.ti.directory.DirectoryEnvironment internal class AdminConfigFileStore(customConfigPath: Path? = null) : FileObjectStore( "directory-admin.yaml", @@ -43,6 +46,8 @@ data class AdminStatus( ) class AdminAPI(val globalAPI: GlobalAPI) { + val config by lazy { loadConfig() } + fun createClient(env: AdminEnvironment): Client { val tokenStore = TokenStore() val envConfig = config.environment(env) @@ -77,10 +82,8 @@ class AdminAPI(val globalAPI: GlobalAPI) { return store.reset() } - val config by lazy { loadConfig() } - fun openVault(vaultPassword: String): KeyStoreVault { - return KeyStoreVaultProvider().open(vaultPassword) + return KeyStoreVaultProvider().open(vaultPassword, SERVICE_NAME) } suspend fun status(includeBackendInfo: Boolean = false): AdminStatus { diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/AdminCli.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/AdminCli.kt index 5c09ecf7..3178a1f2 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/AdminCli.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/admin/AdminCli.kt @@ -4,6 +4,7 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.core.subcommands import de.gematik.ti.directory.cli.GlobalAPI +import de.gematik.ti.directory.cli.VaultCommand import de.gematik.ti.directory.cli.catching import mu.KotlinLogging @@ -34,7 +35,7 @@ class AdminCli : init { subcommands( - VaultCommand(), + VaultCommand(SERVICE_NAME), ConfigCommand(), StatusCommand(), CertInfoCommand(), diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/bff/AdminEndpoint.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/bff/AdminEndpoint.kt index 90f4b7d4..beb556af 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/bff/AdminEndpoint.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/bff/AdminEndpoint.kt @@ -1,5 +1,6 @@ package de.gematik.ti.directory.cli.bff +import de.gematik.ti.directory.DirectoryEnvironment import de.gematik.ti.directory.admin.* import de.gematik.ti.directory.elaborate.ElaborateDirectoryEntry import de.gematik.ti.directory.elaborate.elaborate @@ -16,7 +17,7 @@ import java.io.IOException @Serializable data class LoginWithVaultRepresentation( - val env: AdminEnvironment, + val env: DirectoryEnvironment, val vaultPassword: String, ) @@ -34,7 +35,7 @@ class Admin { @Serializable @Resource("{envTitle}") class Env(val parent: Admin = Admin(), private val envTitle: String) { - val env get() = AdminEnvironment.valueOf(envTitle) + val env get() = DirectoryEnvironment.valueOf(envTitle) @Serializable @Resource("search") diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/bff/VaultEndpoint.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/bff/VaultEndpoint.kt index 4ab35ba7..81d1bd7d 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/bff/VaultEndpoint.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/bff/VaultEndpoint.kt @@ -1,17 +1,16 @@ package de.gematik.ti.directory.cli.bff -import de.gematik.ti.directory.admin.AdminEnvironment +import de.gematik.ti.directory.DirectoryEnvironment +import de.gematik.ti.directory.cli.admin.SERVICE_NAME import de.gematik.ti.directory.cli.util.KeyStoreVaultProvider import de.gematik.ti.directory.cli.util.VaultException import io.ktor.http.* import io.ktor.resources.* import io.ktor.server.application.* import io.ktor.server.request.* -import io.ktor.server.resources.* import io.ktor.server.resources.post import io.ktor.server.response.* import io.ktor.server.routing.* -import io.ktor.server.sessions.* import kotlinx.serialization.Serializable @Serializable @@ -25,13 +24,13 @@ fun Route.vaultRoute() { post { val vaultPassword = call.receive().vaultPassword try { - val vault = KeyStoreVaultProvider().open(vaultPassword) + val vault = KeyStoreVaultProvider().open(vaultPassword, SERVICE_NAME) val adminAPI = call.application.attributes[AdminAPIKey] vault.list().forEach { secret -> application.log.info("Logging in to: ${secret.variant}") - adminAPI.login(AdminEnvironment.valueOf(secret.variant), secret.name, secret.secret) + adminAPI.login(DirectoryEnvironment.valueOf(secret.variant), secret.name, secret.secret) } call.respond(Outcome("", "Used vault to login to: ${vault.list().map { it.variant }.joinToString()}")) diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvCommands.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvCommands.kt new file mode 100644 index 00000000..a6329c85 --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvCommands.kt @@ -0,0 +1,19 @@ +package de.gematik.ti.directory.cli.fhir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands + +class FdvCommands : CliktCommand(name = "fdv", help = "Commands for FDVSearchAPI") { + init { + subcommands( + FdvLoginCommand(), + FdvTokenCommand(), + SearchCommand { context, query -> context.client.searchFdv(query) }, + FdvShowCommand(), + ) + } + + override fun run() { + // no-op + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvLoginCommand.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvLoginCommand.kt new file mode 100644 index 00000000..dc02d11f --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvLoginCommand.kt @@ -0,0 +1,26 @@ +package de.gematik.ti.directory.cli.fhir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.CliktError +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.prompt +import de.gematik.ti.directory.cli.catching + +class FdvLoginCommand : CliktCommand(name = "login", help = "Login to FDVSearchAPI") { + private val context by requireObject() + private val password by option( + "--password", + "-p", + help = "Password for protection of the Vault", + envvar = "VAULT_PASSWORD", + ).prompt("Enter Vault Password", hideInput = true) + + override fun run() = + catching { + val vault = context.fhirAPI.openVaultFdv(password) + val credentials = vault.get(context.env.name) ?: throw CliktError("No credentials found for ${context.env.name}") + context.fhirAPI.loginFdv(context.env, credentials.name, credentials.secret) + echo("Logged in as ${credentials.name} to FHIR FDV API (${context.env.name})") + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvShowCommand.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvShowCommand.kt new file mode 100644 index 00000000..44ff8d8f --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvShowCommand.kt @@ -0,0 +1,67 @@ +package de.gematik.ti.directory.cli.fhir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.switch +import de.gematik.ti.directory.cli.catching +import de.gematik.ti.directory.fhir.SearchQuery +import de.gematik.ti.directory.fhir.SearchResource +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging + +class FdvShowCommand : CliktCommand(name = "show", help = "Shows an entry") { + private val logger = KotlinLogging.logger {} + private val context by requireObject() + + private val telematikID by argument("TELEMATIK_ID", help = "Telematik-ID of the entry") + + private val outputFormat by option().switch( + "--json" to OutputFormat.JSON, + "--json-ext" to OutputFormat.JSON_EXT, + "--yaml" to OutputFormat.YAML, + "--human" to OutputFormat.HUMAN, + ).default(OutputFormat.HUMAN) + + private val active: Boolean by option("--active", "-a", help = "Filter by active status").flag(default = true) + + override fun run() = + catching { + runBlocking { + launch { + val practitionerRoleQuery = SearchQuery(SearchResource.PractitionerRole) + practitionerRoleQuery.addParam("practitioner.active", active.toString()) + practitionerRoleQuery.addParam( + "practitioner.identifier", + "https://gematik.de/fhir/sid/telematik-id|$telematikID", + ) + practitionerRoleQuery.addParam("_include", "PractitionerRole:practitioner") + practitionerRoleQuery.addParam("_include", "PractitionerRole:location") + practitionerRoleQuery.addParam("_include", "PractitionerRole:endpoint") + val practitionerRoleBundle = context.client.searchFdv(practitionerRoleQuery) + if (practitionerRoleBundle.entry?.isNotEmpty() == true) { + echo(practitionerRoleBundle.toStringOutput(outputFormat)) + } + } + launch { + val healthcareServiceQuery = SearchQuery(SearchResource.HealthcareService) + healthcareServiceQuery.addParam("organization.active", active.toString()) + healthcareServiceQuery.addParam( + "organization.identifier", + "https://gematik.de/fhir/sid/telematik-id|$telematikID", + ) + healthcareServiceQuery.addParam("_include", "HealthcareService:organization") + healthcareServiceQuery.addParam("_include", "HealthcareService:location") + healthcareServiceQuery.addParam("_include", "HealthcareService:endpoint") + val healthcareServiceBundle = context.client.searchFdv(healthcareServiceQuery) + if (healthcareServiceBundle.entry?.isNotEmpty() == true) { + echo(healthcareServiceBundle.toStringOutput(outputFormat)) + } + } + } + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvTokenCommand.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvTokenCommand.kt new file mode 100644 index 00000000..f501e5fa --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FdvTokenCommand.kt @@ -0,0 +1,26 @@ +package de.gematik.ti.directory.cli.fhir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.option +import de.gematik.ti.directory.cli.catching + +class FdvTokenCommand : CliktCommand(name = "token", help = "Set or get Access Token for FDVSearchAPI") { + private val context by requireObject() + private val token by option( + "-s", + "--set", + metavar = "ACCESS_TOKEN", + help = "Sets OAuth2 Access Token", + envvar = "FDV_SEARCH_ACCESS_TOKEN", + ) + + override fun run() = + catching { + token?.let { context.fhirAPI.storeAccessTokenFdv(context.env, it) } + + echo( + context.fhirAPI.retrieveAccessTokenFdv(context.env), + ) + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FhirAPI.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FhirAPI.kt new file mode 100644 index 00000000..d4b10af8 --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FhirAPI.kt @@ -0,0 +1,148 @@ +package de.gematik.ti.directory.cli.fhir + +import de.gematik.ti.directory.ClientCredentialsAuthenticator +import de.gematik.ti.directory.DirectoryAuthException +import de.gematik.ti.directory.DirectoryEnvironment +import de.gematik.ti.directory.cli.GlobalAPI +import de.gematik.ti.directory.cli.util.KeyStoreVault +import de.gematik.ti.directory.cli.util.KeyStoreVaultProvider +import de.gematik.ti.directory.cli.util.TokenStore +import de.gematik.ti.directory.fhir.Client +import de.gematik.ti.directory.fhir.DefaultConfig +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive + +const val FDV_SEARCH_SERVICE_NAME = "urn:gematik:directory:fhir:fdv-search" + +class FhirAPI(val globalAPI: GlobalAPI) { + val config = DefaultConfig + + fun createClient(env: DirectoryEnvironment): Client { + val cfg = config.environment(env) + val client = + Client { + envConfig = cfg + authFdv { + accessToken { + retrieveAccessTokenFdv(env) + } + } + authSearch { + accessToken { + retrieveAccessTokenSearch(env) + } + } + if (globalAPI.config.httpProxy.enabled) { + httpProxyURL = globalAPI.config.httpProxy.proxyURL + } + } + return client + } + + fun openVaultFdv(vaultPassword: String): KeyStoreVault { + return KeyStoreVaultProvider().open(vaultPassword, FDV_SEARCH_SERVICE_NAME) + } + + fun storeAccessTokenSearch( + env: DirectoryEnvironment, + accessToken: String + ) { + val tokenStore = TokenStore() + val envConfig = config.environment(env) + tokenStore.addAccessToken(envConfig.search.apiURL, accessToken) + } + + fun retrieveAccessTokenSearch(env: DirectoryEnvironment): String { + val tokenStore = TokenStore() + val envConfig = config.environment(env) + return tokenStore.accessTokenFor( + envConfig.search.apiURL, + )?.accessToken ?: throw DirectoryAuthException("You are not logged in to environment (SearchAPI): $env") + } + + fun storeAccessTokenFdv( + env: DirectoryEnvironment, + accessToken: String + ) { + val tokenStore = TokenStore() + val envConfig = config.environment(env) + tokenStore.addAccessToken(envConfig.fdv.apiURL, accessToken) + } + + fun retrieveAccessTokenFdv(env: DirectoryEnvironment): String { + val tokenStore = TokenStore() + val envConfig = config.environment(env) + return tokenStore.accessTokenFor( + envConfig.fdv.apiURL, + )?.accessToken ?: throw DirectoryAuthException("You are not logged in to environment (FDVSearchAPI): $env") + } + + fun loginFdv( + env: DirectoryEnvironment, + clientID: String, + clientSecret: String + ): Map { + val tokenStore = TokenStore() + val envConfig = config.environment(env) + + val auth = + ClientCredentialsAuthenticator( + envConfig.fdv.authenticationEndpoint, + if (globalAPI.config.httpProxy.enabled) globalAPI.config.httpProxy.proxyURL else null, + ) + val authResponse = runBlocking { auth.authenticate(clientID, clientSecret) } + + val httpClient = + HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + }, + ) + } + if (globalAPI.config.httpProxy.enabled) { + engine { + logger.debug { "Using proxy: ${globalAPI.config.httpProxy.proxyURL} for FHIR authorization" } + proxy = ProxyBuilder.http(globalAPI.config.httpProxy.proxyURL) + } + } + } + val token = authResponse.accessToken + + val authzResponse: JsonObject = + runBlocking { + val response = + httpClient.get(envConfig.fdv.authorizationEndpoint) { + headers { + append("Authorization", "Bearer $token") + } + } + + if (response.status.value != 200) { + val body: String = response.body() + throw DirectoryAuthException( + "Login failed: env:$env , clientID:$clientID, url:${envConfig.fdv.authorizationEndpoint}, status:${response.status.value}, body:$body", + ) + } + response.body() + } + + if (authzResponse["access_token"] == null) { + throw DirectoryAuthException("Login failed: env:$env , clientID:$clientID, url:${envConfig.fdv.apiURL}") + } + tokenStore.addAccessToken(envConfig.fdv.apiURL, authzResponse["access_token"]!!.jsonPrimitive.content) + + logger.info { "Login successful: env:$env , clientID:$clientID, url:${envConfig.fdv.apiURL}" } + return tokenStore.claimsFor(envConfig.fdv.apiURL)!! + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FhirCLI.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FhirCLI.kt new file mode 100644 index 00000000..592ce97c --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/FhirCLI.kt @@ -0,0 +1,78 @@ +package de.gematik.ti.directory.cli.fhir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.arguments.help +import com.github.ajalt.clikt.parameters.options.option +import de.gematik.ti.directory.DirectoryEnvironment +import de.gematik.ti.directory.cli.GlobalAPI +import de.gematik.ti.directory.cli.VaultCommand +import de.gematik.ti.directory.cli.catching +import mu.KotlinLogging + +val logger = KotlinLogging.logger {} + +class FhirCliContext( + val fhirAPI: FhirAPI, +) + +class FhirCliEnvironmentContext( + val fhirAPI: FhirAPI, + var env: DirectoryEnvironment, +) { + val client by lazy { + fhirAPI.createClient(env) + } +} + +class FhirCli : + CliktCommand(name = "fhir", help = """CLI for FHIR Directory""".trimMargin()) { + override fun run() = + catching { + val adminAPI = FhirAPI(GlobalAPI()) + currentContext.obj = FhirCliContext(adminAPI) + } + + init { + subcommands( + VaultCommand(FDV_SEARCH_SERVICE_NAME, "fdv-vault", "Manage FDVSearchAPI credentials in the Vault"), + EnvironmentCommands(DirectoryEnvironment.pu), + EnvironmentCommands(DirectoryEnvironment.ru), + EnvironmentCommands(DirectoryEnvironment.tu), + ) + } +} + +class EnvironmentCommands(env: DirectoryEnvironment) : CliktCommand(name = env.name, help = """Commands for $env instance""".trimMargin()) { + private val context by requireObject() + + init { + subcommands( + FdvCommands(), + SearchTokenCommand(), + SearchCommand { ctx, query -> + ctx.client.search(query) + }, + ) + } + + override fun run() = + catching { + currentContext.obj = FhirCliEnvironmentContext(context.fhirAPI, DirectoryEnvironment.valueOf(commandName)) + } +} + +class SearchTokenCommand : CliktCommand(name = "token", help = "Set or get Access Token for SearchAPI") { + private val context by requireObject() + private val token by option("-s", "--set", metavar = "ACCESS_TOKEN", help = "Sets OAuth2 Access Token", envvar = "SEARCH_ACCESS_TOKEN") + + override fun run() = + catching { + token?.let { context.fhirAPI.storeAccessTokenSearch(context.env, it) } + + echo( + context.fhirAPI.retrieveAccessTokenSearch(context.env), + ) + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/Output.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/Output.kt new file mode 100644 index 00000000..eba15b69 --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/Output.kt @@ -0,0 +1,63 @@ +package de.gematik.ti.directory.cli.fhir + +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import de.gematik.ti.directory.fhir.FHIRSerializerModule +import de.gematik.ti.directory.fhir.FhirContextR4 +import de.gematik.ti.directory.fhir.toDirectoryEntries +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.hl7.fhir.r4.model.* + +val fhirExtFormatter = + Json { + prettyPrint = true + serializersModule = FHIRSerializerModule + } + +enum class OutputFormat { + JSON, + JSON_EXT, + YAML, + HUMAN, + TABLE, +} + +fun Bundle.toJson(): String { + val parser = FhirContextR4.newJsonParser() + parser.setPrettyPrint(true) + return parser.encodeResourceToString(this) +} + +fun Bundle.toStringOutput(format: OutputFormat): String { + return when (format) { + OutputFormat.JSON -> toJson() + OutputFormat.JSON_EXT -> toJsonExt() + OutputFormat.YAML -> toYaml() + OutputFormat.HUMAN -> toHuman() + OutputFormat.TABLE -> toTable() + } +} + +fun Bundle.toJsonExt(): String { + val entries = this.toDirectoryEntries() + return fhirExtFormatter.encodeToString(entries) +} + +private fun toYaml(jsonString: String): String { + // parse json + val jsonMapper = ObjectMapper(JsonFactory()) + val json = jsonMapper.readTree(jsonString) + // convert to yaml + val yamlMapper = ObjectMapper(YAMLFactory()) + return yamlMapper.writeValueAsString(json) +} + +fun Bundle.toYaml(): String { + return toYaml(toJson()) +} + +fun Bundle.toHuman(): String { + return toYaml(toJsonExt()) +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/OutputBundleTable.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/OutputBundleTable.kt new file mode 100644 index 00000000..cde19b42 --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/OutputBundleTable.kt @@ -0,0 +1,50 @@ +package de.gematik.ti.directory.cli.fhir + +import de.gematik.ti.directory.fhir.toDirectoryEntries +import hu.vissy.texttable.dsl.tableFormatter +import org.hl7.fhir.r4.model.Bundle + +fun Bundle.toTable(): String { + val formatter = + tableFormatter { + labeled("TelematikID", "Gesamt") { + extractor { it.telematikID } + } + + class State(var count: Int = 0) + stateful("Name") { + initState { State() } + extractor { directoryEntry, state -> + state.count += 1 + directoryEntry.displayName ?: "N/A" + } + cellFormatter { + maxWidth = 24 + } + aggregator { _, state -> + state.count.toString() + } + } + + stateless("Address") { + extractor { directoryEntry -> + buildString { + directoryEntry.location?.firstOrNull()?.let { + append(it.address?.line?.joinToString()) + append(", ") + append(it.address?.postalCode) + append(" ") + append(it.address?.city) + } + } + } + cellFormatter { + maxWidth = 40 + } + } + + showAggregation = true + } + + return formatter.apply(this.toDirectoryEntries()) +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchCommand.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchCommand.kt new file mode 100644 index 00000000..ffd85b79 --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchCommand.kt @@ -0,0 +1,35 @@ +package de.gematik.ti.directory.cli.fhir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.core.subcommands +import de.gematik.ti.directory.cli.catching +import de.gematik.ti.directory.fhir.SearchQuery +import org.hl7.fhir.r4.model.Bundle + +class SearchCommand(val search: suspend (FhirCliEnvironmentContext, SearchQuery) -> Bundle) : CliktCommand( + name = "search", + help = "Search FHIR Directory", +) { + private val context by requireObject() + + class SearchContext(val search: suspend (FhirCliEnvironmentContext, SearchQuery) -> Bundle, val ctx: FhirCliEnvironmentContext) + + init { + subcommands( + SearchHealthcareServiceCommand(), + SearchPractitionerRoleCommand(), + ) + } + + override fun aliases(): Map> = + mapOf( + "hs" to listOf("healthcare-service"), + "pr" to listOf("practitioner-role"), + ) + + override fun run() = + catching { + currentContext.obj = SearchContext(search, context) + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchHealthcareServiceCommand.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchHealthcareServiceCommand.kt new file mode 100644 index 00000000..e1579a9b --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchHealthcareServiceCommand.kt @@ -0,0 +1,99 @@ +package de.gematik.ti.directory.cli.fhir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.options.* +import de.gematik.ti.directory.cli.catching +import de.gematik.ti.directory.fhir.SearchQuery +import de.gematik.ti.directory.fhir.SearchResource +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging + +class SearchHealthcareServiceCommand : CliktCommand(name = "healthcare-service", help = "Search HealthcareService resources (alias: hs)") { + private val logger = KotlinLogging.logger {} + private val context by requireObject() + + private val outputFormat by option().switch( + "--json" to OutputFormat.JSON, + "--json-ext" to OutputFormat.JSON_EXT, + "--yaml" to OutputFormat.YAML, + "--human" to OutputFormat.HUMAN, + "--table" to OutputFormat.TABLE, + ).default(OutputFormat.TABLE) + + private val active: Boolean by option("--active", "-a", help = "Filter by active status").flag(default = true) + + private val includeOrganization by option().switch( + "--include-organization" to "HealthcareService:organization", + "--exclude-organization" to "", + ).default("HealthcareService:organization") + + private val includeLocation by option().switch( + "--include-location" to "HealthcareService:location", + "--exclude-location" to "", + ).default("HealthcareService:location") + + private val includeEndpoint by option().switch( + "--include-endpoint" to "HealthcareService:endpoint", + "--exclude-endpoint" to "", + ).default("HealthcareService:endpoint") + + private val textArguments by argument("SEARCH_TEXT").multiple(required = false) + private val telematikID by option("--telematik-id", "-t", help = "Telematik-ID of the Organization") + private val professionOID by option("--professionOID", help = "OID of the profession") + private val summary by option("--summary", "-s", help = "Summary mode").flag() + + private val customParams: Map by option( + "-p", + "--param", + help = "Specify query parameters to find matching entries", + metavar = "NAME=VALUE", + ).associate() + + override fun run() = + catching { + logger.info { "Searching HealthcareService resources in FHIR Directory ${context.ctx.env.name}" } + val query = SearchQuery(SearchResource.HealthcareService) + + listOf(includeOrganization, includeLocation, includeEndpoint).forEach { + if (it.isNotEmpty()) { + query.addParam("_include", it) + } + } + + query.addParam("organization.active", active.toString()) + + if (textArguments.isNotEmpty()) { + query.addParam("_text", textArguments.joinToString(" ")) + } + + if (telematikID != null) { + query.addParam("organization.identifier", "https://gematik.de/fhir/sid/telematik-id|$telematikID") + } + + if (professionOID != null) { + query.addParam("organization.type", "https://gematik.de/fhir/directory/CodeSystem/OrganizationProfessionOID|$professionOID") + } + + if (summary) { + query.addParam("_summary", "count") + } + + customParams.forEach { + query.addParam(it.key, it.value) + } + + val bundle = + runBlocking { + context.search(context.ctx, query) + } + + if (summary) { + echo("Total: ${bundle.total}") + } else { + echo(bundle.toStringOutput(outputFormat)) + } + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchPractitionerRoleCommand.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchPractitionerRoleCommand.kt new file mode 100644 index 00000000..e931bae2 --- /dev/null +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/fhir/SearchPractitionerRoleCommand.kt @@ -0,0 +1,105 @@ +package de.gematik.ti.directory.cli.fhir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.options.* +import de.gematik.ti.directory.cli.catching +import de.gematik.ti.directory.fhir.SearchQuery +import de.gematik.ti.directory.fhir.SearchResource +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging + +class SearchPractitionerRoleCommand : CliktCommand(name = "practitioner-role", help = "Search PractitionerRole resources (alias: pr)") { + private val logger = KotlinLogging.logger {} + private val context by requireObject() + + private val outputFormat by option().switch( + "--json" to OutputFormat.JSON, + "--json-ext" to OutputFormat.JSON_EXT, + "--yaml" to OutputFormat.YAML, + "--human" to OutputFormat.HUMAN, + "--table" to OutputFormat.TABLE, + ).default(OutputFormat.TABLE) + + private val active: Boolean by option("--active", "-a", help = "Filter by active status").flag(default = true) + + private val includePractitioner by option().switch( + "--include-practitioner" to "PractitionerRole:practitioner", + "--exclude-practitioner" to "", + ).default("PractitionerRole:practitioner") + + private val includeLocation by option().switch( + "--include-location" to "PractitionerRole:location", + "--exclude-location" to "", + ).default("PractitionerRole:location") + + private val includeEndpoint by option().switch( + "--include-endpoint" to "PractitionerRole:endpoint", + "--exclude-endpoint" to "", + ).default("PractitionerRole:endpoint") + + private val textArguments by argument("SEARCH_TEXT").multiple(required = false) + private val telematikID by option("--telematik-id", "-t", help = "Telematik-ID of the Practitioner") + private val professionOID by option("--professionOID", help = "OID of the profession") + private val summary by option("--summary", "-s", help = "Summary mode").flag() + + private val customParams: Map by option( + "-p", + "--param", + help = "Specify query parameters to find matching entries", + metavar = "NAME=VALUE", + ).associate() + + override fun run() = + catching { + logger.info { "Searching PractitionerRole resources in FHIR Directory ${context.ctx.env.name}" } + val query = SearchQuery(SearchResource.PractitionerRole) + + listOf(includePractitioner, includeLocation, includeEndpoint).forEach { + if (it.isNotEmpty()) { + query.addParam("_include", it) + } + } + customParams.forEach { + query.addParam(it.key, it.value) + } + + query.addParam("practitioner.active", active.toString()) + + if (textArguments.isNotEmpty()) { + query.addParam("_text", textArguments.joinToString(" ")) + } + + if (telematikID != null) { + query.addParam("practitioner.identifier", "https://gematik.de/fhir/sid/telematik-id|$telematikID") + } + + if (professionOID != null) { + query.addParam( + "practitioner.qualification", + "https://gematik.de/fhir/directory/CodeSystem/PractitionerProfessionOID|$professionOID", + ) + } + + if (summary) { + query.addParam("_summary", "count") + } + + customParams.forEach { + query.addParam(it.key, it.value) + } + + val bundle = + runBlocking { + context.search(context.ctx, query) + } + + if (summary) { + echo("Total: ${bundle.total}") + } else { + echo(bundle.toStringOutput(outputFormat)) + } + } +} diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/util/TokensStore.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/util/TokensStore.kt index 6b4c8366..15d0a8b7 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/util/TokensStore.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/util/TokensStore.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive import mu.KotlinLogging import java.nio.file.Path @@ -48,7 +49,11 @@ class TokenStore(customConfigPath: Path? = null) : FileObjectStore(tokenBody) return jsonObject.map { entry -> - Pair(entry.key, entry.value.jsonPrimitive.content) + if (entry.value is JsonPrimitive) { + Pair(entry.key, entry.value.jsonPrimitive.content) + } else { + Pair(entry.key, entry.value.toString()) + } }.toMap() } diff --git a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/util/Vault.kt b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/util/Vault.kt index 2ff5e5f5..a0c07c2e 100644 --- a/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/util/Vault.kt +++ b/vzd-cli/src/main/kotlin/de/gematik/ti/directory/cli/util/Vault.kt @@ -12,8 +12,6 @@ private val logger = KotlinLogging.logger {} data class Secret(var variant: String, var name: String, var secret: String) -private const val DEFAULT_SERVICE_NAME = "urn:gematik:directory:admin" - class VaultException(message: String) : Exception(message) class KeyStoreVaultProvider(val customVaultPath: Path? = null) { @@ -37,7 +35,7 @@ class KeyStoreVaultProvider(val customVaultPath: Path? = null) { fun open( password: String, - serviceName: String = DEFAULT_SERVICE_NAME, + serviceName: String, ): KeyStoreVault { return KeyStoreVault(password, vaultPath, serviceName) } diff --git a/vzd-cli/src/test/kotlin/de/gematik/ti/directory/admin/TestPaging.kt b/vzd-cli/src/test/kotlin/de/gematik/ti/directory/admin/TestPaging.kt index 74c3f850..ac1697c4 100644 --- a/vzd-cli/src/test/kotlin/de/gematik/ti/directory/admin/TestPaging.kt +++ b/vzd-cli/src/test/kotlin/de/gematik/ti/directory/admin/TestPaging.kt @@ -19,13 +19,13 @@ class TestPaging : FeatureSpec({ val withOutPaging = client?.readDirectoryEntryForSync( mapOf( - "telematikID" to "9-*", + "telematikID" to "9-2*", ), )?.size var withPaging = 0 client?.streamDirectoryEntriesPaging( mapOf( - "telematikID" to "9-*", + "telematikID" to "9-2*", ), 100, ) { diff --git a/vzd-cli/src/test/kotlin/de/gematik/ti/directory/admin/TestVault.kt b/vzd-cli/src/test/kotlin/de/gematik/ti/directory/admin/TestVault.kt index e306ba38..4062a89f 100644 --- a/vzd-cli/src/test/kotlin/de/gematik/ti/directory/admin/TestVault.kt +++ b/vzd-cli/src/test/kotlin/de/gematik/ti/directory/admin/TestVault.kt @@ -9,6 +9,7 @@ import kotlin.io.path.createTempDirectory import kotlin.io.path.deleteIfExists class TestVault : FeatureSpec({ + val testServiceName = "directory-vault-test" val vaultDir = createTempDirectory() val vaultPath = Path(vaultDir.toString(), "directory-vault-test.keystore") val badPassword = "BadPassword" @@ -25,38 +26,38 @@ class TestVault : FeatureSpec({ feature("Secrets verwalten") { scenario("Credentials KeyStore wird automatisch erzeugt") { vaultPath.toFile().exists() shouldBe false - val vault = KeyStoreVaultProvider(vaultPath).open(badPassword) + val vault = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault.store("ru", "test1", longSecret) vaultPath.toFile().exists() shouldBe true - val vault2 = KeyStoreVaultProvider(vaultPath).open(badPassword) + val vault2 = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault2.get("ru")?.secret shouldBe longSecret } scenario("Löschen von existierenden Secret") { - val vault = KeyStoreVaultProvider(vaultPath).open(badPassword) + val vault = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault.store("ru", "test-to-be-deleted", "secret") - val vault2 = KeyStoreVaultProvider(vaultPath).open(badPassword) + val vault2 = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault2.get("ru") shouldNotBe null vault2.delete("ru") vault2.get("ru") shouldBe null - val vault3 = KeyStoreVaultProvider(vaultPath).open(badPassword) + val vault3 = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault3.get("ru") shouldBe null } scenario("Credentials leeren") { - val vault = KeyStoreVaultProvider(vaultPath).open(badPassword) + val vault = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault.store("ru", "id1", "secret1") vault.store("tu", "id2", "secret2") vault.store("pu", "id2", "secret3") vault.get("ru") shouldNotBe null vault.get("tu") shouldNotBe null vault.get("pu") shouldNotBe null - KeyStoreVaultProvider(vaultPath).open(badPassword).clear() - val vault2 = KeyStoreVaultProvider(vaultPath).open(badPassword) + KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName).clear() + val vault2 = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault2.get("ru") shouldBe null vault2.get("tu") shouldBe null vault2.get("pu") shouldBe null } scenario("Credentials zurücksetzen") { - val vault = KeyStoreVaultProvider(vaultPath).open(badPassword) + val vault = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault.store("ru", "id1", "secret1") vault.store("tu", "id2", "secret2") vault.store("pu", "id2", "secret3") @@ -64,7 +65,7 @@ class TestVault : FeatureSpec({ vault.get("tu") shouldNotBe null vault.get("pu") shouldNotBe null KeyStoreVaultProvider(vaultPath).purge() - val vault2 = KeyStoreVaultProvider(vaultPath).open(badPassword) + val vault2 = KeyStoreVaultProvider(vaultPath).open(badPassword, testServiceName) vault2.get("ru") shouldBe null vault2.get("tu") shouldBe null vault2.get("pu") shouldBe null diff --git a/vzd-cli/startScriptTemplates/windowsStartScript.txt b/vzd-cli/startScriptTemplates/windowsStartScript.txt index 17cc892c..5466d217 100644 --- a/vzd-cli/startScriptTemplates/windowsStartScript.txt +++ b/vzd-cli/startScriptTemplates/windowsStartScript.txt @@ -65,8 +65,8 @@ set CMD_LINE_ARGS=%* @rem Setup the command line @rem check if updated jar is available and move it in place of the old jar -if exist ${classpath}.update ( - move ${classpath}.update $classpath >nul +if exist "${classpath}.update" ( + move "${classpath}.update" "$classpath" >nul ) set CLASSPATH=$classpath