about summary refs log tree commit diff
path: root/gn_auth/auth/authorisation/data
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth/auth/authorisation/data')
-rw-r--r--gn_auth/auth/authorisation/data/genotypes.py6
-rw-r--r--gn_auth/auth/authorisation/data/mrna.py7
-rw-r--r--gn_auth/auth/authorisation/data/phenotypes.py163
-rw-r--r--gn_auth/auth/authorisation/data/views.py39
4 files changed, 184 insertions, 31 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 7ed69e3..4bf6746 100644
--- a/gn_auth/auth/authorisation/data/views.py
+++ b/gn_auth/auth/authorisation/data/views.py
@@ -11,6 +11,9 @@ 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
 
@@ -19,7 +22,6 @@ 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
@@ -33,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):
     """
@@ -187,7 +190,7 @@ def __search_mrna__():
 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()):
@@ -310,6 +313,7 @@ def link_mrna() -> Response:
         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 +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()))))