aboutsummaryrefslogtreecommitdiff
path: root/gn_auth/auth
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth/auth')
-rw-r--r--gn_auth/auth/authorisation/resources/groups/models.py34
-rw-r--r--gn_auth/auth/authorisation/resources/groups/views.py52
-rw-r--r--gn_auth/auth/authorisation/users/views.py118
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