diff --git a/.env.example b/.env.example index 9932e55..dc8184e 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ MONGO_PASSWORD=nlpmongo SERVER_PROTOCOL=http:// SERVER_DOMAIN=localhost SERVER_PORT=8080 +SERVER_SECRET_KEY=changeme ## DB DB_PROTOCOL=mongodb:// diff --git a/README.md b/README.md index 72ccc06..c32e891 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,20 @@ Node using Docker (production mode) or the Python development server. - Using Docker: http://localhost/ui - Using Python: http://localhost:8080/ui +### Authentication + +A user who send requests to a data node instance must authenticate itself with +an API key. Below is an example on how to specify the API key in a `curl` +request: + +```console +curl -X GET "http://localhost:8080/api/v1/datasets?limit=10" -H "accept: application/json" -H "X-API-Key: changeme" +``` + +The API key used by the data node must be set at startup as the environment +variable `SERVER_SECRET_KEY`. The value of this environment variable can be set +in the configuration file `.env`. + ## Contributing Thinking about contributing to this project? Get started by reading our diff --git a/server/openapi_server/__main__.py b/server/openapi_server/__main__.py index 04a4b01..1b7e858 100644 --- a/server/openapi_server/__main__.py +++ b/server/openapi_server/__main__.py @@ -5,7 +5,7 @@ from mongoengine import connect from openapi_server import encoder -from openapi_server.config import Config as config +from openapi_server.config import config app = connexion.App(__name__, specification_dir='./openapi/') @@ -13,10 +13,10 @@ app.add_api('openapi.yaml', pythonic_params=True) connect( - db=config().db_database, - username=config().db_username, - password=config().db_password, - host=config().db_host + db=config.db_database, + username=config.db_username, + password=config.db_password, + host=config.db_host ) app.add_url_rule('/', 'root', lambda: flask.redirect('/api/v1/ui')) @@ -24,7 +24,7 @@ def main(): - app.run(port=config().server_port, debug=False) + app.run(port=config.server_port, debug=True) if __name__ == '__main__': diff --git a/server/openapi_server/config.py b/server/openapi_server/config.py index 87d7f21..c9809b0 100644 --- a/server/openapi_server/config.py +++ b/server/openapi_server/config.py @@ -1,10 +1,14 @@ import os -# from abc import abstractmethod +# import string +# import random + defaultValues = { "SERVER_PROTOCOL": "http://", "SERVER_DOMAIN": "localhost", "SERVER_PORT": "8080", + "SERVER_SECRET_KEY": "", + # "SERVER_SECRET_KEY": ''.join(random.sample(string.ascii_letters + string.digits, 32)), # noqa: E501 "DB_PROTOCOL": "mongodb://", "DB_DOMAIN": "localhost", "DB_PORT": "27017", @@ -25,11 +29,7 @@ def __init__(self): def get_property(self, property_name): if os.getenv(property_name) is not None: return os.getenv(property_name) - # we don't want KeyError? - if property_name not in self._defaultValues.keys(): - return None # No default value found - # return default value - return self._defaultValues[property_name] + return self._defaultValues.get(property_name) class Config(AbstractConfig): @@ -63,6 +63,10 @@ def server_api_url(self): base_path='/api/v1' ) + @property + def secret_key(self): + return self.get_property('SERVER_SECRET_KEY') + @property def db_protocol(self): return self.get_property('DB_PROTOCOL') @@ -91,3 +95,6 @@ def db_password(self): def db_host(self): return "%s%s:%s" % ( self.db_protocol, self.db_domain, self.db_port) + + +config = Config() diff --git a/server/openapi_server/controllers/annotation_controller.py b/server/openapi_server/controllers/annotation_controller.py index 26fc7e7..0004afe 100644 --- a/server/openapi_server/controllers/annotation_controller.py +++ b/server/openapi_server/controllers/annotation_controller.py @@ -9,7 +9,7 @@ from openapi_server.dbmodels.annotation_store import AnnotationStore as DbAnnotationStore # noqa: E501 from openapi_server.dbmodels.annotation import Annotation as DbAnnotation from openapi_server import util -from openapi_server.config import Config +from openapi_server.config import config def create_annotation(dataset_id, annotation_store_id, annotation_id): # noqa: E501 @@ -205,7 +205,7 @@ def list_annotations(dataset_id, annotation_store_id, limit=None, offset=None): next_ = ( "%s/datasets/%s/annotationStores/%s/annotations" "?limit=%s&offset=%s") % \ - (Config().server_api_url, dataset_id, annotation_store_id, limit, # noqa: E501 + (config.server_api_url, dataset_id, annotation_store_id, limit, # noqa: E501 offset + limit) res = PageOfAnnotations( offset=offset, diff --git a/server/openapi_server/controllers/annotation_store_controller.py b/server/openapi_server/controllers/annotation_store_controller.py index 22bd734..3686f0f 100644 --- a/server/openapi_server/controllers/annotation_store_controller.py +++ b/server/openapi_server/controllers/annotation_store_controller.py @@ -7,7 +7,7 @@ from openapi_server.dbmodels.annotation import Annotation as DbAnnotation # noqa: E501 from openapi_server.dbmodels.annotation_store import AnnotationStore as DbAnnotationStore # noqa: E501 from openapi_server.dbmodels.dataset import Dataset as DbDataset -from openapi_server.config import Config +from openapi_server.config import config def create_annotation_store(dataset_id, annotation_store_id): # noqa: E501 @@ -140,7 +140,7 @@ def list_annotation_stores(dataset_id, limit=None, offset=None): # noqa: E501 next_ = "" if len(annotation_stores) == limit: next_ = "%s/datasets/%s/annotationStores?limit=%s&offset=%s" % \ - (Config().server_api_url, dataset_id, limit, offset + limit) + (config.server_api_url, dataset_id, limit, offset + limit) res = PageOfAnnotationStores( offset=offset, limit=limit, diff --git a/server/openapi_server/controllers/dataset_controller.py b/server/openapi_server/controllers/dataset_controller.py index 58d530b..db40fdf 100644 --- a/server/openapi_server/controllers/dataset_controller.py +++ b/server/openapi_server/controllers/dataset_controller.py @@ -5,7 +5,7 @@ from openapi_server.models.dataset_create_response import DatasetCreateResponse # noqa: E501 from openapi_server.models.error import Error # noqa: E501 from openapi_server.models.page_of_datasets import PageOfDatasets # noqa: E501 -from openapi_server.config import Config +from openapi_server.config import config from openapi_server.controllers.annotation_store_controller import delete_annotation_store_by_name, list_annotation_stores # noqa: E501 from openapi_server.controllers.fhir_store_controller import delete_fhir_store_by_name, list_fhir_stores # noqa: E501 @@ -127,7 +127,7 @@ def list_datasets(limit=None, offset=None): # noqa: E501 next_ = "" if len(datasets) == limit: next_ = "%s/datasets?limit=%s&offset=%s" % \ - (Config().server_api_url, limit, offset + limit) + (config.server_api_url, limit, offset + limit) res = PageOfDatasets( offset=offset, limit=limit, diff --git a/server/openapi_server/controllers/fhir_store_controller.py b/server/openapi_server/controllers/fhir_store_controller.py index fdd4224..0f1bf45 100644 --- a/server/openapi_server/controllers/fhir_store_controller.py +++ b/server/openapi_server/controllers/fhir_store_controller.py @@ -8,7 +8,7 @@ from openapi_server.dbmodels.fhir_store import FhirStore as DbFhirStore from openapi_server.dbmodels.note import Note as DbNote from openapi_server.dbmodels.patient import Patient as DbPatient -from openapi_server.config import Config +from openapi_server.config import config def create_fhir_store(dataset_id, fhir_store_id): # noqa: E501 @@ -150,7 +150,7 @@ def list_fhir_stores(dataset_id, limit=None, offset=None): # noqa: E501 next_ = "" if len(fhir_stores) == limit: next_ = "%s/datasets/%s/FhirStores?limit=%s&offset=%s" % \ - (Config().server_api_url, dataset_id, limit, offset + limit) + (config.server_api_url, dataset_id, limit, offset + limit) res = PageOfFhirStores( offset=offset, limit=limit, diff --git a/server/openapi_server/controllers/note_controller.py b/server/openapi_server/controllers/note_controller.py index 1e80de7..d859367 100644 --- a/server/openapi_server/controllers/note_controller.py +++ b/server/openapi_server/controllers/note_controller.py @@ -9,7 +9,7 @@ from openapi_server.dbmodels.fhir_store import FhirStore as DbFhirStore from openapi_server.dbmodels.note import Note as DbNote from openapi_server.dbmodels.patient import Patient as DbPatient -from openapi_server.config import Config +from openapi_server.config import config def create_note(dataset_id, fhir_store_id, note_id): # noqa: E501 @@ -180,7 +180,7 @@ def list_notes(dataset_id, fhir_store_id, limit=None, offset=None): # noqa: E50 if len(notes) == limit: next_ = '{api_url}/{fhir_store_name}/fhir/Note?limit={limit}' \ '&offset={offset}'.format( - api_url=Config().server_api_url, + api_url=config.server_api_url, fhir_store_name=store_name, limit=limit, offset=offset + limit diff --git a/server/openapi_server/controllers/patient_controller.py b/server/openapi_server/controllers/patient_controller.py index 634fbd5..5ee3c62 100644 --- a/server/openapi_server/controllers/patient_controller.py +++ b/server/openapi_server/controllers/patient_controller.py @@ -8,7 +8,7 @@ from openapi_server.models.patient import Patient # noqa: E501 from openapi_server.models.patient_create_request import PatientCreateRequest # noqa: E501 from openapi_server.models.patient_create_response import PatientCreateResponse # noqa: E501 -from openapi_server.config import Config +from openapi_server.config import config from openapi_server.controllers.note_controller import delete_notes_by_patient # noqa: E501 @@ -154,7 +154,7 @@ def list_patients(dataset_id, fhir_store_id, limit=None, offset=None): # noqa: if len(patients) == limit: next_ = '{api_url}/{fhir_store_name}/fhir/Patient?limit={limit}' \ '&offset={offset}'.format( - api_url=Config().server_api_url, + api_url=config.server_api_url, fhir_store_name=store_name, limit=limit, offset=offset + limit diff --git a/server/openapi_server/controllers/security_controller_.py b/server/openapi_server/controllers/security_controller_.py index bc1adc3..28b847f 100644 --- a/server/openapi_server/controllers/security_controller_.py +++ b/server/openapi_server/controllers/security_controller_.py @@ -1 +1,26 @@ -# from typing import List +from openapi_server.config import config + + +def info_from_ApiKeyAuth(api_key, required_scopes): + """ + Check and retrieve authentication information from api_key. Returned value + will be passed in 'token_info' parameter of your operation function, if + there is one. 'sub' or 'uid' will be set in 'user' parameter of your + operation function, if there is one. + + :param api_key API key provided by Authorization header :type api_key: str + :param required_scopes Always None. Used for other authentication method + :type required_scopes: None :return: Information attached to provided + api_key or None if api_key is invalid or does not allow access to called API + :rtype: dict | None + """ + try: + # disable authentication if secret key is empty + # TODO the time required to evaluate the key value must be independent + # of the content of the api key defined by the server. + if not config.secret_key or api_key == config.secret_key: + return {'uid': 'user_id'} + except Exception as error: + print("Invalid API key", error) + + return None diff --git a/server/openapi_server/openapi/openapi.yaml b/server/openapi_server/openapi/openapi.yaml index 8522374..b4f09f3 100644 --- a/server/openapi_server/openapi/openapi.yaml +++ b/server/openapi_server/openapi/openapi.yaml @@ -86,6 +86,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Get all datasets tags: - Dataset @@ -133,6 +135,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Create a dataset tags: - Dataset @@ -170,6 +174,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Delete a dataset by ID tags: - Dataset @@ -206,6 +212,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Get a dataset by ID tags: - Dataset @@ -259,6 +267,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: List the annotation stores in a dataset tags: - AnnotationStore @@ -314,6 +324,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Create an annotation store tags: - AnnotationStore @@ -359,6 +371,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Delete an annotation store tags: - AnnotationStore @@ -403,6 +417,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Get an annotation store tags: - AnnotationStore @@ -464,6 +480,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: List the annotations in an annotation store tags: - Annotation @@ -527,6 +545,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Create an annotation tags: - Annotation @@ -580,6 +600,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Delete an annotation tags: - Annotation @@ -632,6 +654,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Get an annotation tags: - Annotation @@ -685,6 +709,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: List the FHIR stores in a dataset tags: - FhirStore @@ -740,6 +766,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Create a FHIR store tags: - FhirStore @@ -785,6 +813,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Delete a FHIR store tags: - FhirStore @@ -829,6 +859,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Get a FHIR store tags: - FhirStore @@ -890,6 +922,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: List notes tags: - Note @@ -953,6 +987,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Create a note tags: - Note @@ -1006,6 +1042,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Delete a note tags: - Note @@ -1058,6 +1096,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Get a note tags: - Note @@ -1119,6 +1159,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: List the Patients in a FHIR store tags: - Patient @@ -1182,6 +1224,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Create a FHIR patient tags: - Patient @@ -1235,6 +1279,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Delete a FHIR patient tags: - Patient @@ -1287,6 +1333,8 @@ paths: $ref: '#/components/schemas/Error' description: The request cannot be fulfilled due to an unexpected server error + security: + - ApiKeyAuth: [] summary: Get a FHIR patient tags: - Patient @@ -2252,3 +2300,9 @@ components: $ref: '#/components/schemas/Note' type: array type: object + securitySchemes: + ApiKeyAuth: + in: header + name: X-API-Key + type: apiKey + x-apikeyInfoFunc: openapi_server.controllers.security_controller_.info_from_ApiKeyAuth diff --git a/server/openapi_server/test/integration/test_annotation_controller.py b/server/openapi_server/test/integration/test_annotation_controller.py index dd6fc84..9de9b29 100644 --- a/server/openapi_server/test/integration/test_annotation_controller.py +++ b/server/openapi_server/test/integration/test_annotation_controller.py @@ -80,6 +80,7 @@ def test_create_annotation(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -104,6 +105,7 @@ def test_delete_annotation(self): 'awesome-dataset', 'awesome-annotation-store', 'awesome-annotation') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -125,6 +127,7 @@ def test_get_annotation(self): 'awesome-dataset', 'awesome-annotation-store', 'awesome-annotation') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -148,6 +151,7 @@ def test_list_annotations(self): ('offset', 0)] headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' diff --git a/server/openapi_server/test/integration/test_annotation_store_controller.py b/server/openapi_server/test/integration/test_annotation_store_controller.py index a76b282..1f83297 100644 --- a/server/openapi_server/test/integration/test_annotation_store_controller.py +++ b/server/openapi_server/test/integration/test_annotation_store_controller.py @@ -33,6 +33,7 @@ def test_create_annotation_store(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -55,6 +56,7 @@ def test_delete_annotation_store(self): 'awesome-dataset', 'awesome-annotation-store') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -75,6 +77,7 @@ def test_get_annotation_store(self): 'awesome-dataset', 'awesome-annotation-store') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -97,6 +100,7 @@ def test_list_annotation_stores(self): ('offset', 0)] headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' diff --git a/server/openapi_server/test/integration/test_dataset_controller.py b/server/openapi_server/test/integration/test_dataset_controller.py index 3219b46..e970aef 100644 --- a/server/openapi_server/test/integration/test_dataset_controller.py +++ b/server/openapi_server/test/integration/test_dataset_controller.py @@ -30,6 +30,7 @@ def test_create_dataset(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets', @@ -50,6 +51,7 @@ def test_delete_dataset(self): util.create_test_dataset("awesome-dataset") headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}'.format( @@ -68,6 +70,7 @@ def test_get_dataset(self): util.create_test_dataset("awesome-dataset") headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}'.format( @@ -88,6 +91,7 @@ def test_list_datasets(self): ('offset', 0)] headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets', diff --git a/server/openapi_server/test/integration/test_fhir_store_controller.py b/server/openapi_server/test/integration/test_fhir_store_controller.py index 7ee8361..290ecb2 100644 --- a/server/openapi_server/test/integration/test_fhir_store_controller.py +++ b/server/openapi_server/test/integration/test_fhir_store_controller.py @@ -33,6 +33,7 @@ def test_create_fhir_store(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores' @@ -54,6 +55,7 @@ def test_delete_fhir_store(self): util.create_test_fhir_store('awesome-dataset', 'awesome-fhir-store') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -73,6 +75,7 @@ def test_get_fhir_store(self): util.create_test_fhir_store('awesome-dataset', 'awesome-fhir-store') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -94,6 +97,7 @@ def test_list_fhir_stores(self): ('offset', 0)] headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores'.format( diff --git a/server/openapi_server/test/integration/test_note_controller.py b/server/openapi_server/test/integration/test_note_controller.py index b79bbd1..ed8c9e9 100644 --- a/server/openapi_server/test/integration/test_note_controller.py +++ b/server/openapi_server/test/integration/test_note_controller.py @@ -46,6 +46,7 @@ def test_create_note(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -70,6 +71,7 @@ def test_delete_note(self): 'awesome-dataset', 'awesome-fhir-store', 'awesome-note') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -91,6 +93,7 @@ def test_get_note(self): 'awesome-dataset', 'awesome-fhir-store', 'awesome-note') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -114,6 +117,7 @@ def test_list_notes(self): ('offset', 0)] headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' diff --git a/server/openapi_server/test/integration/test_patient_controller.py b/server/openapi_server/test/integration/test_patient_controller.py index c9c389f..2a89ded 100644 --- a/server/openapi_server/test/integration/test_patient_controller.py +++ b/server/openapi_server/test/integration/test_patient_controller.py @@ -38,6 +38,7 @@ def test_create_patient(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -62,6 +63,7 @@ def test_delete_patient(self): 'awesome-dataset', 'awesome-fhir-store', 'awesome-patient') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -83,6 +85,7 @@ def test_get_patient(self): 'awesome-dataset', 'awesome-fhir-store', 'awesome-patient') headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -106,6 +109,7 @@ def test_list_patients(self): ('offset', 0)] headers = { 'Accept': 'application/json', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}'