aboutsummaryrefslogtreecommitdiff
path: root/gn_auth/auth/authorisation/data/views.py
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth/auth/authorisation/data/views.py')
-rw-r--r--gn_auth/auth/authorisation/data/views.py319
1 files changed, 180 insertions, 139 deletions
diff --git a/gn_auth/auth/authorisation/data/views.py b/gn_auth/auth/authorisation/data/views.py
index 7ed69e3..0ffc08e 100644
--- a/gn_auth/auth/authorisation/data/views.py
+++ b/gn_auth/auth/authorisation/data/views.py
@@ -2,15 +2,20 @@
import sys
import uuid
import json
-from dataclasses import asdict
+import logging
from typing import Any
-from functools import partial
+from functools import reduce, partial
import redis
from MySQLdb.cursors import DictCursor
from authlib.integrations.flask_oauth2.errors import _HTTPException
from flask import request, jsonify, Response, Blueprint, current_app as app
+
+from gn_libs import mysqldb as gn3db
+from gn_libs import sqlite3 as db
+from gn_libs.sqlite3 import with_db_connection
+
from gn_auth import jobs
from gn_auth.commands import run_async_cmd
@@ -18,54 +23,30 @@ from gn_auth.auth.requests import request_json
from gn_auth.auth.errors import InvalidData, NotFoundError
from gn_auth.auth.authorisation.resources.groups.models import group_by_id
-from ...db import sqlite3 as db
-from ...db import mariadb as gn3db
-from ...db.sqlite3 import with_db_connection
-
from ..checks import require_json
-from ..users.models import user_resource_roles
-
-from ..resources.checks import authorised_for
-from ..resources.models import (
- user_resources, public_resources, attach_resources_data)
-
from ...authentication.users import User
from ...authentication.oauth2.resource_server import require_oauth
-from ..data.phenotypes import link_phenotype_data
-from ..data.mrna import link_mrna_data, ungrouped_mrna_data
-from ..data.genotypes import link_genotype_data, ungrouped_genotype_data
-
+from .mrna import (
+ link_mrna_data,
+ ungrouped_mrna_data,
+ resources_by_datasets_and_traits as mrna_resources_by_datasets_and_traits)
+from .genotypes import (
+ link_genotype_data,
+ ungrouped_genotype_data,
+ resources_by_datasets_and_traits as geno_resources_by_datasets_and_traits)
+from .phenotypes import (
+ phenosbp,
+ link_phenotype_data,
+ pheno_traits_from_db,
+ resources_by_datasets_and_traits as pheno_resources_by_datasets_and_traits)
+
+
+logger = logging.getLogger(__name__)
data = Blueprint("data", __name__)
+data.register_blueprint(phenosbp, url_prefix="/phenotypes")
-def build_trait_name(trait_fullname):
- """
- Initialises the trait's name, and other values from the search data provided
-
- This is a copy of `gn3.db.traits.build_trait_name` function.
- """
- def dataset_type(dset_name):
- if dset_name.find('Temp') >= 0:
- return "Temp"
- if dset_name.find('Geno') >= 0:
- return "Geno"
- if dset_name.find('Publish') >= 0:
- return "Publish"
- return "ProbeSet"
-
- name_parts = trait_fullname.split("::")
- assert len(name_parts) >= 2, f"Name format error: '{trait_fullname}'"
- dataset_name = name_parts[0]
- dataset_type = dataset_type(dataset_name)
- return {
- "db": {
- "dataset_name": dataset_name,
- "dataset_type": dataset_type},
- "trait_fullname": trait_fullname,
- "trait_name": name_parts[1],
- "cellid": name_parts[2] if len(name_parts) == 3 else ""
- }
@data.route("species")
def list_species() -> Response:
@@ -77,101 +58,144 @@ def list_species() -> Response:
@data.route("/authorisation", methods=["POST"])
@require_json
-def authorisation() -> Response:
+def authorisation() -> Response:# pylint: disable=[too-many-locals]
"""Retrieve the authorisation level for datasets/traits for the user."""
# Access endpoint with something like:
- # curl -X POST http://127.0.0.1:8080/api/oauth2/data/authorisation \
+ # curl -X POST http://127.0.0.1:8081/auth/data/authorisation \
# -H "Content-Type: application/json" \
# -d '{"traits": ["HC_M2_0606_P::1442370_at", "BXDGeno::01.001.695",
# "BXDPublish::10001"]}'
+ def __organise_traits__(acc, curr):
+ dset, _trt = curr
+ key = "ProbeSet"
+ if dset.endswith("Publish"):
+ key = "Publish"
+ elif dset.endswith("Geno"):
+ key="Geno"
+ elif dset.endswith("Temp"):
+ key = "Temp"
+ else:
+ key = "ProbeSet"
+
+ return {
+ **acc,
+ key: acc.get(key, tuple()) + (curr,)
+ }
+ _dset_traits: dict[str, tuple[tuple[str, str], ...]] = reduce(
+ __organise_traits__,
+ (
+ (dset.strip(), trt.strip()) for dset, trt in
+ (trtstr.split("::") for trtstr in
+ request_json().get("traits", []))),
+ {key: tuple() for key in ("Publish", "ProbeSet", "Geno", "Temp")})
+
db_uri = app.config["AUTH_DB"]
- privileges = {}
user = User(uuid.uuid4(), "anon@ymous.user", "Anonymous User")
- with db.connection(db_uri) as auth_conn:
- try:
- with require_oauth.acquire("profile group resource") as _token:
- user = _token.user
- resources = attach_resources_data(
- auth_conn, user_resources(auth_conn, _token.user))
- resources_roles = user_resource_roles(auth_conn, _token.user)
- privileges = {
- resource_id: tuple(
- privilege.privilege_id
- for roles in resources_roles[resource_id]
- for privilege in roles.privileges)#("group:resource:view-resource",)
- for resource_id, is_authorised
- in authorised_for(
- auth_conn, _token.user,
- ("group:resource:view-resource",), tuple(
- resource.resource_id for resource in resources)).items()
- if is_authorised
- }
- 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
-
- def __gen_key__(resource, data_item):
- if resource.resource_category.resource_category_key.lower() == "phenotype":
- return (
- f"{resource.resource_category.resource_category_key.lower()}::"
- f"{data_item['dataset_name']}::{data_item['PublishXRefId']}")
- return (
- f"{resource.resource_category.resource_category_key.lower()}::"
- f"{data_item['dataset_name']}")
-
- data_to_resource_map = {
- __gen_key__(resource, data_item): resource.resource_id
- for resource in resources
- for data_item in resource.resource_data
+ with (db.connection(db_uri) as authconn, db.cursor(authconn) as cursor):
+ _all_resources = {
+ _rrow["resource_id"]: _rrow
+ for _rtypes in (
+ pheno_resources_by_datasets_and_traits(
+ authconn, _dset_traits["Publish"]),
+ geno_resources_by_datasets_and_traits(
+ authconn, _dset_traits["Geno"]),
+ mrna_resources_by_datasets_and_traits(
+ authconn, _dset_traits["ProbeSet"]))
+ for _rrow in _rtypes
}
- privileges = {
- **{
- resource.resource_id: ("system:resource:public-read",)
- for resource in resources if resource.public
- },
- **privileges}
-
- args = request.get_json()
- traits_names = args["traits"] # type: ignore[index]
- def __translate__(val):
+ if (len(_all_resources.keys()) == 0 and
+ len(_dset_traits.get("Temp", tuple())) == 0):
+ raise NotFoundError(
+ "No resource(s) found for specified trait(s). Do(es) the "
+ "trait(s) actually exist?")
+
+ # Handle Temp traits specially - they should be public/anonymous resources
+ if len(_dset_traits.get("Temp", tuple())) > 0:
+ # Create a synthetic public resource for Temp traits
+ # Use a predictable ID to identify synthetic temp resources
+ temp_resource_id = "gn-auth-temp-traits"
+ _all_resources[temp_resource_id] = {
+ "resource_id": temp_resource_id,
+ "resource_data": tuple(f"{dset}::{trait}" for dset, trait in _dset_traits["Temp"])
+ }
+
+ _resource_ids = tuple(_all_resources.keys())
+
+
+ def __explode_resource_data__(trait_fullname):
+ _dset, _trt = trait_fullname.split("::")
return {
- "Temp": "Temp",
- "ProbeSet": "mRNA",
- "Geno": "Genotype",
- "Publish": "Phenotype"
- }[val]
-
- def __trait_key__(trait):
- dataset_type = __translate__(trait['db']['dataset_type']).lower()
- dataset_name = trait["db"]["dataset_name"]
- if dataset_type == "phenotype":
- return f"{dataset_type}::{dataset_name}::{trait['trait_name']}"
- return f"{dataset_type}::{dataset_name}"
-
- return jsonify(tuple(
- {
- "user": asdict(user),
- **{key:trait[key] for key in ("trait_fullname", "trait_name")},
- "dataset_name": trait["db"]["dataset_name"],
- "dataset_type": __translate__(trait["db"]["dataset_type"]),
- "resource_id": data_to_resource_map.get(__trait_key__(trait)),
- "privileges": privileges.get(
- data_to_resource_map.get(
- __trait_key__(trait),
- uuid.UUID("4afa415e-94cb-4189-b2c6-f9ce2b6a878d")),
- tuple()) + (
- # Temporary traits do not exist in db: Set them
- # as public-read
- ("system:resource:public-read",)
- if trait["db"]["dataset_type"] == "Temp"
- else tuple())
- } for trait in
- (build_trait_name(trait_fullname)
- for trait_fullname in traits_names)))
+ "dataset_name": _dset,
+ "dataset_type": (
+ "Phenotype" if _dset.endswith("Publish")
+ else ("Genotype" if _dset.endswith("Geno")
+ else ("Temporary" if _dset.endswith("Temp")
+ else "mRNA"))),
+ "trait_name": _trt,
+ "trait_fullname": trait_fullname
+ }
+
+ _paramstr = ", ".join(["?"] * len(_resource_ids))
+ _privileges_by_resource: dict[str, tuple[str, ...]] = {}
+
+ # Separate synthetic temp resources from real resources
+ temp_resource_id = "gn-auth-temp-traits"
+ real_resource_ids = tuple(rid for rid in _resource_ids if rid != temp_resource_id)
+
+ # Query privileges only for real resources
+ if len(real_resource_ids) > 0:
+ real_paramstr = ", ".join(["?"] * len(real_resource_ids))
+ try:
+ with require_oauth.acquire("profile group resource") as _token:
+ user = _token.user
+ cursor.execute(
+ "SELECT ur.resource_id, r.role_id, rp.privilege_id "
+ "FROM user_roles AS ur "
+ "INNER JOIN roles AS r ON ur.role_id=r.role_id "
+ "INNER JOIN role_privileges AS rp ON r.role_id=rp.role_id "
+ "WHERE ur.user_id = ? "
+ f"AND ur.resource_id IN ({real_paramstr})",
+ (str(user.user_id),) + real_resource_ids
+ )
+ _privileges_by_resource = reduce(
+ lambda acc, curr: {
+ **acc,
+ curr["resource_id"]: (
+ acc.get(curr["resource_id"], tuple())
+ + (curr["privilege_id"],))
+ },
+ cursor.fetchall(),
+ {})
+ except _HTTPException as exc:
+ err_msg = json.loads(exc.body)
+ if err_msg["error"] == "missing_authorization":
+ cursor.execute(
+ "SELECT rsc.resource_id "
+ "FROM resources AS rsc "
+ "WHERE rsc.public = '1' "
+ f"AND rsc.resource_id IN ({real_paramstr}) ",
+ real_resource_ids)
+ _privileges_by_resource = {
+ row["resource_id"]: ('group:resource:view-resource',)
+ for row in cursor.fetchall()
+ }
+ else:
+ raise exc from None
+
+ # Temp resources are always publicly viewable
+ if temp_resource_id in _resource_ids:
+ _privileges_by_resource[temp_resource_id] = ('group:resource:view-resource',)
+
+ return jsonify({
+ "authorisation": [{
+ **resource,
+ "resource_data": [
+ __explode_resource_data__(item)
+ for item in resource["resource_data"]],
+ "privileges": _privileges_by_resource.get(resource["resource_id"], tuple())
+ } for resource in _all_resources.values()]
+ })
+
def __search_mrna__():
query = __request_key__("query", "")
@@ -182,12 +206,12 @@ def __search_mrna__():
ungrouped_mrna_data, gn3conn=gn3conn, search_query=query,
selected=__request_key_list__("selected"),
limit=limit, offset=offset)
- return jsonify(with_db_connection(__ungrouped__))
+ return jsonify(with_db_connection(app.config["SQL_URI"], __ungrouped__))
def __request_key__(key: str, default: Any = ""):
if bool(request_json()):
return request_json().get(#type: ignore[union-attr]
- key, request.args.get(key, request_json().get(key, default)))
+ key, request.args.get(key, default))
return request.args.get(key, request_json().get(key, default))
def __request_key_list__(key: str, default: tuple[Any, ...] = tuple()):
@@ -207,7 +231,7 @@ def __search_genotypes__():
ungrouped_genotype_data, gn3conn=gn3conn, search_query=query,
selected=__request_key_list__("selected"),
limit=limit, offset=offset)
- return jsonify(with_db_connection(__ungrouped__))
+ return jsonify(with_db_connection(app.config["SQL_URI"], __ungrouped__))
def __search_phenotypes__():
# launch the external process to search for phenotypes
@@ -216,7 +240,7 @@ def __search_phenotypes__():
job_id = uuid.uuid4()
selected = __request_key__("selected_traits", [])
command =[
- sys.executable, "-m", "scripts.search_phenotypes",
+ sys.executable, "-m", "gn_auth.scripts.search_phenotypes",
__request_key__("species_name"),
__request_key__("query"),
str(job_id),
@@ -282,6 +306,7 @@ def link_genotypes() -> Response:
return link_genotype_data(conn, group_by_id(conn, group_id), datasets)
return jsonify(with_db_connection(
+ app.config["SQL_URI"],
partial(__link__, **__values__(request_json()))))
@data.route("/link/mrna", methods=["POST"])
@@ -307,9 +332,11 @@ def link_mrna() -> Response:
return link_mrna_data(conn, group_by_id(conn, group_id), datasets)
return jsonify(with_db_connection(
+ app.config["SQL_URI"],
partial(__link__, **__values__(request_json()))))
@data.route("/link/phenotype", methods=["POST"])
+@require_oauth("profile group resource")
def link_phenotype() -> Response:
"""Link phenotype data to group."""
def __values__(form):
@@ -325,14 +352,28 @@ def link_phenotype() -> Response:
raise InvalidData("Expected at least one dataset to be provided.")
return {
"group_id": uuid.UUID(form["group_id"]),
- "traits": form["selected"]
+ "traits": form["selected"],
+ "using_raw_ids": bool(form.get("using-raw-ids") == "on")
}
- with gn3db.database_connection(app.config["SQL_URI"]) as gn3conn:
- def __link__(conn: db.DbConnection, group_id: uuid.UUID,
- traits: tuple[dict, ...]) -> dict:
- return link_phenotype_data(
- conn, gn3conn, group_by_id(conn, group_id), traits)
+ with (require_oauth.acquire("profile group resource") as token,
+ gn3db.database_connection(app.config["SQL_URI"]) as gn3conn):
+ def __link__(
+ conn: db.DbConnection,
+ group_id: uuid.UUID,
+ traits: tuple[dict, ...],
+ using_raw_ids: bool = False
+ ) -> dict:
+ if using_raw_ids:
+ return link_phenotype_data(conn,
+ token.user,
+ group_by_id(conn, group_id),
+ traits)
+ return link_phenotype_data(conn,
+ token.user,
+ group_by_id(conn, group_id),
+ pheno_traits_from_db(gn3conn, traits))
return jsonify(with_db_connection(
+ app.config["SQL_URI"],
partial(__link__, **__values__(request_json()))))