Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features/new auth #60

Merged
merged 17 commits into from
Oct 18, 2023
2 changes: 1 addition & 1 deletion bento_beacon/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .endpoints.datasets import datasets
from .utils.exceptions import APIException
from werkzeug.exceptions import HTTPException
from .authz import authz_middleware
from .authz.middleware import authz_middleware
from .config_files.config import Config
from .utils.beacon_response import beacon_error_response
from .utils.beacon_request import save_request_data, validate_request
Expand Down
Empty file added bento_beacon/authz/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions bento_beacon/authz/headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from flask import request, Request
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file is just unused stuff, but we could leave it if you intend to do some non-bearer-token stuff in the future

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it's going to be in the katsu / gohan stuff



def auth_header_getter(r: Request) -> dict[str, str]:
token = r.headers.get("authorization")
return {"Authorization": token} if token else {}


def auth_header_from_request() -> dict[str, str]:
return auth_header_getter(request)
22 changes: 20 additions & 2 deletions bento_beacon/authz.py → bento_beacon/authz/middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from flask import request
from bento_lib.auth.middleware.flask import FlaskAuthMiddleware
from .config_files.config import Config
from .utils.beacon_response import build_response_meta
from ..config_files.config import Config
from ..utils.beacon_response import build_response_meta

__all__ = [
"authz_middleware",
Expand All @@ -10,13 +11,16 @@
"PERMISSION_QUERY_DATASET_LEVEL_COUNTS",
"PERMISSION_QUERY_DATA",
"PERMISSION_DOWNLOAD_DATA",
"check_permissions",
"check_permission"
]


authz_middleware = FlaskAuthMiddleware(
Config.AUTHZ_URL,
enabled=Config.AUTHZ_ENABLED,
beacon_meta_callback=build_response_meta,
debug_mode=Config.BENTO_DEBUG
)

# for now, these will go unused - Beacon currently does not have a strong concept of Bento projects/datasets
Expand All @@ -27,3 +31,17 @@
# these permissions can open up various aspects of handoff / full-search
PERMISSION_QUERY_DATA = "query:data"
PERMISSION_DOWNLOAD_DATA = "download:data"


def check_permissions(permissions: list[str]) -> bool:
auth_res = authz_middleware.authz_post(request, "/policy/evaluate", body={
"requested_resource": {"everything": True},
"required_permissions": permissions,
},
)["result"]
authz_middleware.mark_authz_done(request)
return auth_res


def check_permission(permission: str) -> bool:
return check_permissions([permission])
6 changes: 4 additions & 2 deletions bento_beacon/config_files/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class Config:

DEFAULT_PAGINATION_PAGE_SIZE = 10

BENTO_DEBUG = os.environ.get("BENTO_DEBUG", os.environ.get(
"FLASK_DEBUG", "false")).strip().lower() in ('true', '1', 't')

BENTO_DOMAIN = os.environ.get("BENTOV2_DOMAIN")
BEACON_BASE_URL = os.environ.get("BEACON_BASE_URL")
BENTO_PUBLIC_URL = os.environ.get("BENTOV2_PUBLIC_URL")
Expand Down Expand Up @@ -140,8 +143,7 @@ class Config:
# -------------------
# drs

DRS_INTERNAL_URL = os.environ.get("DRS_INTERNAL_URL")
DRS_EXTERNAL_URL = os.environ.get("DRS_EXTERNAL_URL")
DRS_URL = os.environ.get("DRS_URL")

# -------------------
# authorization
Expand Down
2 changes: 1 addition & 1 deletion bento_beacon/endpoints/cohorts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from flask import Blueprint, current_app
from ..authz import authz_middleware
from ..authz.middleware import authz_middleware
from ..utils.exceptions import NotImplemented
from ..utils.beacon_response import beacon_response

Expand Down
2 changes: 1 addition & 1 deletion bento_beacon/endpoints/datasets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from flask import Blueprint, current_app
from ..authz import authz_middleware
from ..authz.middleware import authz_middleware
from ..utils.exceptions import NotImplemented
from ..utils.beacon_response import beacon_response
from ..utils.katsu_utils import katsu_datasets
Expand Down
86 changes: 56 additions & 30 deletions bento_beacon/endpoints/individuals.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
from flask import Blueprint
from functools import reduce
from ..authz import authz_middleware
from ..authz.middleware import authz_middleware, PERMISSION_DOWNLOAD_DATA, PERMISSION_QUERY_DATA, check_permission
from ..utils.beacon_request import (
query_parameters_from_request,
summary_stats_requested,
)
)
from ..utils.beacon_response import (
beacon_response,
add_info_to_response,
add_stats_to_response,
add_overview_stats_to_response,
zero_count_response
zero_count_response,
beacon_full_response
)
from ..utils.katsu_utils import (
katsu_filters_and_sample_ids_query,
katsu_total_individuals_count,
search_from_config
search_from_config,
phenopackets_for_ids
)
from ..utils.search import biosample_id_search
from ..utils.handover_utils import handover_for_ids

individuals = Blueprint("individuals", __name__)


@individuals.route("/individuals", methods=['GET', 'POST'])
@authz_middleware.deco_public_endpoint # TODO: authz - for now. eventually, return more depending on permissions
def get_individuals():

full_response = check_permission(PERMISSION_QUERY_DATA)

variants_query, phenopacket_filters, experiment_filters, config_filters = query_parameters_from_request()

no_query = not (variants_query or phenopacket_filters or experiment_filters or config_filters)
search_sample_ids = variants_query or experiment_filters
config_search_only = config_filters and not (variants_query or phenopacket_filters or experiment_filters)

# return total count of individuals if no query
# TODO: return default granularity rather than count (default could be bool rather than count)
if no_query:
add_info_to_response("no query found, returning total count")
total_count = katsu_total_individuals_count()
Expand Down Expand Up @@ -73,41 +79,61 @@ def get_individuals():
# baroque syntax but covers all cases
individual_ids = list(reduce(set.intersection, (set(ids) for ids in individual_results.values())))

# conditionally add summary statistics to response
if summary_stats_requested():
add_stats_to_response(individual_ids)

if full_response:
return individuals_full_response(individual_ids)

return beacon_response({"count": len(individual_ids), "results": individual_ids})


# -------------------------------------------------------
# unimplemented endpoints
# -------------------------------------------------------
# these would be appropriate for full-access beacons only
# TODO: pagination (ideally after katsu search gets paginated)
def individuals_full_response(ids):

# temp
# if len(ids) > 100:
# return {"message": "too many ids for full response"}

# replace this one once auth is in place
# @individuals.route("/individuals/<id>", methods=['GET', 'POST'])
# @authn_token_required_flask_wrapper
# def individual_by_id(id):
# # get one individual by id, with handover if available
# return individuals_full_response([id])
handover_permission = check_permission(PERMISSION_DOWNLOAD_DATA)
handover = handover_for_ids(ids) if handover_permission else {}
phenopackets_by_result_set = phenopackets_for_ids(ids).get("results", {})
result_ids = list(phenopackets_by_result_set.keys())
result_sets = {}

for r_id in result_ids:
results_this_id = phenopackets_by_result_set.get(r_id, {}).get("matches", [])
results_count = len(results_this_id)
result = {
"id": r_id,
"setType": "individual",
"exists": results_count > 0,
"resultsCount": results_count,
"results": results_this_id,
}
handover_this_id = handover.get(r_id, [])
if handover_this_id:
result["resultsHandovers"] = handover_this_id
result_sets[r_id] = result

# @individuals.route("/individuals/<id>/g_variants", methods=['GET', 'POST'])
# def individual_variants(id):
# # all variants for a particular individual
# # may never be implemented, bad match for our use case (download vcf instead)
# raise NotImplemented()
return beacon_full_response(result_sets)


# @individuals.route("/individuals/<id>/biosamples", methods=['GET', 'POST'])
# def individual_biosamples(id):
# # all biosamples for a particular individual
# pass
@individuals.route("/individuals/<id>", methods=['GET', 'POST'])
@authz_middleware.deco_require_permissions_on_resource({PERMISSION_QUERY_DATA})
def individual_by_id(id):
# forbidden / unauthorized if no permissions
return individuals_full_response([id])


# @individuals.route("/individuals/<id>/filtering_terms", methods=['GET', 'POST'])
# def individual_filtering_terms(id):
# # filtering terms for a particular individual
# # note that this involves private data
# pass
# -------------------------------------------------------
# endpoints in beacon model not yet implemented:
#
# /individuals/<id>/g_variants
# - may be simpler to download vcf instead
# /individuals/<id>/biosamples
# - requires full response code for biosamples (could just serve phenopackets again here)
# /individuals/<id>/filtering_terms
# - unclear use case, isn't reading the full response better?
# - requires better ontology support / better filtering terms implementation
# -------------------------------------------------------
2 changes: 1 addition & 1 deletion bento_beacon/endpoints/info.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from copy import deepcopy
from flask import Blueprint, current_app
from ..authz import authz_middleware
from ..authz.middleware import authz_middleware
from ..utils.beacon_response import beacon_info_response
from ..utils.katsu_utils import (
get_filtering_terms,
Expand Down
2 changes: 1 addition & 1 deletion bento_beacon/endpoints/variants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from flask import Blueprint
from ..authz import authz_middleware
from ..authz.middleware import authz_middleware
from ..utils.beacon_request import query_parameters_from_request
from ..utils.beacon_response import beacon_response, add_info_to_response
from ..utils.gohan_utils import query_gohan, gohan_total_variants_count, gohan_totals_by_sample_id
Expand Down
5 changes: 3 additions & 2 deletions bento_beacon/utils/beacon_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def init_response_data():
g.response_info = {}


# TODO: handle multiple messages
def add_info_to_response(info):
g.response_info["message"] = info

Expand Down Expand Up @@ -83,7 +84,7 @@ def beacon_response(results, collection_response=False):
return r


def beacon_response_with_handover(result_sets):
def beacon_full_response(result_sets):
g.request_data["requestedGranularity"] = "record"
r = {
"meta": build_response_meta(),
Expand Down Expand Up @@ -121,7 +122,7 @@ def build_response_meta():
received_request_summary = received_request()
return {
"beaconId": current_app.config["BEACON_ID"],
"apiVersion": current_app.config["BEACON_SPEC_VERSION"],
"apiVersion": current_app.config["BEACON_SPEC_VERSION"],
"returnedSchemas": returned_schemas,
"returnedGranularity": returned_granularity,
"receivedRequestSummary": received_request_summary
Expand Down
2 changes: 1 addition & 1 deletion bento_beacon/utils/gohan_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import current_app
from .exceptions import APIException, InvalidQuery, NotImplemented
from .nested_query_utils import auth_header_from_request
from ..authz.headers import auth_header_from_request
import requests

# -------------------------------------------------------
Expand Down
Loading