diff options
Diffstat (limited to 'gn_auth/auth')
-rw-r--r-- | gn_auth/auth/authorisation/resources/groups/models.py | 34 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/resources/groups/views.py | 52 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/users/views.py | 118 |
3 files changed, 180 insertions, 24 deletions
diff --git a/gn_auth/auth/authorisation/resources/groups/models.py b/gn_auth/auth/authorisation/resources/groups/models.py index 1d44ca4..a4aacc7 100644 --- a/gn_auth/auth/authorisation/resources/groups/models.py +++ b/gn_auth/auth/authorisation/resources/groups/models.py @@ -583,3 +583,37 @@ def group_resource(conn: db.DbConnection, group_id: UUID) -> Resource: 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()) diff --git a/gn_auth/auth/authorisation/resources/groups/views.py b/gn_auth/auth/authorisation/resources/groups/views.py index e6c92cb..28f0645 100644 --- a/gn_auth/auth/authorisation/resources/groups/views.py +++ b/gn_auth/auth/authorisation/resources/groups/views.py @@ -22,12 +22,22 @@ from gn_auth.auth.authentication.users import User from gn_auth.auth.authentication.oauth2.resource_server import require_oauth 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) +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_privilege_from_group_role) groups = Blueprint("groups", __name__) @@ -368,3 +378,33 @@ 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))) diff --git a/gn_auth/auth/authorisation/users/views.py b/gn_auth/auth/authorisation/users/views.py index 2ad672d..91e459d 100644 --- a/gn_auth/auth/authorisation/users/views.py +++ b/gn_auth/auth/authorisation/users/views.py @@ -3,10 +3,10 @@ import uuid import sqlite3 import secrets import traceback -from typing import Any -from functools import partial from dataclasses import asdict +from typing import Any, Sequence from urllib.parse import urljoin +from functools import reduce, partial from datetime import datetime, timedelta from email.headerregistry import Address from email_validator import validate_email, EmailNotValidError @@ -560,6 +560,56 @@ def change_password(forgot_password_token): return change_password_page +def __delete_users_individually__(cursor, user_ids, tables): + """Recovery function with dismal performance.""" + _errors = tuple() + for _user_id in user_ids: + for _table, _col in tables: + try: + cursor.execute( + f"DELETE FROM {_table} WHERE {_col}=?", + (str(_user_id),)) + except sqlite3.IntegrityError: + _errors = _errors + ( + (("user_id", _user_id), + ("reason", f"User has data in table {_table}")),) + + return _errors + + +def __fetch_non_deletable_users__(cursor, ids_and_reasons): + """Fetch detail for non-deletable users.""" + def __merge__(acc, curr): + _curr = dict(curr) + _this_dict = acc.get( + curr["user_id"], {"reasons": tuple()}) + _this_dict["reasons"] = _this_dict["reasons"] + (_curr["reason"],) + return {**acc, curr["user_id"]: _this_dict} + + _reasons_by_id = reduce(__merge__, + (dict(row) for row in ids_and_reasons), + {}) + _user_ids = tuple(_reasons_by_id.keys()) + _paramstr = ", ".join(["?"] * len(_user_ids)) + cursor.execute(f"SELECT * FROM users WHERE user_id IN ({_paramstr})", + _user_ids) + return tuple({ + "user": dict(row), + "reasons": _reasons_by_id[row["user_id"]]["reasons"] + } for row in cursor.fetchall()) + + +def __non_deletable_with_reason__( + user_ids: tuple[str, ...], + dbrows: Sequence[sqlite3.Row], + reason: str + ) -> tuple[tuple[tuple[str, str], tuple[str, str]], ...]: + """Build a list of 'non-deletable' user objects.""" + return tuple((("user_id", _uid), ("reason", reason)) + for _uid in user_ids + if _uid in tuple(row["user_id"] for row in dbrows)) + + @users.route("/delete", methods=["POST"]) @require_oauth("profile user role") def delete_users(): @@ -577,13 +627,24 @@ def delete_users(): _form = request_json() _user_ids = _form.get("user_ids", []) - _non_deletable = set((str(_token.user.user_id),)) + _non_deletable = set() + if str(_token.user.user_id) in _user_ids: + _non_deletable.add( + (("user_id", str(_token.user.user_id),), + ("reason", "You are not allowed to delete yourself."))) cursor.execute("SELECT user_id FROM group_users") - _non_deletable.update(row["user_id"] for row in cursor.fetchall()) + _group_members = tuple(row["user_id"] for row in cursor.fetchall()) + _non_deletable.update(__non_deletable_with_reason__( + _user_ids, + cursor.fetchall(), + "User is member of a user group.")) cursor.execute("SELECT user_id FROM oauth2_clients;") - _non_deletable.update(row["user_id"] for row in cursor.fetchall()) + _non_deletable.update(__non_deletable_with_reason__( + _user_ids, + cursor.fetchall(), + "User is registered owner of an OAuth client.")) _important_roles = ( "group-leader", @@ -596,9 +657,13 @@ def delete_users(): "INNER JOIN roles ON user_roles.role_id=roles.role_id " f"WHERE roles.role_name IN ({_paramstr})", _important_roles) - _non_deletable.update(row["user_id"] for row in cursor.fetchall()) + _non_deletable.update(__non_deletable_with_reason__( + _user_ids, + cursor.fetchall(), + f"User holds on of the following roles: {_important_roles}")) - _delete = tuple(uid for uid in _user_ids if uid not in _non_deletable) + _delete = tuple(uid for uid in _user_ids if uid not in + (dict(row)["user_id"] for row in _non_deletable)) _paramstr = ", ".join(["?"] * len(_delete)) if len(_delete) > 0: _dependent_tables = ( @@ -610,31 +675,48 @@ def delete_users(): ("user_credentials", "user_id"), ("user_roles", "user_id"), ("user_verification_codes", "user_id")) - for _table, _col in _dependent_tables: - cursor.execute( - f"DELETE FROM {_table} WHERE {_col} IN ({_paramstr})", - _delete) - + try: + for _table, _col in _dependent_tables: + cursor.execute( + f"DELETE FROM {_table} WHERE {_col} IN ({_paramstr})", + _delete) + except sqlite3.IntegrityError: + _non_deletable.update(__delete_users_individually__( + cursor, _delete, _dependent_tables)) + + _not_deleted = __fetch_non_deletable_users__( + cursor, _non_deletable) + _delete = tuple(# rebuild with those that failed. + _user_id for _user_id in _delete if _user_id not in + tuple(row["user"]["user_id"] for row in _not_deleted)) + _paramstr = ", ".join(["?"] * len(_delete)) cursor.execute( f"DELETE FROM users WHERE user_id IN ({_paramstr})", _delete) _deleted_rows = cursor.rowcount - _diff = len(_user_ids) - _deleted_rows return jsonify({ "total-requested": len(_user_ids), "total-deleted": _deleted_rows, - "not-deleted": _diff, + "not-deleted": _not_deleted, + "deleted": _deleted_rows, "message": ( f"Successfully deleted {_deleted_rows} users." + - (" Some users could not be deleted." if _diff > 0 else "")) + (" Some users could not be deleted." + if len(_user_ids) - _deleted_rows > 0 + else "")) }) + _not_deleted = __fetch_non_deletable_users__(cursor, _non_deletable) + return jsonify({ "total-requested": len(_user_ids), "total-deleted": 0, - "not-deleted": len(_user_ids), + "not-deleted": _not_deleted, + "deleted": 0, "error": "Zero users were deleted", "error_description": ( - "Either no users were selected or all the selected users are " - "system administrators, group members, or resource owners.") + "No users were selected for deletion." + if len(_user_ids) == 0 + else ("The selected users are system administrators, group " + "members, or resource owners.")) }), 400 |