diff --git a/beacon/request/handlers.py b/beacon/request/handlers.py index 940ea124..f4893330 100644 --- a/beacon/request/handlers.py +++ b/beacon/request/handlers.py @@ -75,7 +75,7 @@ async def wrapper(request:Request): LOG.debug(f"Query Params = {qparams}") - authenticated=False + LOG.debug(f"Headers = {request.headers}") access_token_header = request.headers.get('Authorization') access_token_cookies = request.cookies.get("Authorization") @@ -145,7 +145,7 @@ async def wrapper(request:Request): # get response of permissions server accessible_datasets:List[str] = [] # array of dataset ids - accessible_datasets, authenticated = await task_permissions + accessible_datasets = await task_permissions # get the max authorized granularity requested_granularity = qparams.query.requested_granularity diff --git a/beacon/request/model.py b/beacon/request/model.py index d9575874..9572495f 100644 --- a/beacon/request/model.py +++ b/beacon/request/model.py @@ -124,12 +124,12 @@ def summary(self): # convert filters to list of strings if self.query.filters: # filters in POST (json) - LOG.debug(f"query Filters = {self.query.filters}") + #LOG.debug(f"query Filters = {self.query.filters}") filters_dict = self.query.filters filters = [] for filter in filters_dict: - LOG.debug(f"Filter type = {type(filter)}") + #LOG.debug(f"Filter type = {type(filter)}") filter_str = filter_to_str(filter) filters.append(filter_str) @@ -137,13 +137,13 @@ def summary(self): else: # filters in URL (e.g. ?filters=NCIT:C20197,NCIT:C16576) filters = [] filters_req = self.query.request_parameters.get("filters", []) - LOG.debug(f"req Filters = {filters_req}") + #LOG.debug(f"req Filters = {filters_req}") if isinstance(filters_req, str): filters = list(filters_req.split(",")) else: filters = filters_req - LOG.debug(f"Filter summary = {filters}") + #LOG.debug(f"Filter summary = {filters}") return { "apiVersion": self.meta.api_version, diff --git a/beacon/utils/auth.py b/beacon/utils/auth.py index 37d54c45..5320f3aa 100644 --- a/beacon/utils/auth.py +++ b/beacon/utils/auth.py @@ -13,6 +13,7 @@ LOG = logging.getLogger(__name__) async def resolve_token(token, requested_datasets_ids): + raise KeyError("This function should not be used anymore") # If the user is not authenticated (ie no token) # we pass (requested_datasets, False) to the database function: it will filter out the datasets list, with the public ones if token is None: @@ -55,11 +56,12 @@ async def resolve_token(token, requested_datasets_ids): # async job to request permissions server for # specifically authorized datasets from user and handle authentication -async def request_permissions(token) -> Tuple[List[str], bool]: +# returns [accessible_datasets, is_authenticated, is_registered] +async def request_permissions(token) -> Tuple[List[str], bool, bool]: if token is None: LOG.debug("Token is none") - return [], False + return [], False, False LOG.debug("About to ask permissions server...") # The permissions server will: @@ -77,9 +79,10 @@ async def request_permissions(token) -> Tuple[List[str], bool]: LOG.error('Permissions server error %d', resp.status) error = await resp.text() LOG.error('Error: %s', error) - return [], False + return [], False, False #raise web.HTTPUnauthorized(body=error) + """ content = await resp.content.read() authorized_datasets = content.decode('utf-8') LOG.debug(f"authorized_datasets decoded = {authorized_datasets}") @@ -90,15 +93,23 @@ async def request_permissions(token) -> Tuple[List[str], bool]: if '[' not in auth_dataset: if ']' not in auth_dataset: auth_datasets.append(auth_dataset) - + """ + + try: + content = await resp.json() + auth_datasets = content["datasets"] + is_registered = content["is_registered"] + except Exception as e: + LOG.error(f"Error while getting results from permission server: {e}") + return [], False, False + LOG.debug(auth_datasets) - return auth_datasets, True + return auth_datasets, True, is_registered -# returns (datasets, authenticated) # returns datasets that are accessible by user # TODO if requested_datasets is given, filters them by perms # otherwise returns all accessible -async def get_accessible_datasets(token, requested_datasets=None) -> Tuple[List[str], bool]: +async def get_accessible_datasets(token, requested_datasets=None) -> List[str]: accessible_datasets:List[str] = [] @@ -120,16 +131,20 @@ async def get_accessible_datasets(token, requested_datasets=None) -> Tuple[List[ LOG.debug(f"registered datasets = {registered_datasets}") # get the result from task - specific_datasets, authenticated = await task_permissions + specific_datasets, is_authenticated, is_registered = await task_permissions LOG.info(f"User specific datasets = {specific_datasets}") # Not authenticated, just give access to public datasets - if not authenticated: - return accessible_datasets, False + if not is_authenticated: + return accessible_datasets + + # authenticated but not researcher status, give access to public and specific + if not is_registered: + return accessible_datasets + specific_datasets - # authenticated, give access to registered and user-specific datasets + # authenticated and registered, give access to everything (add registered and user-specific datasets) accessible_datasets += registered_datasets + specific_datasets - return accessible_datasets, True + return accessible_datasets diff --git a/deploy/conf.py b/deploy/conf.py index 02abba3c..bd23f6cf 100644 --- a/deploy/conf.py +++ b/deploy/conf.py @@ -36,7 +36,7 @@ # Project info # description = (r"Portuguese Beacon hosted at BioData.pt containing data from a Portuguese " - r"cohort of stage II and III colorectal cancer patients. \n" + r"cohort of stage II and III colorectal cancer patients. " r"Study available at: https://www.nature.com/articles/s41525-021-00177-w") version = 'v2.0' welcome_url = 'https://beacon.biodata.pt/' diff --git a/permissions/auth.py b/permissions/auth.py index c4c45589..bdaac0d0 100644 --- a/permissions/auth.py +++ b/permissions/auth.py @@ -18,6 +18,7 @@ from aiohttp import web from permissions.db import search_token +from permissions.tokens import verify_registered LOG = logging.getLogger(__name__) @@ -36,7 +37,7 @@ idp_token_url = 'https://login.elixir-czech.org/oidc/token' ALLOWED_LOCATIONS = config('BEACON_DOMAINS', cast=lambda v: [s.strip() for s in v.split(',')]) -SCOPES = set(["openid", "email", "profile", "country"]) +SCOPES = set(["openid", "email", "profile", "country", "ga4gh_passport_v1"]) # REMS REMS_URL = config('REMS_URL') @@ -111,10 +112,12 @@ async def decorated(request): user_info = token_doc["user_info"] username = user_info.get("preferred_username") user_id = user_info.get('sub') + passport = user_info.get('ga4gh_passport_v1', []) + is_registered = verify_registered(passport, user_id) LOG.debug('username: %s', username) LOG.debug("ELIXIR_ID: %s", user_id) - return await func(request, user_id) + return await func(request, user_id, is_registered) return decorated diff --git a/permissions/handlers.py b/permissions/handlers.py index bb28f318..fc3748d0 100644 --- a/permissions/handlers.py +++ b/permissions/handlers.py @@ -28,9 +28,10 @@ STATE_DEFAULT = b64encode( ("https://"+ALLOWED_LOCATIONS[0]+"/api/").encode("ascii") ).decode("ascii") @bearer_required -# token is already verified against the username with bearer_required +# token and registered status are already verified against the username with bearer_required # this function gets the datasets specifically authorized for this user -async def permission(request: Request, username: Optional[str]): +# returns (specific_datasets, is_registered) in JSON +async def permission(request: Request, username: Optional[str], is_registered): if request.headers.get('Content-Type') == 'application/json': post_data = await request.json() @@ -51,8 +52,12 @@ async def permission(request: Request, username: Optional[str]): LOG.debug('requested datasets: %s', requested_datasets) datasets = await request.app['permissions'].get(username, requested_datasets=requested_datasets) LOG.debug('selected datasets: %s', datasets) + + response = {"datasets": list(datasets or []), + "is_registered": is_registered + } - return web.json_response(list(datasets or [])) # cuz python-json doesn't like sets + return web.json_response(response) # cuz python-json doesn't like sets # Redirect to the login URI async def login_redirect(request: Request): diff --git a/permissions/plugins.py b/permissions/plugins.py index 50acb745..46ffdfe3 100644 --- a/permissions/plugins.py +++ b/permissions/plugins.py @@ -63,9 +63,9 @@ async def get(self, username, requested_datasets=None): datasets = set(self.db.get(username)) if requested_datasets: - return set(requested_datasets).intersection(datasets) + return set(requested_datasets).intersection(datasets), True else: - return datasets + return datasets, True async def close(self): pass @@ -152,15 +152,17 @@ async def get(self, username, requested_datasets=None): # get info from passport try: visas = response["ga4gh_passport_v1"] + for visa in visas: visa_decoded = parse_visa(visa, username) resource_id = visa_decoded["ga4gh_visa_v1"]["value"] - # check if it is a beacon dataset + # check if it is a beacon dataset, if so add dataset to result if resource_id.startswith(REMS_BEACON_RESOURCE_PREFIX): dataset_id = resource_id.split(REMS_BEACON_RESOURCE_PREFIX)[1] result_datasets.append(dataset_id) + except Exception as e: LOG.error(f"Error while parsing passport from REMS:{str(e)}\n") diff --git a/permissions/tokens.py b/permissions/tokens.py index 5957784e..a7c5b982 100644 --- a/permissions/tokens.py +++ b/permissions/tokens.py @@ -1,5 +1,6 @@ import jwt import time +from typing import * from aiohttp import web from os import getenv @@ -13,7 +14,7 @@ LOG = logging.getLogger(__name__) # returns (isOk, exp_date, max_age) -def verify_access_token(access_token) -> tuple[bool, int, int]: +def verify_access_token(access_token) -> Tuple[bool, int, int]: now = int(time.time()) @@ -61,7 +62,7 @@ def verify_access_token(access_token) -> tuple[bool, int, int]: def parse_visa(visa, user_id): try: - payload = jwt.decode(visa, options={"verify_signature": False}) + payload = jwt.decode_jwt(visa) if payload['exp'] < time.time(): raise web.HTTPUnauthorized(text="Visa is expired") @@ -73,11 +74,16 @@ def parse_visa(visa, user_id): if not payload["ga4gh_visa_v1"]["value"].strip(): raise web.HTTPUnauthorized(text="Visa value is empty") + """ + # TODO THIS WILL NEED TO BE REPLACED BY A SERIOUS LIST OF + # TRUSTED ISSUERS AND THEIR JWT ENDPOINT if payload["ga4gh_visa_v1"]["source"] != REMS_PUB_URL: LOG.error(f"Visa source doesn't match") LOG.erorr(f"Expected: {REMS_PUB_URL}") LOG.error(f'Received: {payload["ga4gh_visa_v1"]["source"]}') raise web.HTTPUnauthorized(text="Visa source doesn't match.") + """ + except Exception as e: LOG.error(f"Error while verifying visa.\n{str(e)}") @@ -87,4 +93,26 @@ def parse_visa(visa, user_id): return payload def decode_jwt(jwt_token): - jwt.decode(jwt_token, options={"verify_signature": False}) \ No newline at end of file + return jwt.decode(jwt_token, options={"verify_signature": False}) + +# receives passport in list format (decoded) +# returns True if passports grant registered access, False otherwise +def verify_registered(passport:List[str], user_id) -> bool: + found_researcher_status = False + found_accepted_terms = False + + for encoded_visa in passport: + try: + decoded_visa = parse_visa(encoded_visa, user_id)["ga4gh_visa_v1"] + if decoded_visa["type"] == "ResearcherStatus": + found_researcher_status = True + elif decoded_visa["type"] == "AcceptedTermsAndPolicies": + found_accepted_terms = True + + except Exception as e: + LOG.error(f"Error verifying visa: {e}") + + if found_researcher_status and found_accepted_terms: + return True + + return False \ No newline at end of file diff --git a/training-ui-files/Dockerfile b/training-ui-files/Dockerfile index 9e8b85d6..5ac4d392 100644 --- a/training-ui-files/Dockerfile +++ b/training-ui-files/Dockerfile @@ -12,3 +12,5 @@ COPY generateRandomSecretKey.py /usr/share/training-ui/beacon-2.x-training-ui/ap #CMD git pull && python3 app/manage.py runserver 0.0.0.0:8080 CMD git pull origin dev && git checkout dev && python3 app/manage.py runserver 0.0.0.0:8080 + +