about summary refs log tree commit diff
path: root/gn_auth/auth/authorisation/resources/views.py
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth/auth/authorisation/resources/views.py')
-rw-r--r--gn_auth/auth/authorisation/resources/views.py298
1 files changed, 247 insertions, 51 deletions
diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py
index 50f0d8e..f114476 100644
--- a/gn_auth/auth/authorisation/resources/views.py
+++ b/gn_auth/auth/authorisation/resources/views.py
@@ -1,43 +1,67 @@
 """The views/routes for the resources package"""
-from uuid import UUID, uuid4
+import time
 import json
+import logging
 import operator
 import sqlite3
-import time
+from uuid import UUID, uuid4
 
 from dataclasses import asdict
 from functools import reduce
 
-from authlib.integrations.flask_oauth2.errors import _HTTPException
+from werkzeug.exceptions import BadRequest
 from authlib.jose import jwt
+from authlib.integrations.flask_oauth2.errors import _HTTPException
 from flask import (make_response, request, jsonify, Response,
                    Blueprint, current_app as app)
+import gn_libs.privileges.resources
+
+from gn_auth.auth.requests import request_json
 
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.db.sqlite3 import with_db_connection
+from gn_auth.auth.jwks import newest_jwk, jwks_directory
 
 from gn_auth.auth.authorisation.roles import Role
-from gn_auth.auth.authorisation.privileges import Privilege
-from gn_auth.auth.errors import InvalidData, InconsistencyError, AuthorisationError
+from gn_auth.auth.authorisation.roles.models import (
+    create_role,
+    user_resource_roles as _user_resource_roles)
+from gn_auth.auth.errors import (
+    InvalidData,
+    InconsistencyError,
+    AuthorisationError)
+from gn_auth.auth.authorisation.privileges import (
+    privilege_by_id,
+    privileges_by_ids)
 from gn_auth.auth.authorisation.roles.models import (
     role_by_id,
     db_rows_to_roles,
-    check_user_editable,
     delete_privilege_from_resource_role)
 
 from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
 from gn_auth.auth.authentication.users import User, user_by_id, user_by_email
 
-from .checks import authorised_for
+from .inbredset.views import popbp
+from .genotypes.views import genobp
+from .phenotypes.views import phenobp
+from .errors import MissingGroupError
+from .system.models import system_resource
+from .groups.models import Group, user_group
+from .checks import can_delete, authorised_for
 from .models import (
     Resource, resource_data, resource_by_id, public_resources,
     resource_categories, assign_resource_user, link_data_to_resource,
     unassign_resource_user, resource_category_by_id, user_roles_on_resources,
     unlink_data_from_resource, create_resource as _create_resource,
-    get_resource_id)
-from .groups.models import Group, resource_owner, group_role_by_id
+    get_resource_id, delete_resource as _delete_resource,
+    edit_resource as _edit_resource)
+
+logger = logging.getLogger(__name__)
 
 resources = Blueprint("resources", __name__)
+resources.register_blueprint(popbp, url_prefix="/")
+resources.register_blueprint(genobp, url_prefix="/")
+resources.register_blueprint(phenobp, url_prefix="/")
 
 @resources.route("/categories", methods=["GET"])
 @require_oauth("profile group resource")
@@ -53,17 +77,23 @@ def list_resource_categories() -> Response:
 def create_resource() -> Response:
     """Create a new resource"""
     with require_oauth.acquire("profile group resource") as the_token:
-        form = request.form
+        form = request_json()
         resource_name = form.get("resource_name")
         resource_category_id = UUID(form.get("resource_category"))
         db_uri = app.config["AUTH_DB"]
         with db.connection(db_uri) as conn:
             try:
+                group = user_group(conn, the_token.user).maybe(
+                    False, lambda grp: grp)# type: ignore[misc, arg-type]
+                if not group:
+                    raise MissingGroupError(# Not all resources require an owner group
+                        "User with no group cannot create a resource.")
                 resource = _create_resource(
                     conn,
                     resource_name,
                     resource_category_by_id(conn, resource_category_id),
                     the_token.user,
+                    group,
                     (form.get("public") == "on"))
                 return jsonify(asdict(resource))
             except sqlite3.IntegrityError as sql3ie:
@@ -71,11 +101,12 @@ def create_resource() -> Response:
                                       "resources.resource_name"):
                     raise InconsistencyError(
                         "You cannot have duplicate resource names.") from sql3ie
-                app.logger.debug(
-                    f"{type(sql3ie)=}: {sql3ie=}")
+                logger.debug("type(sql3ie)=%s: sql3ie=%s", type(sql3ie), sql3ie)
                 raise
 
+
 @resources.route("/view/<uuid:resource_id>")
+@resources.route("/<uuid:resource_id>/view")
 @require_oauth("profile group resource")
 def view_resource(resource_id: UUID) -> Response:
     """View a particular resource's details."""
@@ -88,6 +119,49 @@ def view_resource(resource_id: UUID) -> Response:
                 )
             )
 
+
+@resources.route("/<uuid:resource_id>/edit", methods=["POST"])
+@require_oauth("profile group resource")
+def edit_resource(resource_id: UUID) -> Response:
+    """Update/edit basic details regarding a resource."""
+    db_uri = app.config["AUTH_DB"]
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(db_uri) as conn):
+        def __extract_privileges__(roles: tuple[Role, ...]) -> tuple[str, ...]:
+            return tuple(
+                priv.privilege_id for role in roles
+                for priv in role.privileges)
+
+        _sys_resource = system_resource(conn)
+        _privileges = {
+            ("system_privileges"
+             if _rid == _sys_resource.resource_id
+             else "resource_privileges"): __extract_privileges__(_rroles)
+            for _rid, _rroles in user_roles_on_resources(
+                conn,
+                _token.user,
+                (resource_id, _sys_resource.resource_id)
+            ).items()
+        }
+        if not gn_libs.privileges.resources.can_edit(**_privileges):
+            return make_response(jsonify({
+                "error": "AuthorisationError",
+                "error_description": "You are not allowed to edit this resource."
+            }), 401)
+
+        name = (request_json().get("resource_name") or "").strip()
+        if bool(name):
+            return jsonify({
+                "resource": asdict(_edit_resource(conn, resource_id, name)),
+                "message": "Resource updated successfully",
+                "status": "success"
+            })
+
+        return make_response(jsonify({
+            "error_description": "Expected `resource_name` to be provided.",
+            "error": "InvalidInput"
+        }), 400)
+
 def __safe_get_requests_page__(key: str = "page") -> int:
     """Get the results page if it exists or default to the first page."""
     try:
@@ -112,7 +186,7 @@ def view_resource_data(resource_id: UUID) -> Response:
     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)
+        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(
@@ -126,9 +200,9 @@ def view_resource_data(resource_id: UUID) -> Response:
 def link_data():
     """Link group data to a specific resource."""
     try:
-        form = request.form
+        form = request_json()
         assert "resource_id" in form, "Resource ID not provided."
-        assert "data_link_id" in form, "Data Link ID not provided."
+        assert "data_link_ids" in form, "Data Link IDs not provided."
         assert "dataset_type" in form, "Dataset type not specified"
         assert form["dataset_type"].lower() in (
             "mrna", "genotype", "phenotype"), "Invalid dataset type provided."
@@ -136,8 +210,11 @@ def link_data():
         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(form["resource_id"]),
-                    form["dataset_type"], UUID(form["data_link_id"]))
+                    conn,
+                    the_token.user,
+                    UUID(form["resource_id"]),
+                    form["dataset_type"],
+                    tuple(UUID(dlinkid) for dlinkid in form["data_link_ids"]))
 
             return jsonify(with_db_connection(__link__))
     except AssertionError as aserr:
@@ -150,7 +227,7 @@ def link_data():
 def unlink_data():
     """Unlink data bound to a specific resource."""
     try:
-        form = request.form
+        form = request_json()
         assert "resource_id" in form, "Resource ID not provided."
         assert "data_link_id" in form, "Data Link ID not provided."
 
@@ -179,7 +256,7 @@ def resource_users(resource_id: UUID):
                 the_token.user,
                 ("group:resource:view-resource",),
                 (resource_id,))
-            systemlevelauth = __pk__authorised_for(
+            systemlevelauth = authorised_for(
                 conn,
                 the_token.user,
                 ("system:user:list",),
@@ -203,9 +280,11 @@ def resource_users(resource_id: UUID):
                             **users_n_roles,
                             user_id: {
                                 "user": user,
-                                "user_group": Group(
-                                    UUID(row["group_id"]), row["group_name"],
-                                    json.loads(row["group_metadata"])),
+                                "user_group": (
+                                    Group(UUID(row["group_id"]),
+                                          row["group_name"],
+                                          json.loads(row["group_metadata"]))
+                                    if bool(row["group_id"]) else False) ,
                                 "roles": users_n_roles.get(
                                     user_id, {}).get("roles", tuple()) + (role,)
                             }
@@ -213,7 +292,7 @@ def resource_users(resource_id: UUID):
                     cursor.execute(
                         "SELECT g.*, u.*, r.* "
                         "FROM groups AS g INNER JOIN group_users AS gu "
-                        "ON g.group_id=gu.group_id INNER JOIN users AS u "
+                        "ON g.group_id=gu.group_id RIGHT JOIN users AS u "
                         "ON gu.user_id=u.user_id INNER JOIN user_roles AS ur "
                         "ON u.user_id=ur.user_id INNER JOIN roles AS r "
                         "ON ur.role_id=r.role_id "
@@ -226,7 +305,8 @@ def resource_users(resource_id: UUID):
         results = (
             {
                 "user": asdict(row["user"]),
-                "user_group": asdict(row["user_group"]),
+                "user_group": (
+                    asdict(row["user_group"]) if row["user_group"] else False),
                 "roles": tuple(asdict(role) for role in row["roles"])
             } for row in (
                 user_row for user_id, user_row
@@ -237,22 +317,25 @@ def resource_users(resource_id: UUID):
 @require_oauth("profile group resource role")
 def assign_role_to_user(resource_id: UUID) -> Response:
     """Assign a role on the specified resource to a user."""
-    with require_oauth.acquire("profile group resource role") as the_token:
+    with require_oauth.acquire("profile group resource role") as _token:
         try:
-            form = request.form
-            group_role_id = form.get("group_role_id", "")
+            form = request_json()
+            role_id = form.get("role_id", "")
             user_email = form.get("user_email", "")
-            assert bool(group_role_id), "The role must be provided."
+            assert bool(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)
+                authorised_for(
+                    conn,
+                    _token.user,
+                    ("resource:role:assign-role",),
+                    (resource_id,))
+                resource = resource_by_id(conn, _token.user, resource_id)
                 user = user_by_email(conn, user_email)
                 return assign_resource_user(
                     conn, resource, user,
-                    group_role_by_id(conn,
-                                     resource_owner(conn, resource),
-                                     UUID(group_role_id)))
+                    role_by_id(conn, UUID(role_id)))# type: ignore[arg-type]
         except AssertionError as aserr:
             raise AuthorisationError(aserr.args[0]) from aserr
 
@@ -262,21 +345,24 @@ def assign_role_to_user(resource_id: UUID) -> Response:
 @require_oauth("profile group resource role")
 def unassign_role_to_user(resource_id: UUID) -> Response:
     """Unassign a role on the specified resource from a user."""
-    with require_oauth.acquire("profile group resource role") as the_token:
+    with require_oauth.acquire("profile group resource role") as _token:
         try:
-            form = request.form
-            group_role_id = form.get("group_role_id", "")
+            form = request_json()
+            role_id = form.get("role_id", "")
             user_id = form.get("user_id", "")
-            assert bool(group_role_id), "The role must be provided."
+            assert bool(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)
+                authorised_for(
+                    conn,
+                    _token.user,
+                    ("resource:role:assign-role",),
+                    (resource_id,))
+                resource = resource_by_id(conn, _token.user, resource_id)
                 return unassign_resource_user(
                     conn, resource, user_by_id(conn, UUID(user_id)),
-                    group_role_by_id(conn,
-                                     resource_owner(conn, resource),
-                                     UUID(group_role_id)))
+                    role_by_id(conn, UUID(role_id)))# type: ignore[arg-type]
         except AssertionError as aserr:
             raise AuthorisationError(aserr.args[0]) from aserr
 
@@ -380,9 +466,18 @@ def resource_roles(resource_id: UUID) -> Response:
                     "ON rp.privilege_id=p.privilege_id "
                     "WHERE rr.resource_id=? AND rr.role_created_by=?",
                     (str(resource_id), str(_token.user.user_id)))
-                results = cursor.fetchall()
+                user_created = db_rows_to_roles(cursor.fetchall())
+
+                cursor.execute(
+                    "SELECT ur.user_id, ur.resource_id, r.*, p.* 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 "
+                    "INNER JOIN privileges AS p ON rp.privilege_id=p.privilege_id "
+                    "WHERE resource_id=? AND user_id=?",
+                    (str(resource_id), str(_token.user.user_id)))
+                assigned_to_user = db_rows_to_roles(cursor.fetchall())
 
-            return db_rows_to_roles(results)
+            return assigned_to_user + user_created
 
         return jsonify(with_db_connection(__roles__))
 
@@ -391,7 +486,7 @@ def resource_roles(resource_id: UUID) -> Response:
 def resources_authorisation():
     """Get user authorisations for given resource(s):"""
     try:
-        data = request.json
+        data = request_json()
         assert (data and "resource-ids" in data)
         resource_ids = tuple(UUID(resid) for resid in data["resource-ids"])
         pubres = tuple(
@@ -423,6 +518,14 @@ def resources_authorisation():
                 "Expected a JSON object with a 'resource-ids' key.")
         })
         resp.status_code = 400
+    except Exception as _exc:#pylint: disable=[broad-except]
+        logger.debug("Generic exception.", exc_info=True)
+        resp = jsonify({
+            "status": "general-exception",
+            "error_description": (
+                "Failed to fetch the user's privileges.")
+        })
+        resp.status_code = 500
 
     return resp
 
@@ -454,7 +557,6 @@ def get_user_roles_on_resource(name) -> Response:
         response = make_response({
             # Flatten this list
             "roles": roles,
-            "silly": "ausah",
         })
         iat = int(time.time())
         jose_header = {
@@ -475,7 +577,8 @@ def get_user_roles_on_resource(name) -> Response:
             "email": _token.user.email,
             "roles": roles,
         }
-        token = jwt.encode(jose_header, payload, app.config["SSL_PRIVATE_KEY"])
+        token = jwt.encode(
+            jose_header, payload, newest_jwk(jwks_directory(app)))
         response.headers["Authorization"] = f"Bearer {token.decode('utf-8')}"
         return response
 
@@ -507,7 +610,7 @@ def resource_role(resource_id: UUID, role_id: UUID):
 
     _roles = db_rows_to_roles(results)
     if len(_roles) > 1:
-        msg = f"There is data corruption in the database."
+        msg = "There is data corruption in the database."
         return jsonify({
             "error": "RoleNotFound",
             "error_description": msg,
@@ -527,7 +630,6 @@ def unassign_resource_role_privilege(resource_id: UUID, role_id: UUID):
           db.connection(app.config["AUTH_DB"]) as conn,
           db.cursor(conn) as cursor):
         _role = role_by_id(conn, role_id)
-        # check_user_editable(_role) # Check whether role is user editable
 
         _authorised = authorised_for(
             conn,
@@ -539,14 +641,15 @@ def unassign_resource_role_privilege(resource_id: UUID, role_id: UUID):
                 "You are not authorised to edit/update this role.")
 
         # Actually unassign the privilege from the role
-        privilege_id = request.json.get("privilege_id")
+        privilege_id = request_json().get("privilege_id")
         if not privilege_id:
             raise AuthorisationError(
                 "You need to provide a privilege to unassign")
 
-        delete_privilege_from_resource_role(cursor,
-                                            _role,
-                                            privilege_by_id(privilege_id))
+        delete_privilege_from_resource_role(
+            cursor,
+            _role,# type: ignore[arg-type]
+            privilege_by_id(conn, privilege_id))# type: ignore[arg-type]
 
         return jsonify({
             "status": "Success",
@@ -570,3 +673,96 @@ def resource_role_users(resource_id: UUID, role_id: UUID):
         results = cursor.fetchall() or []
 
     return jsonify(tuple(User.from_sqlite3_row(row) for row in results)), 200
+
+
+@resources.route("/<uuid:resource_id>/roles/create", methods=["POST"])
+@require_oauth("profile group resource")
+def create_resource_role(resource_id: UUID):
+    """Create a role to act upon a specific resource."""
+    role_name = request_json().get("role_name", "").strip()
+    if not bool(role_name):
+        raise BadRequest("You must provide the name for the new role.")
+
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(app.config["AUTH_DB"]) as conn,
+          db.cursor(conn) as cursor):
+        resource = resource_by_id(conn, _token.user, resource_id)
+        if not bool(resource):
+            raise BadRequest("No resource with that ID exists.")
+
+        privileges = privileges_by_ids(conn, request_json().get("privileges", []))
+        if len(privileges) == 0:
+            raise BadRequest(
+                "You must provide at least one privilege for the new role.")
+        role = create_role(cursor,
+                           f"{resource.resource_name}::{role_name}",
+                           privileges)
+        cursor.execute(
+            "INSERT INTO resource_roles(resource_id, role_created_by, role_id) "
+            "VALUES (:resource_id, :user_id, :role_id)",
+            {
+                "resource_id": str(resource_id),
+                "user_id": str(_token.user.user_id),
+                "role_id": str(role.role_id)
+            })
+
+    return jsonify(asdict(role))
+
+@resources.route("/<uuid:resource_id>/users/<uuid:user_id>/roles", methods=["GET"])
+@require_oauth("profile group resource role")
+def user_resource_roles(resource_id: UUID, user_id: UUID):
+    """Get a specific user's roles on a particular resource."""
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(app.config["AUTH_DB"]) as conn):
+        if _token.user.user_id != user_id:
+            raise AuthorisationError(
+                "You are not authorised to view the roles this user has.")
+
+        _resource = resource_by_id(conn, _token.user, resource_id)
+        if not bool(_resource):
+            raise BadRequest("No resource was found with the given ID.")
+
+        return jsonify([asdict(role) for role in
+                        _user_resource_roles(conn, _token.user, _resource)])
+
+
+@resources.route("/delete", methods=["POST"])
+@require_oauth("profile group resource")
+def delete_resource():
+    """Delete the specified resource, if possible."""
+    with (require_oauth.acquire("profile group resource") as the_token,
+          db.connection(app.config["AUTH_DB"]) as conn):
+        form = request_json()
+        try:
+            resource_id = UUID(form.get("resource_id"))
+            if not can_delete(conn, the_token.user.user_id, resource_id):
+                raise AuthorisationError(
+                    "You are not allowed to delete this resource.")
+
+            data = resource_data(
+                conn,
+                resource_by_id(conn, the_token.user, resource_id),
+                0,
+                10)
+            if bool(data):
+                return jsonify({
+                    "error": "NonEmptyResouce",
+                    "error-description": "Cannot delete a resource with linked data"
+                }), 400
+
+            _delete_resource(conn, resource_id)
+            return jsonify({
+                "description": f"Successfully deleted resource with ID '{resource_id}'."
+            })
+        except ValueError as _verr:
+            logger.debug("Error!", exc_info=True)
+            return jsonify({
+                "error": "ValueError",
+                "error-description": "An invalid identifier was provided"
+            }), 400
+        except TypeError as _terr:
+            logger.debug("Error!", exc_info=True)
+            return jsonify({
+                "error": "TypeError",
+                "error-description": "An invalid identifier was provided"
+            }), 400