From 6c5c0f90260c82a407b6fc23c35a2947e134e926 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 10 May 2022 12:47:28 -0700 Subject: [PATCH 1/3] add check for site_admin to drs POST operations --- htsget_server/authz.py | 64 +++++++++++++++++++-------------- htsget_server/config.py | 3 ++ htsget_server/drs_openapi.yaml | 16 +++------ htsget_server/drs_operations.py | 9 +++++ 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/htsget_server/authz.py b/htsget_server/authz.py index cd962f50..6637dd5b 100644 --- a/htsget_server/authz.py +++ b/htsget_server/authz.py @@ -1,7 +1,7 @@ import requests import json import os -from config import AUTHZ, TEST_KEY +from config import AUTHZ, TEST_KEY, CANDIG_OPA_SITE_ADMIN_KEY from flask import Flask import drs_operations @@ -19,7 +19,8 @@ def is_authed(id_, request): app.logger.warning("WARNING: TEST MODE, AUTHORIZATION IS DISABLED") return 200 # no auth if "Authorization" in request.headers: - authed_datasets = get_opa_res(request.headers, request.path, request.method) + token = get_auth_token(request.headers) + authed_datasets = get_opa_datasets(token, request.path, request.method) obj, code2 = drs_operations.get_object(id_) if code2 == 200: for dataset in obj["datasets"]: @@ -35,7 +36,36 @@ def is_authed(id_, request): return 403 -def get_opa_token_from_request(headers): +def is_site_admin(request): + """ + Is the user associated with the token a site admin? + """ + if AUTHZ["CANDIG_AUTHORIZATION"] != "OPA": + print("WARNING: AUTHORIZATION IS DISABLED") + app.logger.warning("WARNING: AUTHORIZATION IS DISABLED") + return True # no auth + if request.headers.get("Test_Key") == TEST_KEY: + print("WARNING: TEST MODE, AUTHORIZATION IS DISABLED") + app.logger.warning("WARNING: TEST MODE, AUTHORIZATION IS DISABLED") + return True # no auth + if "Authorization" in request.headers: + token = get_auth_token(request.headers) + response = requests.post( + AUTHZ['CANDIG_OPA_URL'] + "/v1/data/idp/" + CANDIG_OPA_SITE_ADMIN_KEY, + headers={"Authorization": f"Bearer {AUTHZ['CANDIG_OPA_SECRET']}"}, + json={ + "input": { + "token": token + } + } + ) + response.raise_for_status() + if 'result' in response.json(): + return True + return False + + +def get_auth_token(headers): """ Extracts token from request's header Authorization """ @@ -45,42 +75,24 @@ def get_opa_token_from_request(headers): return token.split()[1] -def get_request_body(headers, path, method): +def get_opa_datasets(token, path, method): """ - Returns request body required to query OPA + Get allowed dataset result from OPA """ - return { + body = { "input": { - "token": get_opa_token_from_request(headers), + "token": token, "body": { "path": path, "method": method } } } - - -def get_opa_res(headers, path, method): - """ - Get allowed dataset result from OPA - """ response = requests.post( AUTHZ['CANDIG_OPA_URL'] + "/v1/data/permissions/datasets", headers={"Authorization": f"Bearer {AUTHZ['CANDIG_OPA_SECRET']}"}, - json=get_request_body(headers, path, method) + json=body ) response.raise_for_status() allowed_datasets = response.json()["result"] return allowed_datasets - - -def is_site_admin(headers): - response = requests.post( - AUTHZ['CANDIG_OPA_URL'] + "/v1/data/idp/trusted_researcher", - headers={"Authorization": f"Bearer {AUTHZ['CANDIG_OPA_SECRET']}"}, - json=get_request_body(headers, "", "") - ) - response.raise_for_status() - if response.json()['result'] == 'true': - return True - return False diff --git a/htsget_server/config.py b/htsget_server/config.py index 077291a2..09789b07 100644 --- a/htsget_server/config.py +++ b/htsget_server/config.py @@ -6,6 +6,9 @@ config.read('./config.ini') AUTHZ = config['authz'] +CANDIG_OPA_SITE_ADMIN_KEY = os.environ.get("CANDIG_OPA_SITE_ADMIN_KEY") +if CANDIG_OPA_SITE_ADMIN_KEY is None: + CANDIG_OPA_SITE_ADMIN_KEY = "site_admin" DB_PATH = config['paths']['DBPath'] LOCAL_FILE_PATH = config['paths']['LocalFilesPath'] diff --git a/htsget_server/drs_openapi.yaml b/htsget_server/drs_openapi.yaml index 840818ad..cebc35ab 100644 --- a/htsget_server/drs_openapi.yaml +++ b/htsget_server/drs_openapi.yaml @@ -26,13 +26,13 @@ paths: schema: type: object /objects: + parameters: + - $ref: '#/components/parameters/BearerAuth' get: summary: List all DRS objects description: >- Returns object metadata, and a list of access methods that can be used to fetch object bytes. operationId: drs_operations.list_objects - parameters: - - $ref: '#/components/parameters/BearerAuth' responses: 200: description: The `DrsObject` was found successfully @@ -50,8 +50,6 @@ paths: post: summary: Add a DrsObject to the db operationId: drs_operations.post_object - parameters: - - $ref: '#/components/parameters/BearerAuth' requestBody: $ref: '#/components/requestBodies/DrsObjectRequest' responses: @@ -83,7 +81,6 @@ paths: Returns object metadata, and a list of access methods that can be used to fetch object bytes. operationId: drs_operations.get_object parameters: - - $ref: '#/components/parameters/BearerAuth' - $ref: '#/components/parameters/Expand' responses: 200: @@ -125,11 +122,11 @@ paths: tags: - Objects /datasets: + parameters: + - $ref: '#/components/parameters/BearerAuth' get: description: List all datasets available operationId: drs_operations.list_datasets - parameters: - - $ref: '#/components/parameters/BearerAuth' responses: 200: description: ok @@ -143,8 +140,6 @@ paths: post: description: Create a dataset operationId: drs_operations.post_dataset - parameters: - - $ref: '#/components/parameters/BearerAuth' requestBody: $ref: '#/components/requestBodies/DatasetRequest' responses: @@ -158,11 +153,10 @@ paths: /datasets/{dataset_id}: parameters: - $ref: "#/components/parameters/DatasetId" + - $ref: '#/components/parameters/BearerAuth' get: description: Describe a dataset operationId: drs_operations.get_dataset - parameters: - - $ref: '#/components/parameters/BearerAuth' responses: 200: description: ok diff --git a/htsget_server/drs_operations.py b/htsget_server/drs_operations.py index c2afa608..0c4a9211 100644 --- a/htsget_server/drs_operations.py +++ b/htsget_server/drs_operations.py @@ -5,6 +5,7 @@ from config import LOCAL_FILE_PATH, get_minio_client from flask import request import os +import authz # API endpoints def get_service_info(): @@ -47,6 +48,8 @@ def get_access_url(object_id, access_id): def post_object(): + if not authz.is_site_admin(request): + return {"message": "User is not authorized to POST"}, 403 client, bucket = get_minio_client() new_object = database.create_drs_object(connexion.request.json) if "access_methods" in new_object: @@ -71,6 +74,8 @@ def post_object(): def delete_object(object_id): + if not authz.is_site_admin(request): + return {"message": "User is not authorized to POST"}, 403 try: new_object = database.delete_drs_object(object_id) return new_object, 200 @@ -84,6 +89,8 @@ def list_datasets(): def post_dataset(): + if not authz.is_site_admin(request): + return {"message": "User is not authorized to POST"}, 403 new_dataset = database.create_dataset(connexion.request.json) return new_dataset, 200 @@ -96,6 +103,8 @@ def get_dataset(dataset_id): def delete_dataset(dataset_id): + if not authz.is_site_admin(request): + return {"message": "User is not authorized to POST"}, 403 try: new_dataset = database.delete_dataset(dataset_id) return new_dataset, 200 From 613214b7b31afc62f5d9473f2a46ec6d95287a3a Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 10 May 2022 21:06:16 -0700 Subject: [PATCH 2/3] remove setup.cfg/.py --- Dockerfile | 2 +- setup.cfg | 25 ------------------------- 2 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 setup.cfg diff --git a/Dockerfile b/Dockerfile index cccf9376..a7f00761 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,7 +45,7 @@ RUN sed -i s@\@${opa_secret}@ config.ini \ && sed -i s@\@${minio_bucket_name}@ config.ini RUN touch initial_setup -RUN python setup.py install && pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt RUN sqlite3 data/files.db -init data/files.sql diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 11883a59..00000000 --- a/setup.cfg +++ /dev/null @@ -1,25 +0,0 @@ -[metadata] -name = htsget_app -version = attr: 0.1.4 -url = https://github.com/CanDIG/htsget_app -description = CanDIG htsget API that follows the htsget standard -long_description = file: README.rst -keywords = htsget_app -classifiers = - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Programming Language :: Python :: 3.7', -[options] -zip_safe = False -include_package_data = True -packages = find: -install_requires = - Flask - Flask-Cors - minio - pysam - sqlalchemy - connexion -tests_require = - pytest \ No newline at end of file From 5ca0b343ee1f103ed12c9ef4f82784229f6300ef Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Wed, 11 May 2022 11:20:14 -0700 Subject: [PATCH 3/3] Update Dockerfile --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a7f00761..1f662187 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,8 +44,7 @@ RUN sed -i s@\@${opa_secret}@ config.ini \ && sed -i s@\@${minio_url}@ config.ini \ && sed -i s@\@${minio_bucket_name}@ config.ini -RUN touch initial_setup -RUN pip install --no-cache-dir -r requirements.txt +RUN touch initial_setup && pip install --no-cache-dir -r requirements.txt RUN sqlite3 data/files.db -init data/files.sql