From 8b7c598407a5fea9a3d78473e72df87606998cd4 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Fri, 4 Aug 2023 10:10:28 +0300 Subject: Copy over files from GN3 repository. --- gn_auth/auth/authorisation/resources/__init__.py | 2 + gn_auth/auth/authorisation/resources/checks.py | 47 ++ gn_auth/auth/authorisation/resources/models.py | 579 +++++++++++++++++++++++ gn_auth/auth/authorisation/resources/views.py | 272 +++++++++++ 4 files changed, 900 insertions(+) create mode 100644 gn_auth/auth/authorisation/resources/__init__.py create mode 100644 gn_auth/auth/authorisation/resources/checks.py create mode 100644 gn_auth/auth/authorisation/resources/models.py create mode 100644 gn_auth/auth/authorisation/resources/views.py (limited to 'gn_auth/auth/authorisation/resources') diff --git a/gn_auth/auth/authorisation/resources/__init__.py b/gn_auth/auth/authorisation/resources/__init__.py new file mode 100644 index 0000000..869ab60 --- /dev/null +++ b/gn_auth/auth/authorisation/resources/__init__.py @@ -0,0 +1,2 @@ +"""Initialise the `gn3.auth.authorisation.resources` package.""" +from .models import Resource, ResourceCategory diff --git a/gn_auth/auth/authorisation/resources/checks.py b/gn_auth/auth/authorisation/resources/checks.py new file mode 100644 index 0000000..fafde76 --- /dev/null +++ b/gn_auth/auth/authorisation/resources/checks.py @@ -0,0 +1,47 @@ +"""Handle authorisation checks for resources""" +from uuid import UUID +from functools import reduce +from typing import Sequence + +from gn3.auth import db +from gn3.auth.authentication.users import User + +def __organise_privileges_by_resource_id__(rows): + def __organise__(privs, row): + resource_id = UUID(row["resource_id"]) + return { + **privs, + resource_id: (row["privilege_id"],) + privs.get( + resource_id, tuple()) + } + return reduce(__organise__, rows, {}) + +def authorised_for(conn: db.DbConnection, user: User, privileges: tuple[str], + resource_ids: Sequence[UUID]) -> dict[UUID, bool]: + """ + Check whether `user` is authorised to access `resources` according to given + `privileges`. + """ + with db.cursor(conn) as cursor: + cursor.execute( + ("SELECT guror.*, rp.privilege_id FROM " + "group_user_roles_on_resources AS guror " + "INNER JOIN group_roles AS gr ON " + "(guror.group_id=gr.group_id AND guror.role_id=gr.role_id) " + "INNER JOIN roles AS r ON gr.role_id=r.role_id " + "INNER JOIN role_privileges AS rp ON r.role_id=rp.role_id " + "WHERE guror.user_id=? " + f"AND guror.resource_id IN ({', '.join(['?']*len(resource_ids))})" + f"AND rp.privilege_id IN ({', '.join(['?']*len(privileges))})"), + ((str(user.user_id),) + tuple( + str(r_id) for r_id in resource_ids) + tuple(privileges))) + resource_privileges = __organise_privileges_by_resource_id__( + cursor.fetchall()) + authorised = tuple(resource_id for resource_id, res_privileges + in resource_privileges.items() + if all(priv in res_privileges + for priv in privileges)) + return { + resource_id: resource_id in authorised + for resource_id in resource_ids + } diff --git a/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py new file mode 100644 index 0000000..b301a93 --- /dev/null +++ b/gn_auth/auth/authorisation/resources/models.py @@ -0,0 +1,579 @@ +"""Handle the management of resources.""" +import json +import sqlite3 +from uuid import UUID, uuid4 +from functools import reduce, partial +from typing import Any, Dict, Sequence, Optional, NamedTuple + +from gn3.auth import db +from gn3.auth.dictify import dictify +from gn3.auth.authentication.users import User +from gn3.auth.db_utils import with_db_connection + +from .checks import authorised_for + +from ..checks import authorised_p +from ..errors import NotFoundError, AuthorisationError +from ..groups.models import ( + Group, GroupRole, user_group, group_by_id, is_group_leader) + +class MissingGroupError(AuthorisationError): + """Raised for any resource operation without a group.""" + +class ResourceCategory(NamedTuple): + """Class representing a resource category.""" + resource_category_id: UUID + resource_category_key: str + resource_category_description: str + + def dictify(self) -> dict[str, Any]: + """Return a dict representation of `ResourceCategory` objects.""" + return { + "resource_category_id": self.resource_category_id, + "resource_category_key": self.resource_category_key, + "resource_category_description": self.resource_category_description + } + +class Resource(NamedTuple): + """Class representing a resource.""" + group: Group + resource_id: UUID + resource_name: str + resource_category: ResourceCategory + public: bool + resource_data: Sequence[dict[str, Any]] = tuple() + + def dictify(self) -> dict[str, Any]: + """Return a dict representation of `Resource` objects.""" + return { + "group": dictify(self.group), "resource_id": self.resource_id, + "resource_name": self.resource_name, + "resource_category": dictify(self.resource_category), + "public": self.public, + "resource_data": self.resource_data + } + +def __assign_resource_owner_role__(cursor, resource, user): + """Assign `user` the 'Resource Owner' role for `resource`.""" + cursor.execute( + "SELECT gr.* FROM group_roles AS gr INNER JOIN roles AS r " + "ON gr.role_id=r.role_id WHERE r.role_name='resource-owner' " + "AND gr.group_id=?", + (str(resource.group.group_id),)) + role = cursor.fetchone() + if not role: + cursor.execute("SELECT * FROM roles WHERE role_name='resource-owner'") + role = cursor.fetchone() + cursor.execute( + "INSERT INTO group_roles VALUES " + "(:group_role_id, :group_id, :role_id)", + {"group_role_id": str(uuid4()), + "group_id": str(resource.group.group_id), + "role_id": role["role_id"]}) + + cursor.execute( + "INSERT INTO group_user_roles_on_resources " + "VALUES (" + ":group_id, :user_id, :role_id, :resource_id" + ")", + {"group_id": str(resource.group.group_id), + "user_id": str(user.user_id), + "role_id": role["role_id"], + "resource_id": str(resource.resource_id)}) + +@authorised_p(("group:resource:create-resource",), + error_description="Insufficient privileges to create a resource", + oauth2_scope="profile resource") +def create_resource( + conn: db.DbConnection, resource_name: str, + resource_category: ResourceCategory, user: User, + public: bool) -> Resource: + """Create a resource item.""" + with db.cursor(conn) as cursor: + group = user_group(conn, user).maybe( + False, lambda grp: grp)# type: ignore[misc, arg-type] + if not group: + raise MissingGroupError( + "User with no group cannot create a resource.") + resource = Resource( + group, uuid4(), resource_name, resource_category, public) + cursor.execute( + "INSERT INTO resources VALUES (?, ?, ?, ?, ?)", + (str(resource.group.group_id), str(resource.resource_id), + resource_name, + str(resource.resource_category.resource_category_id), + 1 if resource.public else 0)) + __assign_resource_owner_role__(cursor, resource, user) + + return resource + +def resource_category_by_id( + conn: db.DbConnection, category_id: UUID) -> ResourceCategory: + """Retrieve a resource category by its ID.""" + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT * FROM resource_categories WHERE " + "resource_category_id=?", + (str(category_id),)) + results = cursor.fetchone() + if results: + return ResourceCategory( + UUID(results["resource_category_id"]), + results["resource_category_key"], + results["resource_category_description"]) + + raise NotFoundError( + f"Could not find a ResourceCategory with ID '{category_id}'") + +def resource_categories(conn: db.DbConnection) -> Sequence[ResourceCategory]: + """Retrieve all available resource categories""" + with db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM resource_categories") + return tuple( + ResourceCategory(UUID(row[0]), row[1], row[2]) + for row in cursor.fetchall()) + return tuple() + +def public_resources(conn: db.DbConnection) -> Sequence[Resource]: + """List all resources marked as public""" + categories = { + str(cat.resource_category_id): cat for cat in resource_categories(conn) + } + with db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM resources WHERE public=1") + results = cursor.fetchall() + group_uuids = tuple(row[0] for row in results) + query = ("SELECT * FROM groups WHERE group_id IN " + f"({', '.join(['?'] * len(group_uuids))})") + cursor.execute(query, group_uuids) + groups = { + row[0]: Group( + UUID(row[0]), row[1], json.loads(row[2] or "{}")) + for row in cursor.fetchall() + } + return tuple( + Resource(groups[row[0]], UUID(row[1]), row[2], categories[row[3]], + bool(row[4])) + for row in results) + +def group_leader_resources( + conn: db.DbConnection, user: User, group: Group, + res_categories: Dict[UUID, ResourceCategory]) -> Sequence[Resource]: + """Return all the resources available to the group leader""" + if is_group_leader(conn, user, group): + with db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM resources WHERE group_id=?", + (str(group.group_id),)) + return tuple( + Resource(group, UUID(row[1]), row[2], + res_categories[UUID(row[3])], bool(row[4])) + for row in cursor.fetchall()) + return tuple() + +def user_resources(conn: db.DbConnection, user: User) -> Sequence[Resource]: + """List the resources available to the user""" + categories = { # Repeated in `public_resources` function + cat.resource_category_id: cat for cat in resource_categories(conn) + } + with db.cursor(conn) as cursor: + def __all_resources__(group) -> Sequence[Resource]: + gl_resources = group_leader_resources(conn, user, group, categories) + + cursor.execute( + ("SELECT resources.* FROM group_user_roles_on_resources " + "LEFT JOIN resources " + "ON group_user_roles_on_resources.resource_id=resources.resource_id " + "WHERE group_user_roles_on_resources.group_id = ? " + "AND group_user_roles_on_resources.user_id = ?"), + (str(group.group_id), str(user.user_id))) + rows = cursor.fetchall() + private_res = tuple( + Resource(group, UUID(row[1]), row[2], categories[UUID(row[3])], + bool(row[4])) + for row in rows) + return tuple({ + res.resource_id: res + for res in + (private_res + gl_resources + public_resources(conn))# type: ignore[operator] + }.values()) + + # Fix the typing here + return user_group(conn, user).map(__all_resources__).maybe(# type: ignore[arg-type,misc] + public_resources(conn), lambda res: res)# type: ignore[arg-type,return-value] + +def resource_data(conn, resource, offset: int = 0, limit: Optional[int] = None) -> tuple[dict, ...]: + """ + Retrieve the data for `resource`, optionally limiting the number of items. + """ + resource_data_function = { + "mrna": mrna_resource_data, + "genotype": genotype_resource_data, + "phenotype": phenotype_resource_data + } + with db.cursor(conn) as cursor: + return tuple( + dict(data_row) for data_row in + resource_data_function[ + resource.resource_category.resource_category_key]( + cursor, resource.resource_id, offset, limit)) + +def attach_resource_data(cursor: db.DbCursor, resource: Resource) -> Resource: + """Attach the linked data to the resource""" + resource_data_function = { + "mrna": mrna_resource_data, + "genotype": genotype_resource_data, + "phenotype": phenotype_resource_data + } + category = resource.resource_category + data_rows = tuple( + dict(data_row) for data_row in + resource_data_function[category.resource_category_key]( + cursor, resource.resource_id)) + return Resource( + resource.group, resource.resource_id, resource.resource_name, + resource.resource_category, resource.public, data_rows) + +def mrna_resource_data(cursor: db.DbCursor, + resource_id: UUID, + offset: int = 0, + limit: Optional[int] = None) -> Sequence[sqlite3.Row]: + """Fetch data linked to a mRNA resource""" + cursor.execute( + (("SELECT * FROM mrna_resources AS mr " + "INNER JOIN linked_mrna_data AS lmr " + "ON mr.data_link_id=lmr.data_link_id " + "WHERE mr.resource_id=?") + ( + f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")), + (str(resource_id),)) + return cursor.fetchall() + +def genotype_resource_data( + cursor: db.DbCursor, + resource_id: UUID, + offset: int = 0, + limit: Optional[int] = None) -> Sequence[sqlite3.Row]: + """Fetch data linked to a Genotype resource""" + cursor.execute( + (("SELECT * FROM genotype_resources AS gr " + "INNER JOIN linked_genotype_data AS lgd " + "ON gr.data_link_id=lgd.data_link_id " + "WHERE gr.resource_id=?") + ( + f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")), + (str(resource_id),)) + return cursor.fetchall() + +def phenotype_resource_data( + cursor: db.DbCursor, + resource_id: UUID, + offset: int = 0, + limit: Optional[int] = None) -> Sequence[sqlite3.Row]: + """Fetch data linked to a Phenotype resource""" + cursor.execute( + ("SELECT * FROM phenotype_resources AS pr " + "INNER JOIN linked_phenotype_data AS lpd " + "ON pr.data_link_id=lpd.data_link_id " + "WHERE pr.resource_id=?") + ( + f" LIMIT {limit} OFFSET {offset}" if bool(limit) else ""), + (str(resource_id),)) + return cursor.fetchall() + +def resource_by_id( + conn: db.DbConnection, user: User, resource_id: UUID) -> Resource: + """Retrieve a resource by its ID.""" + if not authorised_for( + conn, user, ("group:resource:view-resource",), + (resource_id,))[resource_id]: + raise AuthorisationError( + "You are not authorised to access resource with id " + f"'{resource_id}'.") + + with db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM resources WHERE resource_id=:id", + {"id": str(resource_id)}) + row = cursor.fetchone() + if row: + return Resource( + group_by_id(conn, UUID(row["group_id"])), + UUID(row["resource_id"]), row["resource_name"], + resource_category_by_id(conn, row["resource_category_id"]), + bool(int(row["public"]))) + + raise NotFoundError(f"Could not find a resource with id '{resource_id}'") + +def __link_mrna_data_to_resource__( + conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: + """Link mRNA Assay data with a resource.""" + with db.cursor(conn) as cursor: + params = { + "group_id": str(resource.group.group_id), + "resource_id": str(resource.resource_id), + "data_link_id": str(data_link_id) + } + cursor.execute( + "INSERT INTO mrna_resources VALUES" + "(:group_id, :resource_id, :data_link_id)", + params) + return params + +def __link_geno_data_to_resource__( + conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: + """Link Genotype data with a resource.""" + with db.cursor(conn) as cursor: + params = { + "group_id": str(resource.group.group_id), + "resource_id": str(resource.resource_id), + "data_link_id": str(data_link_id) + } + cursor.execute( + "INSERT INTO genotype_resources VALUES" + "(:group_id, :resource_id, :data_link_id)", + params) + return params + +def __link_pheno_data_to_resource__( + conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: + """Link Phenotype data with a resource.""" + with db.cursor(conn) as cursor: + params = { + "group_id": str(resource.group.group_id), + "resource_id": str(resource.resource_id), + "data_link_id": str(data_link_id) + } + cursor.execute( + "INSERT INTO phenotype_resources VALUES" + "(:group_id, :resource_id, :data_link_id)", + params) + return params + +def link_data_to_resource( + conn: db.DbConnection, user: User, resource_id: UUID, dataset_type: str, + data_link_id: UUID) -> dict: + """Link data to resource.""" + if not authorised_for( + conn, user, ("group:resource:edit-resource",), + (resource_id,))[resource_id]: + raise AuthorisationError( + "You are not authorised to link data to resource with id " + f"{resource_id}") + + resource = with_db_connection(partial( + resource_by_id, user=user, resource_id=resource_id)) + return { + "mrna": __link_mrna_data_to_resource__, + "genotype": __link_geno_data_to_resource__, + "phenotype": __link_pheno_data_to_resource__, + }[dataset_type.lower()](conn, resource, data_link_id) + +def __unlink_mrna_data_to_resource__( + conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: + """Unlink data from mRNA Assay resources""" + with db.cursor(conn) as cursor: + cursor.execute("DELETE FROM mrna_resources " + "WHERE resource_id=? AND data_link_id=?", + (str(resource.resource_id), str(data_link_id))) + return { + "resource_id": str(resource.resource_id), + "dataset_type": resource.resource_category.resource_category_key, + "data_link_id": data_link_id + } + +def __unlink_geno_data_to_resource__( + conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: + """Unlink data from Genotype resources""" + with db.cursor(conn) as cursor: + cursor.execute("DELETE FROM genotype_resources " + "WHERE resource_id=? AND data_link_id=?", + (str(resource.resource_id), str(data_link_id))) + return { + "resource_id": str(resource.resource_id), + "dataset_type": resource.resource_category.resource_category_key, + "data_link_id": data_link_id + } + +def __unlink_pheno_data_to_resource__( + conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: + """Unlink data from Phenotype resources""" + with db.cursor(conn) as cursor: + cursor.execute("DELETE FROM phenotype_resources " + "WHERE resource_id=? AND data_link_id=?", + (str(resource.resource_id), str(data_link_id))) + return { + "resource_id": str(resource.resource_id), + "dataset_type": resource.resource_category.resource_category_key, + "data_link_id": str(data_link_id) + } + +def unlink_data_from_resource( + conn: db.DbConnection, user: User, resource_id: UUID, data_link_id: UUID): + """Unlink data from resource.""" + if not authorised_for( + conn, user, ("group:resource:edit-resource",), + (resource_id,))[resource_id]: + raise AuthorisationError( + "You are not authorised to link data to resource with id " + f"{resource_id}") + + resource = with_db_connection(partial( + resource_by_id, user=user, resource_id=resource_id)) + dataset_type = resource.resource_category.resource_category_key + return { + "mrna": __unlink_mrna_data_to_resource__, + "genotype": __unlink_geno_data_to_resource__, + "phenotype": __unlink_pheno_data_to_resource__, + }[dataset_type.lower()](conn, resource, data_link_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: dict[UUID, tuple[dict, ...]] = reduce(__organise__, data_rows, {}) + return tuple( + Resource( + resource.group, resource.resource_id, resource.resource_name, + resource.resource_category, resource.public, + organised.get(resource.resource_id, tuple())) + 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_mrna_data AS lmd" + " ON mr.data_link_id=lmd.data_link_id " + 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_genotype_data AS lgd " + "ON gr.data_link_id=lgd.data_link_id " + 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_phenotype_data AS lpd " + "ON pr.data_link_id=lpd.data_link_id " + 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) + +@authorised_p( + ("group:user:assign-role",), + "You cannot assign roles to users for this group.", + oauth2_scope="profile group role resource") +def assign_resource_user( + conn: db.DbConnection, resource: Resource, user: User, + role: GroupRole) -> dict: + """Assign `role` to `user` for the specific `resource`.""" + with db.cursor(conn) as cursor: + cursor.execute( + "INSERT INTO " + "group_user_roles_on_resources(group_id, user_id, role_id, " + "resource_id) " + "VALUES (?, ?, ?, ?) " + "ON CONFLICT (group_id, user_id, role_id, resource_id) " + "DO NOTHING", + (str(resource.group.group_id), str(user.user_id), + str(role.role.role_id), str(resource.resource_id))) + return { + "resource": dictify(resource), + "user": dictify(user), + "role": dictify(role), + "description": ( + f"The user '{user.name}'({user.email}) was assigned the " + f"'{role.role.role_name}' role on resource with ID " + f"'{resource.resource_id}'.")} + +@authorised_p( + ("group:user:assign-role",), + "You cannot assign roles to users for this group.", + oauth2_scope="profile group role resource") +def unassign_resource_user( + conn: db.DbConnection, resource: Resource, user: User, + role: GroupRole) -> dict: + """Assign `role` to `user` for the specific `resource`.""" + with db.cursor(conn) as cursor: + cursor.execute( + "DELETE FROM group_user_roles_on_resources " + "WHERE group_id=? AND user_id=? AND role_id=? AND resource_id=?", + (str(resource.group.group_id), str(user.user_id), + str(role.role.role_id), str(resource.resource_id))) + return { + "resource": dictify(resource), + "user": dictify(user), + "role": dictify(role), + "description": ( + f"The user '{user.name}'({user.email}) had the " + f"'{role.role.role_name}' role on resource with ID " + f"'{resource.resource_id}' taken away.")} + +def save_resource( + conn: db.DbConnection, user: User, resource: Resource) -> Resource: + """Update an existing resource.""" + resource_id = resource.resource_id + authorised = authorised_for( + conn, user, ("group:resource:edit-resource",), (resource_id,)) + if authorised[resource_id]: + with db.cursor(conn) as cursor: + cursor.execute( + "UPDATE resources SET " + "resource_name=:resource_name, " + "public=:public " + "WHERE group_id=:group_id " + "AND resource_id=:resource_id", + { + "resource_name": resource.resource_name, + "public": 1 if resource.public else 0, + "group_id": str(resource.group.group_id), + "resource_id": str(resource.resource_id) + }) + return resource + + raise AuthorisationError( + "You do not have the appropriate privileges to edit this resource.") diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py new file mode 100644 index 0000000..3b2bbeb --- /dev/null +++ b/gn_auth/auth/authorisation/resources/views.py @@ -0,0 +1,272 @@ +"""The views/routes for the resources package""" +import uuid +import json +import sqlite3 +from functools import reduce + +from flask import request, jsonify, Response, Blueprint, current_app as app + +from gn3.auth.db_utils import with_db_connection + +from .checks import authorised_for +from .models import ( + Resource, save_resource, resource_data, resource_by_id, resource_categories, + assign_resource_user, link_data_to_resource, unassign_resource_user, + resource_category_by_id, unlink_data_from_resource, + create_resource as _create_resource) + +from ..roles import Role +from ..errors import InvalidData, InconsistencyError, AuthorisationError +from ..groups.models import Group, GroupRole, group_role_by_id + +from ... import db +from ...dictify import dictify +from ...authentication.oauth2.resource_server import require_oauth +from ...authentication.users import User, user_by_id, user_by_email + +resources = Blueprint("resources", __name__) + +@resources.route("/categories", methods=["GET"]) +@require_oauth("profile group resource") +def list_resource_categories() -> Response: + """Retrieve all resource categories""" + db_uri = app.config["AUTH_DB"] + with db.connection(db_uri) as conn: + return jsonify(tuple( + dictify(category) for category in resource_categories(conn))) + +@resources.route("/create", methods=["POST"]) +@require_oauth("profile group resource") +def create_resource() -> Response: + """Create a new resource""" + with require_oauth.acquire("profile group resource") as the_token: + form = request.form + resource_name = form.get("resource_name") + resource_category_id = uuid.UUID(form.get("resource_category")) + db_uri = app.config["AUTH_DB"] + with db.connection(db_uri) as conn: + try: + resource = _create_resource( + conn, + resource_name, + resource_category_by_id(conn, resource_category_id), + the_token.user, + (form.get("public") == "on")) + return jsonify(dictify(resource)) + except sqlite3.IntegrityError as sql3ie: + if sql3ie.args[0] == ("UNIQUE constraint failed: " + "resources.resource_name"): + raise InconsistencyError( + "You cannot have duplicate resource names.") from sql3ie + app.logger.debug( + f"{type(sql3ie)=}: {sql3ie=}") + raise + +@resources.route("/view/") +@require_oauth("profile group resource") +def view_resource(resource_id: uuid.UUID) -> Response: + """View a particular resource's details.""" + with require_oauth.acquire("profile group resource") as the_token: + db_uri = app.config["AUTH_DB"] + with db.connection(db_uri) as conn: + return jsonify(dictify(resource_by_id( + conn, the_token.user, resource_id))) + +def __safe_get_requests_page__(key: str = "page") -> int: + """Get the results page if it exists or default to the first page.""" + try: + return abs(int(request.args.get(key, "1"), base=10)) + except ValueError as _valerr: + return 1 + +def __safe_get_requests_count__(key: str = "count_per_page") -> int: + """Get the results page if it exists or default to the first page.""" + try: + count = request.args.get(key, "0") + if count != 0: + return abs(int(count, base=10)) + return 0 + except ValueError as _valerr: + return 0 + +@resources.route("/view//data") +@require_oauth("profile group resource") +def view_resource_data(resource_id: uuid.UUID) -> Response: + """Retrieve a particular resource's data.""" + with require_oauth.acquire("profile group resource") as the_token: + db_uri = app.config["AUTH_DB"] + count_per_page = __safe_get_requests_count__("count_per_page") + offset = (__safe_get_requests_page__("page") - 1) + with db.connection(db_uri) as conn: + resource = resource_by_id(conn, the_token.user, resource_id) + return jsonify(resource_data( + conn, + resource, + ((offset * count_per_page) if bool(count_per_page) else offset), + count_per_page)) + +@resources.route("/data/link", methods=["POST"]) +@require_oauth("profile group resource") +def link_data(): + """Link group data to a specific resource.""" + try: + form = request.form + assert "resource_id" in form, "Resource ID not provided." + assert "data_link_id" in form, "Data Link ID not provided." + assert "dataset_type" in form, "Dataset type not specified" + assert form["dataset_type"].lower() in ( + "mrna", "genotype", "phenotype"), "Invalid dataset type provided." + + with require_oauth.acquire("profile group resource") as the_token: + def __link__(conn: db.DbConnection): + return link_data_to_resource( + conn, the_token.user, uuid.UUID(form["resource_id"]), + form["dataset_type"], uuid.UUID(form["data_link_id"])) + + return jsonify(with_db_connection(__link__)) + except AssertionError as aserr: + raise InvalidData(aserr.args[0]) from aserr + + + +@resources.route("/data/unlink", methods=["POST"]) +@require_oauth("profile group resource") +def unlink_data(): + """Unlink data bound to a specific resource.""" + try: + form = request.form + assert "resource_id" in form, "Resource ID not provided." + assert "data_link_id" in form, "Data Link ID not provided." + + with require_oauth.acquire("profile group resource") as the_token: + def __unlink__(conn: db.DbConnection): + return unlink_data_from_resource( + conn, the_token.user, uuid.UUID(form["resource_id"]), + uuid.UUID(form["data_link_id"])) + return jsonify(with_db_connection(__unlink__)) + except AssertionError as aserr: + raise InvalidData(aserr.args[0]) from aserr + +@resources.route("/user/list", methods=["GET"]) +@require_oauth("profile group resource") +def resource_users(resource_id: uuid.UUID): + """Retrieve all users with access to the given resource.""" + with require_oauth.acquire("profile group resource") as the_token: + def __the_users__(conn: db.DbConnection): + resource = resource_by_id(conn, the_token.user, resource_id) + authorised = authorised_for( + conn, the_token.user, ("group:resource:edit-resource",), + (resource_id,)) + if authorised.get(resource_id, False): + with db.cursor(conn) as cursor: + def __organise_users_n_roles__(users_n_roles, row): + user_id = uuid.UUID(row["user_id"]) + user = users_n_roles.get(user_id, {}).get( + "user", User(user_id, row["email"], row["name"])) + role = GroupRole( + uuid.UUID(row["group_role_id"]), + resource.group, + Role(uuid.UUID(row["role_id"]), row["role_name"], + bool(int(row["user_editable"])), tuple())) + return { + **users_n_roles, + user_id: { + "user": user, + "user_group": Group( + uuid.UUID(row["group_id"]), row["group_name"], + json.loads(row["group_metadata"])), + "roles": users_n_roles.get( + user_id, {}).get("roles", tuple()) + (role,) + } + } + cursor.execute( + "SELECT g.*, u.*, r.*, gr.group_role_id " + "FROM groups AS g INNER JOIN " + "group_users AS gu ON g.group_id=gu.group_id " + "INNER JOIN users AS u ON gu.user_id=u.user_id " + "INNER JOIN group_user_roles_on_resources AS guror " + "ON u.user_id=guror.user_id INNER JOIN roles AS r " + "ON guror.role_id=r.role_id " + "INNER JOIN group_roles AS gr ON r.role_id=gr.role_id " + "WHERE guror.resource_id=?", + (str(resource_id),)) + return reduce(__organise_users_n_roles__, cursor.fetchall(), {}) + raise AuthorisationError( + "You do not have sufficient privileges to view the resource " + "users.") + results = ( + { + "user": dictify(row["user"]), + "user_group": dictify(row["user_group"]), + "roles": tuple(dictify(role) for role in row["roles"]) + } for row in ( + user_row for user_id, user_row + in with_db_connection(__the_users__).items())) + return jsonify(tuple(results)) + +@resources.route("/user/assign", methods=["POST"]) +@require_oauth("profile group resource role") +def assign_role_to_user(resource_id: uuid.UUID) -> Response: + """Assign a role on the specified resource to a user.""" + with require_oauth.acquire("profile group resource role") as the_token: + try: + form = request.form + group_role_id = form.get("group_role_id", "") + user_email = form.get("user_email", "") + assert bool(group_role_id), "The role must be provided." + assert bool(user_email), "The user email must be provided." + + def __assign__(conn: db.DbConnection) -> dict: + resource = resource_by_id(conn, the_token.user, resource_id) + user = user_by_email(conn, user_email) + return assign_resource_user( + conn, resource, user, + group_role_by_id(conn, resource.group, + uuid.UUID(group_role_id))) + except AssertionError as aserr: + raise AuthorisationError(aserr.args[0]) from aserr + + return jsonify(with_db_connection(__assign__)) + +@resources.route("/user/unassign", methods=["POST"]) +@require_oauth("profile group resource role") +def unassign_role_to_user(resource_id: uuid.UUID) -> Response: + """Unassign a role on the specified resource from a user.""" + with require_oauth.acquire("profile group resource role") as the_token: + try: + form = request.form + group_role_id = form.get("group_role_id", "") + user_id = form.get("user_id", "") + assert bool(group_role_id), "The role must be provided." + assert bool(user_id), "The user id must be provided." + + def __assign__(conn: db.DbConnection) -> dict: + resource = resource_by_id(conn, the_token.user, resource_id) + return unassign_resource_user( + conn, resource, user_by_id(conn, uuid.UUID(user_id)), + group_role_by_id(conn, resource.group, + uuid.UUID(group_role_id))) + except AssertionError as aserr: + raise AuthorisationError(aserr.args[0]) from aserr + + return jsonify(with_db_connection(__assign__)) + +@resources.route("/toggle-public", methods=["POST"]) +@require_oauth("profile group resource role") +def toggle_public(resource_id: uuid.UUID) -> Response: + """Make a resource public if it is private, or private if public.""" + with require_oauth.acquire("profile group resource") as the_token: + def __toggle__(conn: db.DbConnection) -> Resource: + old_rsc = resource_by_id(conn, the_token.user, resource_id) + return save_resource( + conn, the_token.user, Resource( + old_rsc.group, old_rsc.resource_id, old_rsc.resource_name, + old_rsc.resource_category, not old_rsc.public, + old_rsc.resource_data)) + + resource = with_db_connection(__toggle__) + return jsonify({ + "resource": dictify(resource), + "description": ( + "Made resource public" if resource.public + else "Made resource private")}) -- cgit v1.2.3