diff options
Diffstat (limited to 'gn_auth/auth/authorisation/resources/groups')
| -rw-r--r-- | gn_auth/auth/authorisation/resources/groups/data.py | 12 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/resources/groups/models.py | 250 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/resources/groups/views.py | 269 |
3 files changed, 412 insertions, 119 deletions
diff --git a/gn_auth/auth/authorisation/resources/groups/data.py b/gn_auth/auth/authorisation/resources/groups/data.py index 702955d..ad0dfba 100644 --- a/gn_auth/auth/authorisation/resources/groups/data.py +++ b/gn_auth/auth/authorisation/resources/groups/data.py @@ -1,7 +1,7 @@ """Handles the resource objects' data.""" +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.errors import NotFoundError @@ -9,7 +9,7 @@ from gn_auth.auth.authorisation.checks import authorised_p from gn_auth.auth.authorisation.resources.groups import Group def __fetch_mrna_data_by_ids__( - conn: gn3db.DbConnection, dataset_ids: tuple[str, ...]) -> tuple[ + conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[ dict, ...]: """Fetch mRNA Assay data by ID.""" with conn.cursor(DictCursor) as cursor: @@ -27,7 +27,7 @@ def __fetch_mrna_data_by_ids__( raise NotFoundError("Could not find mRNA Assay data with the given ID.") def __fetch_geno_data_by_ids__( - conn: gn3db.DbConnection, dataset_ids: tuple[str, ...]) -> tuple[ + conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[ dict, ...]: """Fetch genotype data by ID.""" with conn.cursor(DictCursor) as cursor: @@ -45,7 +45,7 @@ def __fetch_geno_data_by_ids__( raise NotFoundError("Could not find Genotype data with the given ID.") def __fetch_pheno_data_by_ids__( - conn: gn3db.DbConnection, dataset_ids: tuple[str, ...]) -> tuple[ + conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[ dict, ...]: """Fetch phenotype data by ID.""" with conn.cursor(DictCursor) as cursor: @@ -67,7 +67,7 @@ def __fetch_pheno_data_by_ids__( "Could not find Phenotype/Publish data with the given IDs.") def __fetch_data_by_id( - conn: gn3db.DbConnection, dataset_type: str, + conn: gn3db.Connection, dataset_type: str, dataset_ids: tuple[str, ...]) -> tuple[dict, ...]: """Fetch data from MySQL by IDs.""" fetch_fns = { @@ -83,7 +83,7 @@ def __fetch_data_by_id( "group(s)."), oauth2_scope="profile group resource") def link_data_to_group( - authconn: authdb.DbConnection, gn3conn: gn3db.DbConnection, + authconn: authdb.DbConnection, gn3conn: gn3db.Connection, dataset_type: str, dataset_ids: tuple[str, ...], group: Group) -> tuple[ dict, ...]: """Link the given data to the specified group.""" diff --git a/gn_auth/auth/authorisation/resources/groups/models.py b/gn_auth/auth/authorisation/resources/groups/models.py index 03a93b6..6a7af4c 100644 --- a/gn_auth/auth/authorisation/resources/groups/models.py +++ b/gn_auth/auth/authorisation/resources/groups/models.py @@ -5,16 +5,24 @@ from functools import reduce from dataclasses import dataclass from typing import Any, Sequence, Iterable, Optional +import sqlite3 from flask import g from pymonad.maybe import Just, Maybe, Nothing +from pymonad.either import Left, Right, Either +from pymonad.tools import monad_from_none_or_value from gn_auth.auth.db import sqlite3 as db from gn_auth.auth.authentication.users import User, user_by_id from gn_auth.auth.authorisation.checks import authorised_p from gn_auth.auth.authorisation.privileges import Privilege -from gn_auth.auth.authorisation.resources.base import Resource from gn_auth.auth.authorisation.resources.errors import MissingGroupError +from gn_auth.auth.authorisation.resources.system.models import system_resource +from gn_auth.auth.authorisation.resources.common import ( + grant_access_to_sysadmins) +from gn_auth.auth.authorisation.resources.base import ( + Resource, + resource_from_dbrow) from gn_auth.auth.errors import ( NotFoundError, AuthorisationError, InconsistencyError) from gn_auth.auth.authorisation.roles.models import ( @@ -63,6 +71,13 @@ class MembershipError(AuthorisationError): super().__init__(f"{type(self).__name__}: {error_description}.") +def db_row_to_group(row: sqlite3.Row) -> Group: + """Convert a database row into a group.""" + return Group(UUID(row["group_id"]), + row["group_name"], + json.loads(row["group_metadata"])) + + def user_membership(conn: db.DbConnection, user: User) -> Sequence[Group]: """Returns all the groups that a member belongs to""" query = ( @@ -72,7 +87,7 @@ def user_membership(conn: db.DbConnection, user: User) -> Sequence[Group]: "WHERE group_users.user_id=?") with db.cursor(conn) as cursor: cursor.execute(query, (str(user.user_id),)) - groups = tuple(Group(row[0], row[1], json.loads(row[2])) + groups = tuple(Group(row[0], row[1], json.loads(row[2] or "{}")) for row in cursor.fetchall()) return groups @@ -110,9 +125,10 @@ def create_group( cursor, group_name, ( {"group_description": group_description} if group_description else {})) - group_resource = { + _group_resource_id = uuid4() + _group_resource = { "group_id": str(new_group.group_id), - "resource_id": str(uuid4()), + "resource_id": str(_group_resource_id), "resource_name": group_name, "resource_category_id": str( resource_category_by_key( @@ -123,18 +139,20 @@ def create_group( cursor.execute( "INSERT INTO resources VALUES " "(:resource_id, :resource_name, :resource_category_id, :public)", - group_resource) + _group_resource) cursor.execute( "INSERT INTO group_resources(resource_id, group_id) " "VALUES(:resource_id, :group_id)", - group_resource) + _group_resource) + grant_access_to_sysadmins(cursor, + _group_resource_id, + system_resource(conn).resource_id) add_user_to_group(cursor, new_group, group_leader) revoke_user_role_by_name(cursor, group_leader, "group-creator") - assign_user_role_by_name( - cursor, - group_leader, - UUID(str(group_resource["resource_id"])), - "group-leader") + assign_user_role_by_name(cursor, + group_leader, + _group_resource_id, + "group-leader") return new_group @@ -225,15 +243,56 @@ def is_group_leader(conn: db.DbConnection, user: User, group: Group) -> bool: return "group-leader" in role_names -def all_groups(conn: db.DbConnection) -> Maybe[Sequence[Group]]: +def __build_groups_list_query__( + base: str, + search: Optional[str] = None +) -> tuple[str, tuple[Optional[str], ...]]: + """Build up the query from given search terms.""" + if search is not None and search.strip() != "": + _search = search.strip() + return ((f"{base} WHERE groups.group_name LIKE ? " + "OR groups.group_metadata LIKE ?"), + (f"%{search}%", f"%{search}%")) + return base, tuple() + + +def __limit_results_length__(base: str, start: int = 0, length: int = 0) -> str: + """Add the `LIMIT … OFFSET …` clause to query `base`.""" + if length > 0: + return f"{base} LIMIT {length} OFFSET {start}" + return base + + +def all_groups( + conn: db.DbConnection, + search: Optional[str] = None, + start: int = 0, + length: int = 0 +) -> Maybe[tuple[tuple[Group, ...], int, int]]: """Retrieve all existing groups""" with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM groups") + cursor.execute("SELECT COUNT(*) FROM groups") + _groups_total_count = int(cursor.fetchone()["COUNT(*)"]) + + _qdets = __build_groups_list_query__( + "SELECT COUNT(*) FROM groups", search) + cursor.execute(*__build_groups_list_query__( + "SELECT COUNT(*) FROM groups", search)) + _filtered_total_count = int(cursor.fetchone()["COUNT(*)"]) + + _query, _params = __build_groups_list_query__( + "SELECT * FROM groups", search) + + cursor.execute(__limit_results_length__(_query, start, length), + _params) res = cursor.fetchall() if res: - return Just(tuple( - Group(row["group_id"], row["group_name"], - json.loads(row["group_metadata"])) for row in res)) + return Just(( + tuple( + Group(row["group_id"], row["group_name"], + json.loads(row["group_metadata"])) for row in res), + _groups_total_count, + _filtered_total_count)) return Nothing @@ -260,6 +319,56 @@ def add_user_to_group(cursor: db.DbCursor, the_group: Group, user: User): ("INSERT INTO group_users VALUES (:group_id, :user_id) " "ON CONFLICT (group_id, user_id) DO NOTHING"), {"group_id": str(the_group.group_id), "user_id": str(user.user_id)}) + revoke_user_role_by_name(cursor, user, "group-creator") + + +def resource_from_group(conn: db.DbConnection, the_group: Group) -> Resource: + """Get the resource object that wraps the group for auth purposes.""" + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT " + "resources.resource_id, resources.resource_name, " + "resources.public, resource_categories.* " + "FROM group_resources " + "INNER JOIN resources " + "ON group_resources.resource_id=resources.resource_id " + "INNER JOIN resource_categories " + "ON resources.resource_category_id=resource_categories.resource_category_id " + "WHERE group_resources.group_id=?", + (str(the_group.group_id),)) + results = tuple(resource_from_dbrow(row) for row in cursor.fetchall()) + match len(results): + case 0: + raise InconsistencyError("The group lacks a wrapper resource.") + case 1: + return results[0] + case _: + raise InconsistencyError( + "The group has more than one wrapper resource.") + + +def remove_user_from_group( + conn: db.DbConnection, + group: Group, + user: User, + grp_resource: Resource +): + """Add `user` to `group` as a member.""" + with db.cursor(conn) as cursor: + cursor.execute( + "DELETE FROM group_users " + "WHERE group_id=:group_id AND user_id=:user_id", + {"group_id": str(group.group_id), "user_id": str(user.user_id)}) + cursor.execute( + "DELETE FROM user_roles WHERE user_id=? AND resource_id=?", + (str(user.user_id), str(grp_resource.resource_id))) + assign_user_role_by_name(cursor, + user, + grp_resource.resource_id, + "group-creator") + grant_access_to_sysadmins(cursor, + grp_resource.resource_id, + system_resource(conn).resource_id) @authorised_p( @@ -319,8 +428,8 @@ gjr.status='PENDING'", return tuple(dict(row)for row in cursor.fetchall()) raise AuthorisationError( - "You do not have the appropriate authorisation to access the " - "group's join requests.") + "You need to be the group's leader in order to access the group's join " + "requests.") @authorised_p(("system:group:view-group", "system:group:edit-group"), @@ -489,3 +598,108 @@ def add_resources_to_group(conn: db.DbConnection, "group_id": str(group.group_id), "resource_id": str(rsc.resource_id) } for rsc in resources)) + + +def admin_group(conn: db.DbConnection) -> Either: + """Return a group where at least one system admin is a member.""" + query = ( + "SELECT DISTINCT g.group_id, g.group_name, g.group_metadata " + "FROM roles AS r INNER JOIN user_roles AS ur ON r.role_id=ur.role_id " + "INNER JOIN group_users AS gu ON ur.user_id=gu.user_id " + "INNER JOIN groups AS g ON gu.group_id=g.group_id " + "WHERE role_name='system-administrator'") + with db.cursor(conn) as cursor: + cursor.execute(query) + return monad_from_none_or_value( + Left("There is no group of which the system admininstrator is a " + "member."), + lambda row: Right(Group( + UUID(row["group_id"]), + row["group_name"], + json.loads(row["group_metadata"]))), + cursor.fetchone()) + + +def group_resource(conn: db.DbConnection, group_id: UUID) -> Resource: + """Retrieve the system resource.""" + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT group_resources.group_id, resource_categories.*, " + "resources.resource_id, resources.resource_name, resources.public " + "FROM group_resources INNER JOIN resources " + "ON group_resources.resource_id=resources.resource_id " + "INNER JOIN resource_categories " + "ON resources.resource_category_id=resource_categories.resource_category_id " + "WHERE group_resources.group_id=? " + "AND resource_categories.resource_category_key='group'", + (str(group_id),)) + row = cursor.fetchone() + if row: + return resource_from_dbrow(row) + + raise NotFoundError("Could not find a resource for group with ID " + f"{group_id}") + + +def data_resources( + conn: db.DbConnection, group_id: UUID) -> Iterable[Resource]: + """Fetch a group's data resources.""" + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT resource_ownership.group_id, resources.resource_id, " + "resources.resource_name, resources.public, resource_categories.* " + "FROM resource_ownership INNER JOIN resources " + "ON resource_ownership.resource_id=resources.resource_id " + "INNER JOIN resource_categories " + "ON resources.resource_category_id=resource_categories.resource_category_id " + "WHERE group_id=?", + (str(group_id),)) + yield from (resource_from_dbrow(row) for row in cursor.fetchall()) + + +def group_leaders(conn: db.DbConnection, group_id: UUID) -> Iterable[User]: + """Fetch all of a group's group leaders.""" + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT users.* FROM group_users INNER JOIN group_resources " + "ON group_users.group_id=group_resources.group_id " + "INNER JOIN user_roles " + "ON group_resources.resource_id=user_roles.resource_id " + "INNER JOIN roles " + "ON user_roles.role_id=roles.role_id " + "INNER JOIN users " + "ON user_roles.user_id=users.user_id " + "WHERE group_users.group_id=? " + "AND roles.role_name='group-leader'", + (str(group_id),)) + yield from (User.from_sqlite3_row(row) for row in cursor.fetchall()) + + +def delete_group(conn: db.DbConnection, group_id: UUID): + """ + Delete the group with the given ID + + Parameters: + conn (db.DbConnection): an open connection to an SQLite3 database. + group_id (uuid.UUID): The identifier for the group to delete. + + Returns: + None: It does not return a value. + + Raises: + sqlite3.IntegrityError: if the group has members or linked resources, or + both. + """ + rsc = group_resource(conn, group_id) + with db.cursor(conn) as cursor: + cursor.execute("DELETE FROM group_join_requests WHERE group_id=?", + (str(group_id),)) + cursor.execute("DELETE FROM user_roles WHERE resource_id=?", + (str(rsc.resource_id),)) + cursor.execute( + "DELETE FROM group_resources WHERE group_id=? AND resource_id=?", + (str(group_id), str(rsc.resource_id))) + cursor.execute("DELETE FROM resources WHERE resource_id=?", + (str(rsc.resource_id),)) + cursor.execute("DELETE FROM groups WHERE group_id=?", + (str(group_id),)) 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 |
