From ae0d617cd9b9b8743d0616d7905e9cde101a5254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20B=C3=B6hm?= <97438546+BoehmDo@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:19:23 +0200 Subject: [PATCH] feat: enable passthrough urls and resources --- README.md | 15 ++++++- facade_app/api.py | 18 +++++++- facade_app/config/passthrough_config.yml | 8 ++++ .../resources/fhir_facade_passthrough.py | 23 ++++++++++ facade_app/resources/fhir_facade_server.py | 12 +++++ facade_app/resources/util/util_functions.py | 45 +++++++++++++++++++ 6 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 facade_app/config/passthrough_config.yml create mode 100644 facade_app/resources/fhir_facade_passthrough.py create mode 100644 facade_app/resources/util/util_functions.py diff --git a/README.md b/README.md index e1a88e9..16d7eb2 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ | BA_USER_NAME | | BasicAuth username if required for the connection to the fhir server | | | BA_PASSWORD | | BasicAuth password if required for the connection to the fhir server | | | RESOURCE_CONFIG | | Multiline yaml, analogue to the resource config file | | +| PASSTHROUGH_CONFIG | | Multiline yaml, analogue to the passthrough config file | | | PROVISION_CONFIG | | Multiline json, analogue to the provison config file | | | SSL_CERT | | Certificate for https | | | SSL_KEY | | Key for https | | @@ -77,7 +78,7 @@ Resources: ### Provision Configuration Provisions can be configured either before container startup in the config/general_provison_config.json or during runtime: Pass a List of provision codes in a json format based on the [MII Kerndatensatz](https://simplifier.net/packages/de.medizininformatikinitiative.kerndatensatz.consent/1.0.0-ballot1). -Every Consent is required to have ALL provided provisions as a subset of its provisions. This structure is required for the preconfiguration as well as the parameter version. +Every Patient is required to have ALL provided provisions as a subset of its provisions. This structure is required for the preconfiguration as well as the parameter version. #### Example: ``` @@ -94,3 +95,15 @@ Every Consent is required to have ALL provided provisions as a subset of its pro ] } ``` + +### Passthrough Configuration: +Some Applications might require you to additionally access proprietary endpoints in the underlying FHIR-Server. Similar to the previous configuration this can be achieved through a yaml configuration, that can be provided via environment variable, as well as the passthrough_config.yml file. You only need to provide the URL in a example route "fhir.server.url:portURL". If the url contains the usual /fhir/ base it has to be supplied under Resources, where "fhir.server.url:port/fhir/Resource". + +#### Example: +``` +URLs: + - "/metadata" + - "/test/echo" +Resources: + - "metadata" +``` \ No newline at end of file diff --git a/facade_app/api.py b/facade_app/api.py index 9f40ae5..c571756 100644 --- a/facade_app/api.py +++ b/facade_app/api.py @@ -1,10 +1,24 @@ +import os, yaml from flask import Flask from flask_restful import Api +from resources.fhir_facade_passthrough import FHIR_Facade_Passthrough from resources.fhir_facade_server import FHIR_Facade_Server +temp = os.getenv("PASSTHROUGH_CONFIG", "") +if temp != "": + pass_conf = yaml.safe_load(temp) +else: + with open("../config/passthrough_config.yml") as cfgfile: + pass_conf = yaml.safe_load(cfgfile.read()) + app = Flask(__name__) api = Api(app) # set up web server at FACADE_URL+FACADE_PORT -api.add_resource(FHIR_Facade_Server, '/fhir/', '/fhir//') - +api.add_resource( + FHIR_Facade_Server, + "/fhir/", + "/fhir//", +) +if len(pass_conf["URLs"]) > 0: + api.add_resource(FHIR_Facade_Passthrough, *pass_conf["URLs"]) diff --git a/facade_app/config/passthrough_config.yml b/facade_app/config/passthrough_config.yml new file mode 100644 index 0000000..9403de9 --- /dev/null +++ b/facade_app/config/passthrough_config.yml @@ -0,0 +1,8 @@ +# Add all endpoints and resources that are supposed to be passed through without consent checks +# Endpoints that use the same /fhir/ base url as resources have to be added in the resources section +# E.g. /fhir/metadata +URLs: + - "/metadata" + - "/test/echo" +Resources: + - "metadata" diff --git a/facade_app/resources/fhir_facade_passthrough.py b/facade_app/resources/fhir_facade_passthrough.py new file mode 100644 index 0000000..eb441f2 --- /dev/null +++ b/facade_app/resources/fhir_facade_passthrough.py @@ -0,0 +1,23 @@ +from resources.util.util_functions import get_passthrough_result +from flask import request, Response +from flask_restful import Resource + + +def passthrough_handle_request(self, is_post): + params = request.args.copy() + headers = dict(request.headers) + data = bytes(request.data) + + if is_post: + return get_passthrough_result(request.full_path, params, headers, data, True) + else: + return get_passthrough_result(request.full_path, params, headers, data, False) + + +class FHIR_Facade_Passthrough(Resource): + + def get(self): + return passthrough_handle_request(self, False) + + def post(self): + return passthrough_handle_request(self, True) diff --git a/facade_app/resources/fhir_facade_server.py b/facade_app/resources/fhir_facade_server.py index 74922f6..0a5f05e 100644 --- a/facade_app/resources/fhir_facade_server.py +++ b/facade_app/resources/fhir_facade_server.py @@ -1,6 +1,7 @@ import yaml, json, requests, os, math, time, multiprocessing, logging from requests.auth import HTTPBasicAuth from functools import partial +from resources.fhir_facade_passthrough import passthrough_handle_request import uuid, shortuuid import flask from flask import request, Response @@ -23,6 +24,14 @@ with open("../config/resource_config.yml") as cfgfile: resource_config = yaml.safe_load(cfgfile) +temp = os.getenv("PASSTHROUGH_CONFIG", "") +if temp != "": + pass_config = yaml.safe_load(temp) +else: + with open("../config/passthrough_config.yml") as cfgfile: + pass_config = yaml.safe_load(cfgfile) + +PASS_RESOURCES = pass_config["Resources"] RESOURCE_PATHS = resource_config["Resources"] LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") @@ -44,6 +53,9 @@ def handleRequest(self, resource, search=""): if resource == "Page" and "__page-id" in params: return getPage(params["__page-id"]) + if resource in PASS_RESOURCES: + return passthrough_handle_request(self, False) + # Return error code if request is invalid if search != "_search": return "Syntax error. Use the appropriate fhir-post-search syntax: .../fhir/type/_search" diff --git a/facade_app/resources/util/util_functions.py b/facade_app/resources/util/util_functions.py new file mode 100644 index 0000000..af9333a --- /dev/null +++ b/facade_app/resources/util/util_functions.py @@ -0,0 +1,45 @@ +import os, requests, json +from requests.auth import HTTPBasicAuth + + +def get_passthrough_result(url, params, headers, data, is_post): + SERVER_URL = os.getenv("FHIR_SERVER_URL", "") + + s = requests.sessions.Session() + auth = HTTPBasicAuth(os.getenv("BA_USER_NAME", ""), os.getenv("BA_PASSWORD", "")) + + # Merge params and jsondata + params["_format"] = "application/fhir+json" + try: + data = json.loads(data) + data.update(params) + except: + data = {} + data.update(params) + + headers.update( + { + "Accept": "application/fhir+json", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + + if is_post: + response = s.post( + SERVER_URL.replace("/fhir/", "") + url, + auth=auth, + headers=headers, + params=data, + verify=False, + ).json() + + else: + response = s.get( + SERVER_URL.replace("/fhir/", "") + url, + auth=auth, + headers=headers, + params=data, + verify=False, + ).json() + + return response