From 4ecd5166710f0b99368bc81d701c0f2b12a4831f Mon Sep 17 00:00:00 2001 From: tschaffter Date: Tue, 2 Nov 2021 08:15:15 -0700 Subject: [PATCH 1/7] Run openapi-generator to add security --- .../controllers/security_controller_.py | 20 ++++- server/openapi_server/openapi/openapi.yaml | 90 +++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/server/openapi_server/controllers/security_controller_.py b/server/openapi_server/controllers/security_controller_.py index bc1adc3..c95999a 100644 --- a/server/openapi_server/controllers/security_controller_.py +++ b/server/openapi_server/controllers/security_controller_.py @@ -1 +1,19 @@ -# from typing import List +from typing import List + + +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 + """ + return {'uid': 'user_id'} + + diff --git a/server/openapi_server/openapi/openapi.yaml b/server/openapi_server/openapi/openapi.yaml index 8522374..210888e 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 @@ -1403,6 +1451,12 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfDatasets_allOf' description: A page of datasets + required: + - datasets + - limit + - links + - offset + - totalResults type: object Error: description: Problem details (tools.ietf.org/html/rfc7807) @@ -1473,6 +1527,12 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfAnnotationStores_allOf' description: A page of annotation stores + required: + - annotationStores + - limit + - links + - offset + - totalResults type: object AnnotationStoreId: description: The ID of the annotation store @@ -1745,6 +1805,12 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfAnnotations_allOf' description: A page of annotations + required: + - annotations + - limit + - links + - offset + - totalResults type: object AnnotationId: description: The ID of the annotation @@ -1904,6 +1970,12 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfFhirStores_allOf' description: A page of FHIR stores + required: + - fhirStores + - limit + - links + - offset + - totalResults type: object FhirStoreCreateRequest: description: An empty object @@ -1957,6 +2029,12 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfPatients_allOf' description: A page of FHIR patients + required: + - limit + - links + - offset + - patients + - totalResults type: object PatientCreateRequest: description: A FHIR patient @@ -2036,6 +2114,12 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfNotes_allOf' description: A page of notes + required: + - limit + - links + - notes + - offset + - totalResults type: object NoteCreateRequest: description: A clinical note @@ -2252,3 +2336,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 From cb751dde583529222362141b930e33250c26b7be Mon Sep 17 00:00:00 2001 From: tschaffter Date: Tue, 2 Nov 2021 13:21:29 -0700 Subject: [PATCH 2/7] Implement API key authentication --- .env.example | 1 + README.md | 14 ++++++++ server/openapi_server/__main__.py | 14 ++++---- server/openapi_server/config.py | 19 ++++++---- .../controllers/annotation_controller.py | 4 +-- .../annotation_store_controller.py | 4 +-- .../controllers/dataset_controller.py | 4 +-- .../controllers/fhir_store_controller.py | 4 +-- .../controllers/note_controller.py | 4 +-- .../controllers/patient_controller.py | 4 +-- .../controllers/security_controller_.py | 25 +++++++------ server/openapi_server/openapi/openapi.yaml | 36 ------------------- 12 files changed, 63 insertions(+), 70 deletions(-) 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..173e014 100644 --- a/server/openapi_server/__main__.py +++ b/server/openapi_server/__main__.py @@ -5,18 +5,20 @@ 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/') app.app.json_encoder = encoder.JSONEncoder app.add_api('openapi.yaml', pythonic_params=True) +print(f'Server secret key: {config.secret_key}') + 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 +26,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 c95999a..35b8a19 100644 --- a/server/openapi_server/controllers/security_controller_.py +++ b/server/openapi_server/controllers/security_controller_.py @@ -1,19 +1,24 @@ -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. + 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 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 + :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 """ - return {'uid': 'user_id'} - + try: + # disable authentication if secret key is empty + 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 210888e..b4f09f3 100644 --- a/server/openapi_server/openapi/openapi.yaml +++ b/server/openapi_server/openapi/openapi.yaml @@ -1451,12 +1451,6 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfDatasets_allOf' description: A page of datasets - required: - - datasets - - limit - - links - - offset - - totalResults type: object Error: description: Problem details (tools.ietf.org/html/rfc7807) @@ -1527,12 +1521,6 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfAnnotationStores_allOf' description: A page of annotation stores - required: - - annotationStores - - limit - - links - - offset - - totalResults type: object AnnotationStoreId: description: The ID of the annotation store @@ -1805,12 +1793,6 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfAnnotations_allOf' description: A page of annotations - required: - - annotations - - limit - - links - - offset - - totalResults type: object AnnotationId: description: The ID of the annotation @@ -1970,12 +1952,6 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfFhirStores_allOf' description: A page of FHIR stores - required: - - fhirStores - - limit - - links - - offset - - totalResults type: object FhirStoreCreateRequest: description: An empty object @@ -2029,12 +2005,6 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfPatients_allOf' description: A page of FHIR patients - required: - - limit - - links - - offset - - patients - - totalResults type: object PatientCreateRequest: description: A FHIR patient @@ -2114,12 +2084,6 @@ components: - $ref: '#/components/schemas/ResponsePageMetadata' - $ref: '#/components/schemas/PageOfNotes_allOf' description: A page of notes - required: - - limit - - links - - notes - - offset - - totalResults type: object NoteCreateRequest: description: A clinical note From cff5271421b51869e0f252db728b27f560324e31 Mon Sep 17 00:00:00 2001 From: tschaffter Date: Tue, 2 Nov 2021 13:39:44 -0700 Subject: [PATCH 3/7] No longer printing the API key to stdout --- server/openapi_server/__main__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/openapi_server/__main__.py b/server/openapi_server/__main__.py index 173e014..1b7e858 100644 --- a/server/openapi_server/__main__.py +++ b/server/openapi_server/__main__.py @@ -12,8 +12,6 @@ app.app.json_encoder = encoder.JSONEncoder app.add_api('openapi.yaml', pythonic_params=True) -print(f'Server secret key: {config.secret_key}') - connect( db=config.db_database, username=config.db_username, From 8b2a02db588ab96039841e806581f0ba5f8936c3 Mon Sep 17 00:00:00 2001 From: tschaffter Date: Tue, 2 Nov 2021 13:53:59 -0700 Subject: [PATCH 4/7] Add ApiKeyAuth to headers in integration tests --- .../test/integration/test_annotation_controller.py | 4 ++++ .../test/integration/test_annotation_store_controller.py | 4 ++++ .../test/integration/test_dataset_controller.py | 4 ++++ .../test/integration/test_fhir_store_controller.py | 4 ++++ .../test/integration/test_health_check_controller.py | 1 + .../openapi_server/test/integration/test_note_controller.py | 4 ++++ .../test/integration/test_patient_controller.py | 4 ++++ 7 files changed, 25 insertions(+) diff --git a/server/openapi_server/test/integration/test_annotation_controller.py b/server/openapi_server/test/integration/test_annotation_controller.py index dd6fc84..8cb2fbb 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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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..ba91c9d 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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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..0d379e3 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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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..cfc087f 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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores'.format( diff --git a/server/openapi_server/test/integration/test_health_check_controller.py b/server/openapi_server/test/integration/test_health_check_controller.py index da298b0..dde53e3 100644 --- a/server/openapi_server/test/integration/test_health_check_controller.py +++ b/server/openapi_server/test/integration/test_health_check_controller.py @@ -16,6 +16,7 @@ def test_get_health_check(self): """ headers = { 'Accept': 'application/json', + 'ApiKeyAuth': 'special-key', } response = self.client.open( '/api/v1/healthCheck', diff --git a/server/openapi_server/test/integration/test_note_controller.py b/server/openapi_server/test/integration/test_note_controller.py index b79bbd1..7846b85 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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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..63d7c3c 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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': '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', + 'ApiKeyAuth': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' From ad34c1cc170249155bd55eb17c295be65f4f9ad2 Mon Sep 17 00:00:00 2001 From: tschaffter Date: Tue, 2 Nov 2021 14:08:06 -0700 Subject: [PATCH 5/7] Remove API key frfrom header of healthcheck in tests --- .../test/integration/test_health_check_controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server/openapi_server/test/integration/test_health_check_controller.py b/server/openapi_server/test/integration/test_health_check_controller.py index dde53e3..da298b0 100644 --- a/server/openapi_server/test/integration/test_health_check_controller.py +++ b/server/openapi_server/test/integration/test_health_check_controller.py @@ -16,7 +16,6 @@ def test_get_health_check(self): """ headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', } response = self.client.open( '/api/v1/healthCheck', From 9e61d6b275aacc45ff91287a4bf9c36d00681100 Mon Sep 17 00:00:00 2001 From: tschaffter Date: Tue, 2 Nov 2021 14:16:12 -0700 Subject: [PATCH 6/7] API key header param is X-API-Key, not ApiKeyAuth --- .../test/integration/test_annotation_controller.py | 8 ++++---- .../test/integration/test_annotation_store_controller.py | 8 ++++---- .../test/integration/test_dataset_controller.py | 8 ++++---- .../test/integration/test_fhir_store_controller.py | 8 ++++---- .../test/integration/test_note_controller.py | 8 ++++---- .../test/integration/test_patient_controller.py | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/server/openapi_server/test/integration/test_annotation_controller.py b/server/openapi_server/test/integration/test_annotation_controller.py index 8cb2fbb..9de9b29 100644 --- a/server/openapi_server/test/integration/test_annotation_controller.py +++ b/server/openapi_server/test/integration/test_annotation_controller.py @@ -80,7 +80,7 @@ def test_create_annotation(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -105,7 +105,7 @@ def test_delete_annotation(self): 'awesome-dataset', 'awesome-annotation-store', 'awesome-annotation') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -127,7 +127,7 @@ def test_get_annotation(self): 'awesome-dataset', 'awesome-annotation-store', 'awesome-annotation') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -151,7 +151,7 @@ def test_list_annotations(self): ('offset', 0)] headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + '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 ba91c9d..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,7 +33,7 @@ def test_create_annotation_store(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -56,7 +56,7 @@ def test_delete_annotation_store(self): 'awesome-dataset', 'awesome-annotation-store') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -77,7 +77,7 @@ def test_get_annotation_store(self): 'awesome-dataset', 'awesome-annotation-store') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/annotationStores' @@ -100,7 +100,7 @@ def test_list_annotation_stores(self): ('offset', 0)] headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + '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 0d379e3..e970aef 100644 --- a/server/openapi_server/test/integration/test_dataset_controller.py +++ b/server/openapi_server/test/integration/test_dataset_controller.py @@ -30,7 +30,7 @@ def test_create_dataset(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets', @@ -51,7 +51,7 @@ def test_delete_dataset(self): util.create_test_dataset("awesome-dataset") headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}'.format( @@ -70,7 +70,7 @@ def test_get_dataset(self): util.create_test_dataset("awesome-dataset") headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}'.format( @@ -91,7 +91,7 @@ def test_list_datasets(self): ('offset', 0)] headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + '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 cfc087f..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,7 +33,7 @@ def test_create_fhir_store(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores' @@ -55,7 +55,7 @@ def test_delete_fhir_store(self): util.create_test_fhir_store('awesome-dataset', 'awesome-fhir-store') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -75,7 +75,7 @@ def test_get_fhir_store(self): util.create_test_fhir_store('awesome-dataset', 'awesome-fhir-store') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -97,7 +97,7 @@ def test_list_fhir_stores(self): ('offset', 0)] headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + '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 7846b85..ed8c9e9 100644 --- a/server/openapi_server/test/integration/test_note_controller.py +++ b/server/openapi_server/test/integration/test_note_controller.py @@ -46,7 +46,7 @@ def test_create_note(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -71,7 +71,7 @@ def test_delete_note(self): 'awesome-dataset', 'awesome-fhir-store', 'awesome-note') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -93,7 +93,7 @@ def test_get_note(self): 'awesome-dataset', 'awesome-fhir-store', 'awesome-note') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -117,7 +117,7 @@ def test_list_notes(self): ('offset', 0)] headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + '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 63d7c3c..2a89ded 100644 --- a/server/openapi_server/test/integration/test_patient_controller.py +++ b/server/openapi_server/test/integration/test_patient_controller.py @@ -38,7 +38,7 @@ def test_create_patient(self): headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -63,7 +63,7 @@ def test_delete_patient(self): 'awesome-dataset', 'awesome-fhir-store', 'awesome-patient') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -85,7 +85,7 @@ def test_get_patient(self): 'awesome-dataset', 'awesome-fhir-store', 'awesome-patient') headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' @@ -109,7 +109,7 @@ def test_list_patients(self): ('offset', 0)] headers = { 'Accept': 'application/json', - 'ApiKeyAuth': 'special-key', + 'X-API-Key': 'special-key', } response = self.client.open( '/api/v1/datasets/{dataset_id}/fhirStores/{fhir_store_id}' From 679d4de86ce36bf7ff3541137b698f16035c379f Mon Sep 17 00:00:00 2001 From: tschaffter Date: Tue, 2 Nov 2021 14:25:44 -0700 Subject: [PATCH 7/7] Add note for security improvement --- server/openapi_server/controllers/security_controller_.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/openapi_server/controllers/security_controller_.py b/server/openapi_server/controllers/security_controller_.py index 35b8a19..28b847f 100644 --- a/server/openapi_server/controllers/security_controller_.py +++ b/server/openapi_server/controllers/security_controller_.py @@ -16,6 +16,8 @@ def info_from_ApiKeyAuth(api_key, required_scopes): """ 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: