From 36248ee1592b87edd83a15c68e090ec220992535 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 27 Feb 2023 15:24:08 +0300 Subject: auth: Endpoint to get access levels for data When the endpoint is accessed with a list of traits, it should/will respond with the access privileges for each of the traits attached for the active user. --- gn3/auth/authorisation/data/__init__.py | 0 gn3/auth/authorisation/data/views.py | 67 +++++++++++++++++++++++ gn3/auth/authorisation/resources/models.py | 86 +++++++++++++++++++++++++++++- gn3/auth/views.py | 2 + 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 gn3/auth/authorisation/data/__init__.py create mode 100644 gn3/auth/authorisation/data/views.py (limited to 'gn3/auth') diff --git a/gn3/auth/authorisation/data/__init__.py b/gn3/auth/authorisation/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gn3/auth/authorisation/data/views.py b/gn3/auth/authorisation/data/views.py new file mode 100644 index 0000000..76f4158 --- /dev/null +++ b/gn3/auth/authorisation/data/views.py @@ -0,0 +1,67 @@ +"""Handle data endpoints.""" +import json +from functools import reduce + +from flask import request, Response, Blueprint, current_app as app +from authlib.integrations.flask_oauth2.errors import _HTTPException + +from gn3 import db_utils as gn3db + +from gn3.db.traits import build_trait_name + +from gn3.auth import db +from gn3.auth.authentication.oauth2.resource_server import require_oauth +from gn3.auth.authorisation.resources.models import ( + user_resources, public_resources, attach_resources_data) + +data = Blueprint("data", __name__) + +@data.route("/authorisation", methods=["GET"]) +def authorisation() -> Response: + """Retrive the authorisation level for datasets/traits for the user.""" + db_uri = app.config["AUTH_DB"] + the_user = "ANONYMOUS" + with db.connection(db_uri) as auth_conn, gn3db.database_connection() as gn3conn: + try: + with require_oauth.acquire("profile group resource") as the_token: + resources = attach_resources_data( + auth_conn, user_resources(auth_conn, the_token.user)) + except _HTTPException as exc: + err_msg = json.loads(exc.body) + if err_msg["error"] == "missing_authorization": + resources = attach_resources_data( + auth_conn, public_resources(auth_conn)) + else: + raise exc from None + + # Access endpoint with somethin like: + # curl -X GET http://127.0.0.1:8080/api/oauth2/data/authorisation \ + # -H "Content-Type: application/json" \ + # -d '{"traits": ["HC_M2_0606_P::1442370_at", "BXDGeno::01.001.695", "BXDPublish::10001"]}' + args = request.get_json() + traits_names = args["traits"] + def __translate__(val): + return { + "ProbeSet": "mRNA", + "Geno": "Genotype", + "Publish": "Phenotype" + }[val] + traits = ( + { + **{key:trait[key] for key in ("trait_fullname", "trait_name")}, + "dataset_name": trait["db"]["dataset_name"], + "dataset_type": __translate__(trait["db"]["dataset_type"]) + } for trait in + (build_trait_name(trait_fullname) + for trait_fullname in traits_names)) + # Retrieve the dataset/trait IDs from traits above + + # Compare the traits/dataset names/ids from request with resources' data + # - Any trait not in resources' data is marked inaccessible + # - Any trait in resources' data: + # - IF public is marked readable + # - IF private and user has read privilege: mark readable + # - ELSE mark inaccessible + + + return "NOT COMPLETED! ..." diff --git a/gn3/auth/authorisation/resources/models.py b/gn3/auth/authorisation/resources/models.py index 3547bc6..8940e00 100644 --- a/gn3/auth/authorisation/resources/models.py +++ b/gn3/auth/authorisation/resources/models.py @@ -2,7 +2,7 @@ import json import sqlite3 from uuid import UUID, uuid4 -from functools import partial +from functools import reduce, partial from typing import Any, Dict, Sequence, NamedTuple from gn3.auth import db @@ -328,3 +328,87 @@ def link_data_to_resource( "genotype": __link_geno_data_to_resource__, "phenotype": __link_pheno_data_to_resource__, }[dataset_type.lower()](conn, resource, dataset_id) + +def organise_resources_by_category(resources: Sequence[Resource]) -> dict[ + ResourceCategory, tuple[Resource]]: + """Organise the `resources` by their categories.""" + def __organise__(accumulator, resource): + category = resource.resource_category + return { + **accumulator, + category: accumulator.get(category, tuple()) + (resource,) + } + return reduce(__organise__, resources, {}) + +def __attach_data__( + data_rows: Sequence[sqlite3.Row], + resources: Sequence[Resource]) -> Sequence[Resource]: + def __organise__(acc, row): + resource_id = UUID(row["resource_id"]) + return { + **acc, + resource_id: acc.get(resource_id, tuple()) + (dict(row),) + } + organised = reduce(__organise__, data_rows, {}) + return tuple( + Resource( + resource.group, resource.resource_id, resource.resource_name, + resource.resource_category, resource.public, + organised[resource.resource_id]) + for resource in resources) + +def attach_mrna_resources_data( + cursor, resources: Sequence[Resource]) -> Sequence[Resource]: + """Attach linked data to mRNA Assay resources""" + placeholders = ", ".join(["?"] * len(resources)) + cursor.execute( + "SELECT * FROM mrna_resources AS mr INNER JOIN linked_group_data AS lgd" + " ON (mr.dataset_id=lgd.dataset_or_trait_id " + "AND mr.dataset_type=lgd.dataset_type) " + f"WHERE mr.resource_id IN ({placeholders})", + tuple(str(resource.resource_id) for resource in resources)) + return __attach_data__(cursor.fetchall(), resources) + +def attach_genotype_resources_data( + cursor, resources: Sequence[Resource]) -> Sequence[Resource]: + """Attach linked data to Genotype resources""" + placeholders = ", ".join(["?"] * len(resources)) + cursor.execute( + "SELECT * FROM genotype_resources AS gr " + "INNER JOIN linked_group_data AS lgd " + "ON (gr.trait_id=lgd.dataset_or_trait_id " + "AND gr.dataset_type=lgd.dataset_type) " + f"WHERE gr.resource_id IN {placeholders}", + tuple(str(resource.resource_id) for resource in resources)) + return __attach_data__(cursor.fetchall(), resources) + +def attach_phenotype_resources_data( + cursor, resources: Sequence[Resource]) -> Sequence[Resource]: + """Attach linked data to Phenotype resources""" + placeholders = ", ".join(["?"] * len(resources)) + cursor.execute( + "SELECT * FROM phenotype_resources AS pr " + "INNER JOIN linked_group_data AS lgd " + "ON (pr.trait_id=lgd.dataset_or_trait_id " + "AND pr.dataset_type=lgd.dataset_type) " + f"WHERE pr.resource_id IN {placeholders}", + tuple(str(resource.resource_id) for resource in resources)) + return __attach_data__(cursor.fetchall(), resources) + +def attach_resources_data( + conn: db.DbConnection, resources: Sequence[Resource]) -> Sequence[ + Resource]: + """Attach linked data for each resource in `resources`""" + resource_data_function = { + "mrna": attach_mrna_resources_data, + "genotype": attach_genotype_resources_data, + "phenotype": attach_phenotype_resources_data + } + organised = organise_resources_by_category(resources) + with db.cursor(conn) as cursor: + return tuple( + resource for categories in + (resource_data_function[category.resource_category_key]( + cursor, rscs) + for category, rscs in organised.items()) + for resource in categories) diff --git a/gn3/auth/views.py b/gn3/auth/views.py index 1d78fd6..4e01cc9 100644 --- a/gn3/auth/views.py +++ b/gn3/auth/views.py @@ -3,6 +3,7 @@ from flask import Blueprint from .authentication.oauth2.views import auth +from .authorisation.data.views import data from .authorisation.users.views import users from .authorisation.roles.views import roles from .authorisation.groups.views import groups @@ -11,6 +12,7 @@ from .authorisation.resources.views import resources oauth2 = Blueprint("oauth2", __name__) oauth2.register_blueprint(auth, url_prefix="/") +oauth2.register_blueprint(data, url_prefix="/data") oauth2.register_blueprint(users, url_prefix="/user") oauth2.register_blueprint(roles, url_prefix="/role") oauth2.register_blueprint(groups, url_prefix="/group") -- cgit v1.2.3