diff options
Diffstat (limited to 'gn_auth/auth/authorisation/resources/groups/views.py')
| -rw-r--r-- | gn_auth/auth/authorisation/resources/groups/views.py | 269 |
1 files changed, 174 insertions, 95 deletions
diff --git a/gn_auth/auth/authorisation/resources/groups/views.py b/gn_auth/auth/authorisation/resources/groups/views.py index ef6bb0d..2aa115a 100644 --- a/gn_auth/auth/authorisation/resources/groups/views.py +++ b/gn_auth/auth/authorisation/resources/groups/views.py @@ -3,35 +3,47 @@ The views/routes for the `gn3.auth.authorisation.resources.groups` package. """ import uuid import datetime -import warnings -from typing import Iterable from functools import partial from dataclasses import asdict +import sqlite3 from MySQLdb.cursors import DictCursor -from flask import request, jsonify, Response, Blueprint, current_app +from flask import jsonify, Response, Blueprint, current_app +from gn_libs import mysqldb as gn3db + +from gn_auth.auth.requests import request_json from gn_auth.auth.db import sqlite3 as db -from gn_auth.auth.db import mariadb as gn3db from gn_auth.auth.db.sqlite3 import with_db_connection -from gn_auth.auth.authorisation.roles.models import Role -from gn_auth.auth.authorisation.roles.models import user_roles - -from gn_auth.auth.authorisation.checks import authorised_p -from gn_auth.auth.authorisation.privileges import Privilege, privileges_by_ids +from gn_auth.auth.authorisation.privileges import privileges_by_ids from gn_auth.auth.errors import InvalidData, NotFoundError, AuthorisationError -from gn_auth.auth.authentication.users import User +from gn_auth.auth.authentication.users import User, user_by_id from gn_auth.auth.authentication.oauth2.resource_server import require_oauth +from gn_auth.auth.authorisation.resources.checks import authorised_for_spec +from gn_auth.auth.authorisation.resources.groups.models import (resource_from_group, + remove_user_from_group) + from .data import link_data_to_group -from .models import ( - Group, user_group, all_groups, DUMMY_GROUP, GroupRole, group_by_id, - join_requests, group_role_by_id, GroupCreationError, - accept_reject_join_request, group_users as _group_users, - create_group as _create_group, add_privilege_to_group_role, - delete_privilege_from_group_role, create_group_role as _create_group_role) +from .models import (Group, + GroupRole, + user_group, + all_groups, + DUMMY_GROUP, + group_by_id, + group_leaders, + join_requests, + data_resources, + group_role_by_id, + GroupCreationError, + accept_reject_join_request, + add_privilege_to_group_role, + group_users as _group_users, + create_group as _create_group, + delete_group as _delete_group, + delete_privilege_from_group_role) groups = Blueprint("groups", __name__) @@ -39,26 +51,48 @@ groups = Blueprint("groups", __name__) @require_oauth("profile group") def list_groups(): """Return the list of groups that exist.""" + _kwargs = request_json() + def __add_total_group_count__(groups_info): + return { + "groups": groups_info[0], + "total-groups": groups_info[1], + "total-filtered": groups_info[2] + } + with db.connection(current_app.config["AUTH_DB"]) as conn: - the_groups = all_groups(conn) + return jsonify(all_groups( + conn, + search=_kwargs.get("search"), + start=int(_kwargs.get("start", "0")), + length=int(_kwargs.get("length", "0")) + ).then( + __add_total_group_count__ + ).maybe( + { + "groups": [], + "message": "No groups found!", + "total-groups": 0, + "total-filtered": 0 + }, + lambda _grpdata: _grpdata)) - return jsonify(the_groups.maybe( - [], lambda grps: [asdict(grp) for grp in grps])) @groups.route("/create", methods=["POST"]) @require_oauth("profile group") def create_group(): """Create a new group.""" with require_oauth.acquire("profile group") as the_token: - group_name=request.form.get("group_name", "").strip() + group_name=request_json().get("group_name", "").strip() if not bool(group_name): - raise GroupCreationError("Could not create the group.") + raise GroupCreationError( + "Could not create the group. Invalid Group name provided was " + f"`{group_name}`") db_uri = current_app.config["AUTH_DB"] with db.connection(db_uri) as conn: user = the_token.user new_group = _create_group( - conn, group_name, user, request.form.get("group_description")) + conn, group_name, user, request_json().get("group_description")) return jsonify({ **asdict(new_group), "group_leader": asdict(user) }) @@ -107,7 +141,7 @@ def request_to_join(group_id: uuid.UUID) -> Response: } with require_oauth.acquire("profile group") as the_token: - form = request.form + form = request_json() results = with_db_connection(partial( __request__, user=the_token.user, group_id=group_id, message=form.get( "message", "I hereby request that you add me to your group."))) @@ -126,7 +160,7 @@ def list_join_requests() -> Response: def accept_join_requests() -> Response: """Accept a join request.""" with require_oauth.acquire("profile group") as the_token: - form = request.form + form = request_json() request_id = uuid.UUID(form.get("request_id")) return jsonify(with_db_connection(partial( accept_reject_join_request, request_id=request_id, @@ -137,7 +171,7 @@ def accept_join_requests() -> Response: def reject_join_requests() -> Response: """Reject a join request.""" with require_oauth.acquire("profile group") as the_token: - form = request.form + form = request_json() request_id = uuid.UUID(form.get("request_id")) return jsonify(with_db_connection(partial( accept_reject_join_request, request_id=request_id, @@ -171,7 +205,7 @@ def unlinked_genotype_data( return tuple(dict(row) for row in cursor.fetchall()) def unlinked_phenotype_data( - authconn: db.DbConnection, gn3conn: gn3db.DbConnection, + authconn: db.DbConnection, gn3conn: gn3db.Connection, group: Group) -> tuple[dict, ...]: """ Retrieve all phenotype data linked to a group but not linked to any @@ -237,7 +271,7 @@ def unlinked_data(resource_type: str) -> Response: if resource_type in ("system", "group"): return jsonify(tuple()) - if resource_type not in ("all", "mrna", "genotype", "phenotype"): + if resource_type not in ("all", "mrna", "genotype", "phenotype", "inbredset-group"): raise AuthorisationError(f"Invalid resource type {resource_type}") with require_oauth.acquire("profile group resource") as the_token: @@ -255,7 +289,8 @@ def unlinked_data(resource_type: str) -> Response: "genotype": unlinked_genotype_data, "phenotype": lambda conn, grp: partial( unlinked_phenotype_data, gn3conn=gn3conn)( - authconn=conn, group=grp) + authconn=conn, group=grp), + "inbredset-group": lambda authconn, ugroup: [] # Still need to implement this } return jsonify(tuple( dict(row) for row in unlinked_fns[resource_type]( @@ -268,9 +303,9 @@ def unlinked_data(resource_type: str) -> Response: def link_data() -> Response: """Link selected data to specified group.""" with require_oauth.acquire("profile group resource") as _the_token: - form = request.form + form = request_json() group_id = uuid.UUID(form["group_id"]) - dataset_ids = form.getlist("dataset_ids") + dataset_ids = form.get("dataset_ids") dataset_type = form.get("dataset_type") if dataset_type not in ("mrna", "genotype", "phenotype"): raise InvalidData("Unexpected dataset type requested!") @@ -282,70 +317,6 @@ def link_data() -> Response: return jsonify(with_db_connection(__link__)) -@groups.route("/privileges", methods=["GET"]) -@require_oauth("profile group") -def group_privileges(): - """Return a list of all available group roles.""" - with require_oauth.acquire("profile group role") as the_token: - def __list_privileges__(conn: db.DbConnection) -> Iterable[Privilege]: - ## TODO: Check that user has appropriate privileges - this_user_roles = user_roles(conn, the_token.user) - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM privileges " - "WHERE privilege_id LIKE 'group:%'") - group_level_roles = tuple( - Privilege(row["privilege_id"], row["privilege_description"]) - for row in cursor.fetchall()) - - ## the `user_roles(...)` function changed thus this entire function - ## needs to change or be obsoleted -- also remove the ignore below - return tuple( - privilege for arole in this_user_roles["roles"] - for privilege in arole.privileges) + group_level_roles #type: ignore[attr-defined] - warnings.warn( - (f"The `{__name__}.group_privileges` function is broken and will " - "be deleted."), - PendingDeprecationWarning) - return jsonify(tuple( - asdict(priv) for priv in with_db_connection(__list_privileges__))) - - - -@groups.route("/role/create", methods=["POST"]) -@require_oauth("profile group") -def create_group_role(): - """Create a new group role.""" - with require_oauth.acquire("profile group role") as the_token: - ## TODO: Check that user has appropriate privileges - @authorised_p(("group:role:create-role",), - "You do not have the privilege to create new roles", - oauth2_scope="profile group role") - def __create__(conn: db.DbConnection) -> GroupRole: - ## TODO: Check user cannot assign any privilege they don't have. - form = request.form - role_name = form.get("role_name", "").strip() - privileges_ids = form.getlist("privileges[]") - if len(role_name) == 0: - raise InvalidData("Role name not provided!") - if len(privileges_ids) == 0: - raise InvalidData( - "At least one privilege needs to be provided.") - - group = user_group(conn, the_token.user).maybe(# type: ignore[misc] - DUMMY_GROUP, lambda grp: grp) - - if group == DUMMY_GROUP: - raise AuthorisationError( - "A user without a group cannot create a new role.") - privileges = privileges_by_ids(conn, tuple(privileges_ids)) - if len(privileges_ids) != len(privileges): - raise InvalidData( - f"{len(privileges_ids) - len(privileges)} of the selected " - "privileges were not found in the database.") - - return _create_group_role(conn, group, role_name, privileges) - - return jsonify(with_db_connection(__create__)) @groups.route("/role/<uuid:group_role_id>", methods=["GET"]) @require_oauth("profile group") @@ -374,7 +345,7 @@ def __add_remove_priv_to_from_role__(conn: db.DbConnection, raise AuthorisationError( "You need to be a member of a group to edit roles.") try: - privilege_id = request.form.get("privilege_id", "") + privilege_id = request_json().get("privilege_id", "") assert bool(privilege_id), "Privilege to add must be provided." privileges = privileges_by_ids(conn, (privilege_id,)) if len(privileges) == 0: @@ -413,3 +384,111 @@ def delete_priv_from_role(group_role_id: uuid.UUID) -> Response: direction="DELETE", user=the_token.user))), "description": "Privilege deleted successfully" }) + + +@groups.route("/<uuid:group_id>", methods=["GET"]) +@require_oauth("profile group") +def view_group(group_id: uuid.UUID) -> Response: + """View a particular group's details.""" + # TODO: do authorisation checks here… + with (require_oauth.acquire("profile group") as _token, + db.connection(current_app.config["AUTH_DB"]) as conn): + return jsonify(group_by_id(conn, group_id)) + + +@groups.route("/<uuid:group_id>/data-resources", methods=["GET"]) +@require_oauth("profile group") +def view_group_data_resources(group_id: uuid.UUID) -> Response: + """View data resources linked to the group.""" + # TODO: do authorisation checks here… + with (require_oauth.acquire("profile group") as _token, + db.connection(current_app.config["AUTH_DB"]) as conn): + return jsonify(tuple(data_resources(conn, group_id))) + + +@groups.route("/<uuid:group_id>/leaders", methods=["GET"]) +@require_oauth("profile group") +def view_group_leaders(group_id: uuid.UUID) -> Response: + """View a group's leaders.""" + # TODO: do authorisation checks here… + with (require_oauth.acquire("profile group") as _token, + db.connection(current_app.config["AUTH_DB"]) as conn): + return jsonify(tuple(group_leaders(conn, group_id))) + + +@groups.route("/<uuid:group_id>/remove-member", methods=["POST"]) +@require_oauth("profile group") +def remove_group_member(group_id: uuid.UUID): + """Remove a user as member of this group.""" + with (require_oauth.acquire("profile group") as _token, + db.connection(current_app.config["AUTH_DB"]) as conn): + group = group_by_id(conn, group_id) + grp_resource = resource_from_group(conn, group) + if not authorised_for_spec( + conn, + _token.user.user_id, + grp_resource.resource_id, + "(OR group:user:remove-group-member system:group:remove-group-member)"): + raise AuthorisationError( + "You do not have appropriate privileges to remove a user from this " + "group.") + + form = request_json() + if not bool(form.get("user_id")): + response = jsonify({ + "error": "MissingUserId", + "error-description": ( + "Expected 'user_id' value/parameter was not provided.") + }) + response.status_code = 400 + return response + + try: + user = user_by_id(conn, uuid.UUID(form["user_id"])) + remove_user_from_group(conn, group, user, grp_resource) + success_msg = ( + f"User '{user.name} ({user.email})' is no longer a member of " + f"group '{group.group_name}'.\n" + "They could, however, still have access to resources owned by " + "the group.") + return jsonify({ + "description": success_msg, + "message": success_msg + }) + except ValueError as _verr: + response = jsonify({ + "error": "InvalidUserId", + "error-description": "The 'user_id' provided was invalid" + }) + response.status_code = 400 + return response + + +@groups.route("/<uuid:group_id>/delete", methods=["DELETE"]) +@require_oauth("profile group") +def delete_group(group_id: uuid.UUID) -> Response: + """Delete group with the specified `group_id`.""" + with (require_oauth.acquire("profile group") as _token, + db.connection(current_app.config["AUTH_DB"]) as conn): + group = group_by_id(conn, group_id) + grp_resource = resource_from_group(conn, group) + if not authorised_for_spec( + conn, + _token.user.user_id, + grp_resource.resource_id, + "(AND system:group:delete-group)"): + raise AuthorisationError( + "You do not have appropriate privileges to delete this group.") + try: + _delete_group(conn, group.group_id) + return Response(status=204) + except sqlite3.IntegrityError as _s3ie: + response = jsonify({ + "error": "IntegrityError", + "error-description": ( + "A group that has members, linked resources, or both, " + "cannot be deleted from the system. Remove any members and " + "unlink any linked resources, and try again.") + }) + response.status_code = 400 + return response |
