diff options
Diffstat (limited to 'gn_auth/auth/authorisation/resources/models.py')
| -rw-r--r-- | gn_auth/auth/authorisation/resources/models.py | 225 |
1 files changed, 167 insertions, 58 deletions
diff --git a/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py index e538a87..27ef183 100644 --- a/gn_auth/auth/authorisation/resources/models.py +++ b/gn_auth/auth/authorisation/resources/models.py @@ -1,12 +1,13 @@ """Handle the management of resources.""" +import logging +from datetime import datetime from dataclasses import asdict from uuid import UUID, uuid4 from functools import reduce, partial -from typing import Dict, Sequence, Optional +from typing import Dict, Union, Sequence, Optional -import sqlite3 +from gn_libs import sqlite3 as db -from gn_auth.auth.db import sqlite3 as db from gn_auth.auth.authentication.users import User from gn_auth.auth.db.sqlite3 import with_db_connection @@ -15,10 +16,11 @@ from gn_auth.auth.authorisation.privileges import Privilege from gn_auth.auth.authorisation.checks import authorised_p from gn_auth.auth.errors import NotFoundError, AuthorisationError -from .checks import authorised_for -from .base import Resource, ResourceCategory, resource_from_dbrow from .common import assign_resource_owner_role +from .checks import can_edit, authorised_for_spec +from .base import Resource, ResourceCategory, resource_from_dbrow from .groups.models import Group, is_group_leader +from .inbredset.models import resource_data as inbredset_resource_data from .mrna import ( resource_data as mrna_resource_data, attach_resources_data as mrna_attach_resources_data, @@ -36,41 +38,91 @@ from .phenotypes.models import ( unlink_data_from_resource as phenotype_unlink_data_from_resource) +logger = logging.getLogger(__name__) + + @authorised_p(("group:resource:create-resource",), error_description="Insufficient privileges to create a resource", oauth2_scope="profile resource") def create_resource(# pylint: disable=[too-many-arguments, too-many-positional-arguments] - cursor: sqlite3.Cursor, + conn: Union[db.DbConnection, db.DbCursor], resource_name: str, resource_category: ResourceCategory, user: User, group: Group, - public: bool + public: bool, + created_at: datetime = datetime.now() ) -> Resource: """Create a resource item.""" - resource = Resource(uuid4(), resource_name, resource_category, public) - cursor.execute( - "INSERT INTO resources VALUES (?, ?, ?, ?)", - (str(resource.resource_id), - resource_name, - str(resource.resource_category.resource_category_id), - 1 if resource.public else 0)) - # TODO: @fredmanglis,@rookie101 - # 1. Move the actions below into a (the?) hooks system - # 2. Do more checks: A resource can have varying hooks depending on type - # e.g. if mRNA, pheno or geno resource, assign: - # - "resource-owner" - # if inbredset-group, assign: - # - "resource-owner", - # - "inbredset-group-owner" etc. - # if resource is of type "group", assign: - # - group-leader - cursor.execute("INSERT INTO resource_ownership (group_id, resource_id) " - "VALUES (?, ?)", - (str(group.group_id), str(resource.resource_id))) - assign_resource_owner_role(cursor, resource.resource_id, user.user_id) - - return resource + def __create_resource__(cursor: db.DbCursor) -> Resource: + resource = Resource(uuid4(), resource_name, resource_category, public) + cursor.execute( + "INSERT INTO resources VALUES (?, ?, ?, ?, ?, ?)", + (str(resource.resource_id), + resource_name, + str(resource.resource_category.resource_category_id), + 1 if resource.public else 0, + str(user.user_id), + created_at.timestamp())) + # TODO: @fredmanglis,@rookie101 + # 1. Move the actions below into a (the?) hooks system + # 2. Do more checks: A resource can have varying hooks depending on type + # e.g. if mRNA, pheno or geno resource, assign: + # - "resource-owner" + # if inbredset-group, assign: + # - "resource-owner", + # - "inbredset-group-owner" etc. + # if resource is of type "group", assign: + # - group-leader + cursor.execute("INSERT INTO resource_ownership (group_id, resource_id) " + "VALUES (?, ?)", + (str(group.group_id), str(resource.resource_id))) + assign_resource_owner_role(cursor, resource.resource_id, user.user_id) + + return resource + + if hasattr(conn, "cursor"): # This is a connection: get its cursor. + with db.cursor(conn) as cursor: + return __create_resource__(cursor) + else: + return __create_resource__(conn) + + +def delete_resource(conn: db.DbConnection, resource_id: UUID): + """Delete a resource.""" + with db.cursor(conn) as cursor: + cursor.execute("DELETE FROM user_roles WHERE resource_id=?", + (str(resource_id),)) + cursor.execute("DELETE FROM resource_roles WHERE resource_id=?", + (str(resource_id),)) + cursor.execute("DELETE FROM group_resources WHERE resource_id=?", + (str(resource_id),)) + cursor.execute("DELETE FROM resource_ownership WHERE resource_id=?", + (str(resource_id),)) + cursor.execute("DELETE FROM resources WHERE resource_id=?", + (str(resource_id),)) + + +def edit_resource(conn: db.DbConnection, resource_id: UUID, name: str) -> Resource: + """Edit basic resource details.""" + with db.cursor(conn) as cursor: + cursor.execute("UPDATE resources SET resource_name=? " + "WHERE resource_id=?", + (name, str(resource_id))) + cursor.execute( + "SELECT r.*, rc.* FROM resources AS r " + "INNER JOIN resource_categories AS rc " + "ON r.resource_category_id=rc.resource_category_id " + "WHERE r.resource_id=?", + (str(resource_id),)) + _resource = resource_from_dbrow(cursor.fetchone()) + cursor.execute( + "SELECT u.* FROM resources AS r INNER JOIN users AS u " + "ON r.created_by=u.user_id WHERE r.resource_id=?", + (str(resource_id),)) + return Resource.from_resource( + _resource, created_by=User.from_sqlite3_row(cursor.fetchone())) + def resource_category_by_id( conn: db.DbConnection, category_id: UUID) -> ResourceCategory: @@ -99,6 +151,18 @@ def resource_categories(conn: db.DbConnection) -> Sequence[ResourceCategory]: for row in cursor.fetchall()) return tuple() + +def __fetch_creators__(cursor, creators_ids: tuple[str, ...]): + cursor.execute( + ("SELECT * FROM users " + f"WHERE user_id IN ({', '.join(['?'] * len(creators_ids))})"), + creators_ids) + return { + row["user_id"]: User.from_sqlite3_row(row) + for row in cursor.fetchall() + } + + def public_resources(conn: db.DbConnection) -> Sequence[Resource]: """List all resources marked as public""" categories = { @@ -106,10 +170,19 @@ def public_resources(conn: db.DbConnection) -> Sequence[Resource]: } with db.cursor(conn) as cursor: cursor.execute("SELECT * FROM resources WHERE public=1") - results = cursor.fetchall() + resource_rows = tuple(cursor.fetchall()) + _creators_ = __fetch_creators__( + cursor, tuple(row["created_by"] for row in resource_rows)) return tuple( - Resource(UUID(row[0]), row[1], categories[row[2]], bool(row[3])) - for row in results) + Resource( + UUID(row[0]), + row[1], + categories[row[2]], + bool(row[3]), + created_by=_creators_[row["created_by"]], + created_at=datetime.fromtimestamp(row["created_at"])) + for row in resource_rows) + def group_leader_resources( conn: db.DbConnection, user: User, group: Group, @@ -129,22 +202,63 @@ def group_leader_resources( for row in cursor.fetchall()) return tuple() -def user_resources(conn: db.DbConnection, user: User) -> Sequence[Resource]: + +def user_resources( + conn: db.DbConnection, + user: User, + start_at: int = 0, + count: int = 0, + text_filter: str = "" +) -> tuple[Sequence[Resource], int]: """List the resources available to the user""" - with db.cursor(conn) as cursor: - cursor.execute( - ("SELECT DISTINCT(r.resource_id), r.resource_name, " - "r.resource_category_id, r.public, rc.resource_category_key, " - "rc.resource_category_description " + text_filter = text_filter.strip() + query_template = ("SELECT %%COLUMNS%% " "FROM user_roles AS ur " "INNER JOIN resources AS r ON ur.resource_id=r.resource_id " "INNER JOIN resource_categories AS rc " "ON r.resource_category_id=rc.resource_category_id " - "WHERE ur.user_id=?"), + "WHERE ur.user_id=? %%LIKE%% %%LIMITS%%") + with db.cursor(conn) as cursor: + cursor.execute( + query_template.replace( + "%%COLUMNS%%", "COUNT(DISTINCT(r.resource_id)) AS count" + ).replace( + "%%LIKE%%", "" + ).replace( + "%%LIMITS%%", ""), (str(user.user_id),)) + _total_records = int(cursor.fetchone()["count"]) + cursor.execute( + query_template.replace( + "%%COLUMNS%%", + "DISTINCT(r.resource_id), r.resource_name, " + "r.resource_category_id, r.public, r.created_by, r.created_at, " + "rc.resource_category_key, rc.resource_category_description" + ).replace( + "%%LIKE%%", + ("" if text_filter == "" else ( + "AND (r.resource_name LIKE ? OR " + "rc.resource_category_key LIKE ? OR " + "rc.resource_category_description LIKE ? )")) + ).replace( + "%%LIMITS%%", + ("" if count <= 0 else f"LIMIT {count} OFFSET {start_at}")), + (str(user.user_id),) + ( + tuple() if text_filter == "" else + tuple(f"%{text_filter}%" for _ in range(0, 3)) + )) rows = cursor.fetchall() or [] - return tuple(resource_from_dbrow(row) for row in rows) + _creators_ = __fetch_creators__( + cursor, tuple(row["created_by"] for row in rows)) + + return tuple( + Resource.from_resource( + resource_from_dbrow(row), + created_by=_creators_[row["created_by"]], + created_at=datetime.fromtimestamp(row["created_at"]) + ) for row in rows), _total_records + def resource_data(conn, resource, offset: int = 0, limit: Optional[int] = None) -> tuple[dict, ...]: @@ -159,7 +273,8 @@ def resource_data(conn, resource, offset: int = 0, limit: Optional[int] = None) "genotype-metadata": lambda *args: tuple(), "mrna-metadata": lambda *args: tuple(), "system": lambda *args: tuple(), - "group": lambda *args: tuple() + "group": lambda *args: tuple(), + "inbredset-group": inbredset_resource_data, } with db.cursor(conn) as cursor: return tuple( @@ -187,9 +302,11 @@ def attach_resource_data(cursor: db.DbCursor, resource: Resource) -> Resource: 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]: + if not authorised_for_spec( + conn, + user.user_id, + resource_id, + "(OR group:resource:view-resource system:resource:view)"): raise AuthorisationError( "You are not authorised to access resource with id " f"'{resource_id}'.") @@ -214,12 +331,9 @@ def link_data_to_resource( data_link_ids: tuple[UUID, ...] ) -> tuple[dict, ...]: """Link data to resource.""" - if not authorised_for( - conn, user, ("group:resource:edit-resource",), - (resource_id,))[resource_id]: + if not can_edit(conn, user.user_id, resource_id): raise AuthorisationError( - "You are not authorised to link data to resource with id " - f"{resource_id}") + "You are not authorised to link/unlink data to this resource.") resource = with_db_connection(partial( resource_by_id, user=user, resource_id=resource_id)) @@ -232,12 +346,9 @@ def link_data_to_resource( 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]: + if not can_edit(conn, user.user_id, resource_id): raise AuthorisationError( - "You are not authorised to link data to resource with id " - f"{resource_id}") + "You are not authorised to link/unlink data this resource.") resource = with_db_connection(partial( resource_by_id, user=user, resource_id=resource_id)) @@ -330,9 +441,7 @@ 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]: + if can_edit(conn, user.user_id, resource_id): with db.cursor(conn) as cursor: cursor.execute( "UPDATE resources SET " |
