Skip to content

Commit

Permalink
Merge pull request #60 from bento-platform/features/new-auth
Browse files Browse the repository at this point in the history
Features/new auth
  • Loading branch information
gsfk authored Oct 18, 2023
2 parents ef2ecf7 + 455ad3a commit f8d97c2
Show file tree
Hide file tree
Showing 15 changed files with 118 additions and 118 deletions.
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


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

0 comments on commit f8d97c2

Please sign in to comment.