diff options
Diffstat (limited to 'gn_auth/auth/authorisation/data')
| -rw-r--r-- | gn_auth/auth/authorisation/data/genotypes.py | 6 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/data/mrna.py | 7 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/data/phenotypes.py | 163 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/data/views.py | 62 |
4 files changed, 196 insertions, 42 deletions
diff --git a/gn_auth/auth/authorisation/data/genotypes.py b/gn_auth/auth/authorisation/data/genotypes.py index bdab8fa..ddb0add 100644 --- a/gn_auth/auth/authorisation/data/genotypes.py +++ b/gn_auth/auth/authorisation/data/genotypes.py @@ -3,9 +3,9 @@ import uuid from dataclasses import asdict from typing import Iterable +from gn_libs import mysqldb as gn3db from MySQLdb.cursors import DictCursor -from gn_auth.auth.db import mariadb as gn3db from gn_auth.auth.db import sqlite3 as authdb from gn_auth.auth.authorisation.checks import authorised_p @@ -22,8 +22,8 @@ def linked_genotype_data(conn: authdb.DbConnection) -> Iterable[dict]: "You do not have sufficient privileges to link data to (a) " "group(s)."), oauth2_scope="profile group resource") -def ungrouped_genotype_data(# pylint: disable=[too-many-arguments] - authconn: authdb.DbConnection, gn3conn: gn3db.DbConnection, +def ungrouped_genotype_data(# pylint: disable=[too-many-arguments, too-many-positional-arguments] + authconn: authdb.DbConnection, gn3conn: gn3db.Connection, search_query: str, selected: tuple[dict, ...] = tuple(), limit: int = 10000, offset: int = 0) -> tuple[ dict, ...]: diff --git a/gn_auth/auth/authorisation/data/mrna.py b/gn_auth/auth/authorisation/data/mrna.py index 60470a7..0cc644e 100644 --- a/gn_auth/auth/authorisation/data/mrna.py +++ b/gn_auth/auth/authorisation/data/mrna.py @@ -2,10 +2,11 @@ import uuid from dataclasses import asdict from typing import Iterable + +from gn_libs import mysqldb as gn3db from MySQLdb.cursors import DictCursor from gn_auth.auth.db import sqlite3 as authdb -from gn_auth.auth.db import mariadb as gn3db from gn_auth.auth.authorisation.checks import authorised_p from gn_auth.auth.authorisation.resources.groups.models import Group @@ -21,8 +22,8 @@ def linked_mrna_data(conn: authdb.DbConnection) -> Iterable[dict]: "You do not have sufficient privileges to link data to (a) " "group(s)."), oauth2_scope="profile group resource") -def ungrouped_mrna_data(# pylint: disable=[too-many-arguments] - authconn: authdb.DbConnection, gn3conn: gn3db.DbConnection, +def ungrouped_mrna_data(# pylint: disable=[too-many-arguments, too-many-positional-arguments] + authconn: authdb.DbConnection, gn3conn: gn3db.Connection, search_query: str, selected: tuple[dict, ...] = tuple(), limit: int = 10000, offset: int = 0) -> tuple[ dict, ...]: diff --git a/gn_auth/auth/authorisation/data/phenotypes.py b/gn_auth/auth/authorisation/data/phenotypes.py index 0a76237..788b9e7 100644 --- a/gn_auth/auth/authorisation/data/phenotypes.py +++ b/gn_auth/auth/authorisation/data/phenotypes.py @@ -3,16 +3,27 @@ import uuid from dataclasses import asdict from typing import Any, Iterable +from gn_libs import mysqldb as gn3db +from gn_libs import sqlite3 as authdb from MySQLdb.cursors import DictCursor +from flask import request, jsonify, Response, Blueprint, current_app as app -from gn_auth.auth.db import sqlite3 as authdb -from gn_auth.auth.db import mariadb as gn3db +from gn_auth.auth.authentication.oauth2.resource_server import require_oauth +from gn_auth.auth.errors import AuthorisationError from gn_auth.auth.authorisation.checks import authorised_p -from gn_auth.auth.authorisation.resources.groups.models import Group +from gn_auth.auth.authorisation.resources.checks import can_delete +from gn_auth.auth.authorisation.resources.system.models import system_resource +from gn_auth.auth.authorisation.resources.groups.models import Group, group_resource + + +from gn_auth.auth.authorisation.checks import require_json +from gn_auth.auth.authorisation.resources.checks import authorised_for2 + +phenosbp = Blueprint("phenotypes", __name__) def linked_phenotype_data( - authconn: authdb.DbConnection, gn3conn: gn3db.DbConnection, + authconn: authdb.DbConnection, gn3conn: gn3db.Connection, species: str = "") -> Iterable[dict[str, Any]]: """Retrieve phenotype data linked to user groups.""" authkeys = ("SpeciesId", "InbredSetId", "PublishFreezeId", "PublishXRefId") @@ -53,7 +64,7 @@ def linked_phenotype_data( "group(s)."), oauth2_scope="profile group resource") def ungrouped_phenotype_data( - authconn: authdb.DbConnection, gn3conn: gn3db.DbConnection): + authconn: authdb.DbConnection, gn3conn: gn3db.Connection): """Retrieve phenotype data that is not linked to any user group.""" with gn3conn.cursor() as cursor: params = tuple( @@ -83,7 +94,7 @@ def ungrouped_phenotype_data( return tuple() -def __traits__(gn3conn: gn3db.DbConnection, params: tuple[dict, ...]) -> tuple[dict, ...]: +def pheno_traits_from_db(gn3conn: gn3db.Connection, params: tuple[dict, ...]) -> tuple[dict, ...]: """An internal utility function. Don't use outside of this module.""" if len(params) < 1: return tuple() @@ -110,21 +121,33 @@ def __traits__(gn3conn: gn3db.DbConnection, params: tuple[dict, ...]) -> tuple[d for itm in sublist)) return cursor.fetchall() -@authorised_p(("system:data:link-to-group",), - error_description=( - "You do not have sufficient privileges to link data to (a) " - "group(s)."), - oauth2_scope="profile group resource") + def link_phenotype_data( - authconn:authdb.DbConnection, gn3conn: gn3db.DbConnection, group: Group, - traits: tuple[dict, ...]) -> dict: + authconn: authdb.DbConnection, + user, + group: Group, + traits: tuple[dict, ...] +) -> dict: """Link phenotype traits to a user group.""" + if not (authorised_for2(authconn, + user, + system_resource(authconn), + ("system:data:link-to-group",)) + or + authorised_for2(authconn, + user, + group_resource(authconn, group.group_id), + ("group:data:link-to-group",)) + ): + raise AuthorisationError( + "You do not have sufficient privileges to link data to group " + f"'{group.group_name}'.") with authdb.cursor(authconn) as cursor: params = tuple({ "data_link_id": str(uuid.uuid4()), "group_id": str(group.group_id), **item - } for item in __traits__(gn3conn, traits)) + } for item in traits) cursor.executemany( "INSERT INTO linked_phenotype_data " "VALUES (" @@ -139,3 +162,115 @@ def link_phenotype_data( "group": asdict(group), "traits": params } + + +def unlink_from_resources( + cursor: authdb.DbCursor, + data_link_ids: tuple[uuid.UUID, ...] +) -> tuple[uuid.UUID, ...]: + """Unlink phenotypes from resources.""" + # TODO: Delete in batches + cursor.executemany("DELETE FROM phenotype_resources " + "WHERE data_link_id=? RETURNING resource_id", + tuple((str(_id),) for _id in data_link_ids)) + return tuple(uuid.UUID(row["resource_id"]) for row in cursor.fetchall()) + + +def delete_resources( + cursor: authdb.DbCursor, + resource_ids: tuple[uuid.UUID, ...] +) -> tuple[uuid.UUID, ...]: + """Delete the specified phenotype resources.""" + # TODO: Delete in batches + cursor.executemany("DELETE FROM resources " + "WHERE resource_id=? RETURNING resource_id", + tuple((str(_id),) for _id in resource_ids)) + return tuple(uuid.UUID(row["resource_id"]) for row in cursor.fetchall()) + + +def fetch_data_link_ids( + cursor: authdb.DbCursor, + species_id: int, + population_id: int, + dataset_id: int, + xref_ids: tuple[int, ...] +) -> tuple[uuid.UUID, ...]: + """Fetch `data_link_id` values for phenotypes.""" + paramstr = ", ".join(["(?, ?, ?, ?)"] * len(xref_ids)) + cursor.execute( + "SELECT data_link_id FROM linked_phenotype_data " + "WHERE (SpeciesId, InbredSetId, PublishFreezeId, PublishXRefId) IN " + f"({paramstr})", + tuple(str(field) for arow in + ((species_id, population_id, dataset_id, xref_id) + for xref_id in xref_ids) + for field in arow)) + return tuple(uuid.UUID(row["data_link_id"]) for row in cursor.fetchall()) + + +def fetch_resource_id(cursor: authdb.DbCursor, + data_link_ids: tuple[uuid.UUID, ...]) -> uuid.UUID: + """Retrieve the ID of the resource where the data is linked to. + + RAISES: InvalidResourceError in the case where more the data_link_ids belong + to more than one resource.""" + _paramstr = ", ".join(["?"] * len(data_link_ids)) + cursor.execute( + "SELECT DISTINCT(resource_id) FROM phenotype_resources " + f"WHERE data_link_id IN ({_paramstr})", + tuple(str(_id) for _id in data_link_ids)) + _ids = tuple(uuid.UUID(row['resource_id']) for row in cursor.fetchall()) + if len(_ids) != 1: + raise AuthorisationError( + f"Expected data from 1 resource, got {len(_ids)} resources.") + return _ids[0] + + +def delete_linked_data( + cursor: authdb.DbCursor, + data_link_ids: tuple[uuid.UUID, ...] +) -> int: + """Delete the actual linked data.""" + # TODO: Delete in batches + cursor.executemany("DELETE FROM linked_phenotype_data " + "WHERE data_link_id=?", + tuple((str(_id),) for _id in data_link_ids)) + return cursor.rowcount + + +@phenosbp.route("/<int:species_id>/<int:population_id>/<int:dataset_id>/delete", + methods=["POST"]) +@require_json +def delete_linked_phenotypes_data( + species_id: int, + population_id: int, + dataset_id: int +) -> Response: + """Delete the linked phenotypes data from the database.""" + db_uri = app.config["AUTH_DB"] + with (require_oauth.acquire("profile group resource") as _token, + authdb.connection(db_uri) as auth_conn, + authdb.cursor(auth_conn) as cursor): + _deleted = 0 + xref_ids = tuple(request.json.get("xref_ids", []))#type: ignore[union-attr] + if len(xref_ids) > 0: + # TODO: Use background job, for huge number of xref_ids + data_link_ids = fetch_data_link_ids( + cursor, species_id, population_id, dataset_id, xref_ids) + resource_id = fetch_resource_id(cursor, data_link_ids) + # - Does user have DELETE privilege on the data + if not can_delete(auth_conn, _token.user.user_id, resource_id): + # - No: Raise `AuthorisationError` and bail! + raise AuthorisationError( + "You are not allowed to delete this resource's data.") + # - YES: go ahead and delete data as below. + _resources_ids = unlink_from_resources(cursor, data_link_ids) + delete_resources(cursor, _resources_ids) + _deleted = delete_linked_data(cursor, data_link_ids) + + return jsonify({ + # TODO: "status": "sent-to-background"/"completed"/"failed" + # TODO: "status-url": <status-check-uri> + "requested": len(xref_ids), + "deleted": _deleted + }) diff --git a/gn_auth/auth/authorisation/data/views.py b/gn_auth/auth/authorisation/data/views.py index 83f4e4b..4bf6746 100644 --- a/gn_auth/auth/authorisation/data/views.py +++ b/gn_auth/auth/authorisation/data/views.py @@ -11,14 +11,17 @@ 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_auth import jobs from gn_auth.commands import run_async_cmd +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 @@ -32,11 +35,12 @@ from ..resources.models import ( 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 +from .genotypes import link_genotype_data, ungrouped_genotype_data +from .phenotypes import phenosbp, link_phenotype_data, pheno_traits_from_db data = Blueprint("data", __name__) +data.register_blueprint(phenosbp, url_prefix="/phenotypes") def build_trait_name(trait_fullname): """ @@ -184,18 +188,18 @@ def __search_mrna__(): return jsonify(with_db_connection(__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.form.get(key, default))) - return request.args.get(key, request.form.get(key, default)) + if bool(request_json()): + return request_json().get(#type: ignore[union-attr] + 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()): - if bool(request.json): - return (request.json.get(key,[])#type: ignore[union-attr] - or request.args.getlist(key) or request.form.getlist(key) + if bool(request_json()): + return (request_json().get(key,[])#type: ignore[union-attr] + or request.args.getlist(key) or request_json().get(key) or list(default)) return (request.args.getlist(key) - or request.form.getlist(key) or list(default)) + or request_json().get(key) or list(default)) def __search_genotypes__(): query = __request_key__("query", "") @@ -240,7 +244,7 @@ def __search_phenotypes__(): @require_oauth("profile group resource") def search_unlinked_data(): """Search for various unlinked data.""" - dataset_type = request.json["dataset_type"] + dataset_type = request_json()["dataset_type"] search_fns = { "mrna": __search_mrna__, "genotype": __search_genotypes__, @@ -281,7 +285,7 @@ def link_genotypes() -> Response: return link_genotype_data(conn, group_by_id(conn, group_id), datasets) return jsonify(with_db_connection( - partial(__link__, **__values__(request.json)))) + partial(__link__, **__values__(request_json())))) @data.route("/link/mrna", methods=["POST"]) def link_mrna() -> Response: @@ -306,9 +310,10 @@ def link_mrna() -> Response: return link_mrna_data(conn, group_by_id(conn, group_id), datasets) return jsonify(with_db_connection( - partial(__link__, **__values__(request.json)))) + 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): @@ -324,14 +329,27 @@ 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( - partial(__link__, **__values__(request.json)))) + partial(__link__, **__values__(request_json())))) |
