Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/BioData-PT/beacon2-ri-api in…
Browse files Browse the repository at this point in the history
…to dev
  • Loading branch information
FracassandoCasualmente committed Oct 30, 2023
2 parents 3cd2f5a + 5a4409c commit effe80b
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 30 deletions.
4 changes: 2 additions & 2 deletions beacon/request/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions beacon/request/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,26 +124,26 @@ 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)

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,
Expand Down
39 changes: 27 additions & 12 deletions beacon/utils/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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}")
Expand All @@ -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] = []

Expand All @@ -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


2 changes: 1 addition & 1 deletion deploy/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/'
Expand Down
7 changes: 5 additions & 2 deletions permissions/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from aiohttp import web

from permissions.db import search_token
from permissions.tokens import verify_registered

LOG = logging.getLogger(__name__)

Expand All @@ -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')
Expand Down Expand Up @@ -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

11 changes: 8 additions & 3 deletions permissions/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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):
Expand Down
8 changes: 5 additions & 3 deletions permissions/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
34 changes: 31 additions & 3 deletions permissions/tokens.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import jwt
import time
from typing import *
from aiohttp import web
from os import getenv

Expand All @@ -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())

Expand Down Expand Up @@ -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")
Expand All @@ -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)}")
Expand All @@ -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})
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
2 changes: 2 additions & 0 deletions training-ui-files/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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


0 comments on commit effe80b

Please sign in to comment.