aboutsummaryrefslogtreecommitdiff
path: root/gn_auth/auth/authorisation
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth/auth/authorisation')
-rw-r--r--gn_auth/auth/authorisation/resources/checks.py44
-rw-r--r--gn_auth/auth/authorisation/resources/common.py27
-rw-r--r--gn_auth/auth/authorisation/resources/groups/models.py62
-rw-r--r--gn_auth/auth/authorisation/resources/groups/views.py86
-rw-r--r--gn_auth/auth/authorisation/resources/inbredset/models.py49
-rw-r--r--gn_auth/auth/authorisation/resources/inbredset/views.py46
-rw-r--r--gn_auth/auth/authorisation/resources/models.py86
-rw-r--r--gn_auth/auth/authorisation/resources/views.py57
-rw-r--r--gn_auth/auth/authorisation/users/admin/models.py56
-rw-r--r--gn_auth/auth/authorisation/users/masquerade/views.py16
10 files changed, 431 insertions, 98 deletions
diff --git a/gn_auth/auth/authorisation/resources/checks.py b/gn_auth/auth/authorisation/resources/checks.py
index 5484dbf..ce2b821 100644
--- a/gn_auth/auth/authorisation/resources/checks.py
+++ b/gn_auth/auth/authorisation/resources/checks.py
@@ -1,8 +1,11 @@
"""Handle authorisation checks for resources"""
-from uuid import UUID
+import uuid
+import warnings
from functools import reduce
from typing import Sequence
+from gn_libs.privileges import check
+
from .base import Resource
from ...db import sqlite3 as db
@@ -12,7 +15,7 @@ from ..privileges.models import db_row_to_privilege
def __organise_privileges_by_resource_id__(rows):
def __organise__(privs, row):
- resource_id = UUID(row["resource_id"])
+ resource_id = uuid.UUID(row["resource_id"])
return {
**privs,
resource_id: (row["privilege_id"],) + privs.get(
@@ -24,11 +27,14 @@ def __organise_privileges_by_resource_id__(rows):
def authorised_for(conn: db.DbConnection,
user: User,
privileges: tuple[str, ...],
- resource_ids: Sequence[UUID]) -> dict[UUID, bool]:
+ resource_ids: Sequence[uuid.UUID]) -> dict[uuid.UUID, bool]:
"""
Check whether `user` is authorised to access `resources` according to given
`privileges`.
"""
+ warnings.warn(DeprecationWarning(
+ f"The function `{__name__}.authorised_for` is deprecated. Please use "
+ f"`{__name__}.authorised_for_spec`"))
with db.cursor(conn) as cursor:
cursor.execute(
("SELECT ur.*, rp.privilege_id FROM "
@@ -61,6 +67,9 @@ def authorised_for2(
"""
Check that `user` has **ALL** the specified privileges for the resource.
"""
+ warnings.warn(DeprecationWarning(
+ f"The function `{__name__}.authorised_for2` is deprecated. Please use "
+ f"`{__name__}.authorised_for_spec`"))
with db.cursor(conn) as cursor:
_query = (
"SELECT resources.resource_id, user_roles.user_id, roles.role_id, "
@@ -82,3 +91,32 @@ def authorised_for2(
str_privileges = tuple(privilege.privilege_id for privilege in _db_privileges)
return all((requested_privilege in str_privileges)
for requested_privilege in privileges)
+
+
+def authorised_for_spec(
+ conn: db.DbConnection,
+ user_id: uuid.UUID,
+ resource_id: uuid.UUID,
+ auth_spec: str
+) -> bool:
+ """
+ Check that a user, identified with `user_id`, has a set of privileges that
+ satisfy the `auth_spec` for the resource identified with `resource_id`.
+ """
+ with db.cursor(conn) as cursor:
+ _query = (
+ "SELECT resources.resource_id, user_roles.user_id, roles.role_id, "
+ "privileges.* "
+ "FROM resources INNER JOIN user_roles "
+ "ON resources.resource_id=user_roles.resource_id "
+ "INNER JOIN roles ON user_roles.role_id=roles.role_id "
+ "INNER JOIN role_privileges ON roles.role_id=role_privileges.role_id "
+ "INNER JOIN privileges "
+ "ON role_privileges.privilege_id=privileges.privilege_id "
+ "WHERE resources.resource_id=? "
+ "AND user_roles.user_id=?")
+ cursor.execute(
+ _query,
+ (str(resource_id), str(user_id)))
+ _privileges = tuple(row["privilege_id"] for row in cursor.fetchall())
+ return check(auth_spec, _privileges)
diff --git a/gn_auth/auth/authorisation/resources/common.py b/gn_auth/auth/authorisation/resources/common.py
index 5d2b72b..5a48704 100644
--- a/gn_auth/auth/authorisation/resources/common.py
+++ b/gn_auth/auth/authorisation/resources/common.py
@@ -1,10 +1,10 @@
"""Utilities common to more than one resource."""
import uuid
-from sqlite3 import Cursor
+from gn_auth.auth.db import sqlite3 as db
def assign_resource_owner_role(
- cursor: Cursor,
+ cursor: db.DbCursor,
resource_id: uuid.UUID,
user_id: uuid.UUID
) -> dict:
@@ -22,3 +22,26 @@ def assign_resource_owner_role(
"ON CONFLICT (user_id, role_id, resource_id) DO NOTHING",
params)
return params
+
+
+def grant_access_to_sysadmins(
+ cursor: db.DbCursor,
+ resource_id: uuid.UUID,
+ system_resource_id: uuid.UUID
+):
+ """Grant sysadmins access to resource identified by `resource_id`."""
+ cursor.execute(
+ "SELECT role_id FROM roles WHERE role_name='system-administrator'")
+ sysadminroleid = cursor.fetchone()[0]
+
+ cursor.execute(# Fetch sysadmin IDs.
+ "SELECT user_roles.user_id FROM roles INNER JOIN user_roles "
+ "ON roles.role_id=user_roles.role_id "
+ "WHERE role_name='system-administrator' AND resource_id=?",
+ (str(system_resource_id),))
+
+ cursor.executemany(
+ "INSERT INTO user_roles(user_id, role_id, resource_id) "
+ "VALUES (?, ?, ?)",
+ tuple((row["user_id"], sysadminroleid, str(resource_id))
+ for row in cursor.fetchall()))
diff --git a/gn_auth/auth/authorisation/resources/groups/models.py b/gn_auth/auth/authorisation/resources/groups/models.py
index a4aacc7..34f9b93 100644
--- a/gn_auth/auth/authorisation/resources/groups/models.py
+++ b/gn_auth/auth/authorisation/resources/groups/models.py
@@ -313,6 +313,44 @@ 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())
+ assert len(results) == 1, "Expected a single group resource."
+ return results[0]
+
+
+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)})
+ assign_user_role_by_name(cursor,
+ user,
+ grp_resource.resource_id,
+ "group-creator")
@authorised_p(
@@ -617,3 +655,27 @@ def group_leaders(conn: db.DbConnection, group_id: UUID) -> Iterable[User]:
"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.
+ """
+ with db.cursor(conn) as cursor:
+ cursor.execute("DELETE FROM group_join_requests WHERE group_id=?",
+ (str(group_id),))
+ cursor.execute("DELETE FROM group_resources WHERE group_id=?",
+ (str(group_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 28f0645..2aa115a 100644
--- a/gn_auth/auth/authorisation/resources/groups/views.py
+++ b/gn_auth/auth/authorisation/resources/groups/views.py
@@ -6,6 +6,7 @@ import datetime
from functools import partial
from dataclasses import asdict
+import sqlite3
from MySQLdb.cursors import DictCursor
from flask import jsonify, Response, Blueprint, current_app
@@ -18,9 +19,13 @@ from gn_auth.auth.db.sqlite3 import with_db_connection
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,
GroupRole,
@@ -37,6 +42,7 @@ from .models import (Group,
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__)
@@ -408,3 +414,81 @@ def view_group_leaders(group_id: uuid.UUID) -> Response:
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
diff --git a/gn_auth/auth/authorisation/resources/inbredset/models.py b/gn_auth/auth/authorisation/resources/inbredset/models.py
index 64d41e3..2626f3e 100644
--- a/gn_auth/auth/authorisation/resources/inbredset/models.py
+++ b/gn_auth/auth/authorisation/resources/inbredset/models.py
@@ -1,39 +1,12 @@
"""Functions to handle the low-level details regarding populations auth."""
from uuid import UUID, uuid4
+from typing import Sequence, Optional
import sqlite3
-from gn_auth.auth.errors import NotFoundError
+import gn_auth.auth.db.sqlite3 as db
from gn_auth.auth.authentication.users import User
-from gn_auth.auth.authorisation.resources.groups.models import Group
-from gn_auth.auth.authorisation.resources.base import Resource, ResourceCategory
-from gn_auth.auth.authorisation.resources.models import (
- create_resource as _create_resource)
-
-def create_resource(
- cursor: sqlite3.Cursor,
- resource_name: str,
- user: User,
- group: Group,
- public: bool
-) -> Resource:
- """Convenience function to create a resource of type 'inbredset-group'."""
- cursor.execute("SELECT * FROM resource_categories "
- "WHERE resource_category_key='inbredset-group'")
- category = cursor.fetchone()
- if category:
- return _create_resource(cursor,
- resource_name,
- ResourceCategory(
- resource_category_id=UUID(
- category["resource_category_id"]),
- resource_category_key="inbredset-group",
- resource_category_description=category[
- "resource_category_description"]),
- user,
- group,
- public)
- raise NotFoundError("Could not find a 'inbredset-group' resource category.")
+from gn_auth.auth.authorisation.resources.base import Resource
def assign_inbredset_group_owner_role(
@@ -94,3 +67,19 @@ def link_data_to_resource(# pylint: disable=[too-many-arguments, too-many-positi
"VALUES (:resource_id, :data_link_id)",
params)
return params
+
+
+def resource_data(
+ cursor: db.DbCursor,
+ resource_id: UUID,
+ offset: int = 0,
+ limit: Optional[int] = None) -> Sequence[sqlite3.Row]:
+ """Fetch data linked to a inbred-set resource"""
+ cursor.execute(
+ ("SELECT * FROM inbredset_group_resources AS igr "
+ "INNER JOIN linked_inbredset_groups AS lig "
+ "ON igr.data_link_id=lig.data_link_id "
+ "WHERE igr.resource_id=?") + (
+ f" LIMIT {limit} OFFSET {offset}" if bool(limit) else ""),
+ (str(resource_id),))
+ return cursor.fetchall()
diff --git a/gn_auth/auth/authorisation/resources/inbredset/views.py b/gn_auth/auth/authorisation/resources/inbredset/views.py
index 40dd38d..9603b5b 100644
--- a/gn_auth/auth/authorisation/resources/inbredset/views.py
+++ b/gn_auth/auth/authorisation/resources/inbredset/views.py
@@ -1,20 +1,54 @@
"""Views for InbredSet resources."""
+import uuid
+
from pymonad.either import Left, Right, Either
from flask import jsonify, Response, Blueprint, current_app as app
from gn_auth.auth.db import sqlite3 as db
+from gn_auth.auth.errors import NotFoundError
from gn_auth.auth.requests import request_json
-from gn_auth.auth.db.sqlite3 import with_db_connection
+from gn_auth.auth.authentication.users import User
from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
-from gn_auth.auth.authorisation.resources.groups.models import user_group, admin_group
-
-from .models import (create_resource,
- link_data_to_resource,
+from gn_auth.auth.authorisation.resources.base import Resource, ResourceCategory
+from gn_auth.auth.authorisation.resources.groups.models import (Group,
+ user_group,
+ admin_group)
+from gn_auth.auth.authorisation.resources.models import (
+ create_resource as _create_resource)
+
+from .models import (link_data_to_resource,
assign_inbredset_group_owner_role)
popbp = Blueprint("populations", __name__)
+
+def create_resource(
+ cursor: db.DbCursor,
+ resource_name: str,
+ user: User,
+ group: Group,
+ public: bool
+) -> Resource:
+ """Convenience function to create a resource of type 'inbredset-group'."""
+ cursor.execute("SELECT * FROM resource_categories "
+ "WHERE resource_category_key='inbredset-group'")
+ category = cursor.fetchone()
+ if category:
+ return _create_resource(cursor,
+ resource_name,
+ ResourceCategory(
+ resource_category_id=uuid.UUID(
+ category["resource_category_id"]),
+ resource_category_key="inbredset-group",
+ resource_category_description=category[
+ "resource_category_description"]),
+ user,
+ group,
+ public)
+ raise NotFoundError("Could not find a 'inbredset-group' resource category.")
+
+
@popbp.route("/populations/resource-id/<int:speciesid>/<int:inbredsetid>",
methods=["GET"])
def resource_id_by_inbredset_id(speciesid: int, inbredsetid: int) -> Response:
@@ -30,7 +64,7 @@ def resource_id_by_inbredset_id(speciesid: int, inbredsetid: int) -> Response:
(speciesid, inbredsetid))
return cursor.fetchone()
- res = with_db_connection(__res_by_iset_id__)
+ res = db.with_db_connection(__res_by_iset_id__)
if res:
resp = jsonify({"status": "success", "resource-id": res["resource_id"]})
else:
diff --git a/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py
index e538a87..31371fd 100644
--- a/gn_auth/auth/authorisation/resources/models.py
+++ b/gn_auth/auth/authorisation/resources/models.py
@@ -4,8 +4,6 @@ from uuid import UUID, uuid4
from functools import reduce, partial
from typing import Dict, Sequence, Optional
-import sqlite3
-
from gn_auth.auth.db import sqlite3 as db
from gn_auth.auth.authentication.users import User
from gn_auth.auth.db.sqlite3 import with_db_connection
@@ -15,10 +13,12 @@ from gn_auth.auth.authorisation.privileges import Privilege
from gn_auth.auth.authorisation.checks import authorised_p
from gn_auth.auth.errors import NotFoundError, AuthorisationError
-from .checks import authorised_for
+from .system.models import system_resource
+from .checks import authorised_for, authorised_for_spec
from .base import Resource, ResourceCategory, resource_from_dbrow
-from .common import assign_resource_owner_role
+from .common import assign_resource_owner_role, grant_access_to_sysadmins
from .groups.models import Group, is_group_leader
+from .inbredset.models import resource_data as inbredset_resource_data
from .mrna import (
resource_data as mrna_resource_data,
attach_resources_data as mrna_attach_resources_data,
@@ -40,7 +40,7 @@ from .phenotypes.models import (
error_description="Insufficient privileges to create a resource",
oauth2_scope="profile resource")
def create_resource(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
- cursor: sqlite3.Cursor,
+ conn: db.DbConnection,
resource_name: str,
resource_category: ResourceCategory,
user: User,
@@ -48,29 +48,48 @@ def create_resource(# pylint: disable=[too-many-arguments, too-many-positional-a
public: bool
) -> Resource:
"""Create a resource item."""
- resource = Resource(uuid4(), resource_name, resource_category, public)
- cursor.execute(
- "INSERT INTO resources VALUES (?, ?, ?, ?)",
- (str(resource.resource_id),
- resource_name,
- str(resource.resource_category.resource_category_id),
- 1 if resource.public else 0))
- # TODO: @fredmanglis,@rookie101
- # 1. Move the actions below into a (the?) hooks system
- # 2. Do more checks: A resource can have varying hooks depending on type
- # e.g. if mRNA, pheno or geno resource, assign:
- # - "resource-owner"
- # if inbredset-group, assign:
- # - "resource-owner",
- # - "inbredset-group-owner" etc.
- # if resource is of type "group", assign:
- # - group-leader
- cursor.execute("INSERT INTO resource_ownership (group_id, resource_id) "
- "VALUES (?, ?)",
- (str(group.group_id), str(resource.resource_id)))
- assign_resource_owner_role(cursor, resource.resource_id, user.user_id)
-
- return resource
+ with db.cursor(conn) as cursor:
+ resource = Resource(uuid4(), resource_name, resource_category, public)
+ cursor.execute(
+ "INSERT INTO resources VALUES (?, ?, ?, ?)",
+ (str(resource.resource_id),
+ resource_name,
+ str(resource.resource_category.resource_category_id),
+ 1 if resource.public else 0))
+ # TODO: @fredmanglis,@rookie101
+ # 1. Move the actions below into a (the?) hooks system
+ # 2. Do more checks: A resource can have varying hooks depending on type
+ # e.g. if mRNA, pheno or geno resource, assign:
+ # - "resource-owner"
+ # if inbredset-group, assign:
+ # - "resource-owner",
+ # - "inbredset-group-owner" etc.
+ # if resource is of type "group", assign:
+ # - group-leader
+ cursor.execute("INSERT INTO resource_ownership (group_id, resource_id) "
+ "VALUES (?, ?)",
+ (str(group.group_id), str(resource.resource_id)))
+ assign_resource_owner_role(cursor, resource.resource_id, user.user_id)
+ grant_access_to_sysadmins(
+ cursor, resource.resource_id, system_resource(conn).resource_id)
+
+ return resource
+
+
+def delete_resource(conn: db.DbConnection, resource_id: UUID):
+ """Delete a resource."""
+ with db.cursor(conn) as cursor:
+ cursor.execute("DELETE FROM user_roles WHERE resource_id=?",
+ (str(resource_id),))
+ cursor.execute("DELETE FROM resource_roles WHERE resource_id=?",
+ (str(resource_id),))
+ cursor.execute("DELETE FROM group_resources WHERE resource_id=?",
+ (str(resource_id),))
+ cursor.execute("DELETE FROM resource_ownership WHERE resource_id=?",
+ (str(resource_id),))
+ cursor.execute("DELETE FROM resources WHERE resource_id=?",
+ (str(resource_id),))
+
def resource_category_by_id(
conn: db.DbConnection, category_id: UUID) -> ResourceCategory:
@@ -159,7 +178,8 @@ def resource_data(conn, resource, offset: int = 0, limit: Optional[int] = None)
"genotype-metadata": lambda *args: tuple(),
"mrna-metadata": lambda *args: tuple(),
"system": lambda *args: tuple(),
- "group": lambda *args: tuple()
+ "group": lambda *args: tuple(),
+ "inbredset-group": inbredset_resource_data,
}
with db.cursor(conn) as cursor:
return tuple(
@@ -187,9 +207,11 @@ def attach_resource_data(cursor: db.DbCursor, resource: Resource) -> Resource:
def resource_by_id(
conn: db.DbConnection, user: User, resource_id: UUID) -> Resource:
"""Retrieve a resource by its ID."""
- if not authorised_for(
- conn, user, ("group:resource:view-resource",),
- (resource_id,))[resource_id]:
+ if not authorised_for_spec(
+ conn,
+ user.user_id,
+ resource_id,
+ "(OR group:resource:view-resource system:resource:view)"):
raise AuthorisationError(
"You are not authorised to access resource with id "
f"'{resource_id}'.")
diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py
index 0a68927..a960ca3 100644
--- a/gn_auth/auth/authorisation/resources/views.py
+++ b/gn_auth/auth/authorisation/resources/views.py
@@ -39,18 +39,18 @@ from gn_auth.auth.authorisation.roles.models import (
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 .groups.models import Group, user_group
+from .checks import authorised_for, authorised_for_spec
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)
+ get_resource_id, delete_resource as _delete_resource)
resources = Blueprint("resources", __name__)
resources.register_blueprint(popbp, url_prefix="/")
@@ -75,8 +75,7 @@ def create_resource() -> Response:
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,
- db.cursor(conn) as cursor):
+ 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]
@@ -84,7 +83,7 @@ def create_resource() -> Response:
raise MissingGroupError(# Not all resources require an owner group
"User with no group cannot create a resource.")
resource = _create_resource(
- cursor,
+ conn,
resource_name,
resource_category_by_id(conn, resource_category_id),
the_token.user,
@@ -100,7 +99,9 @@ def create_resource() -> Response:
f"{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."""
@@ -673,3 +674,49 @@ def user_resource_roles(resource_id: UUID, user_id: UUID):
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 authorised_for_spec(
+ conn,
+ the_token.user.user_id,
+ resource_id,
+ "(OR group:resource:delete-resource system:resource:delete)"):
+ raise AuthorisationError("You do not have the appropriate "
+ "privileges 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:
+ app.logger.debug("Error!", exc_info=True)
+ return jsonify({
+ "error": "ValueError",
+ "error-description": "An invalid identifier was provided"
+ }), 400
+ except TypeError as _terr:
+ app.logger.debug("Error!", exc_info=True)
+ return jsonify({
+ "error": "TypeError",
+ "error-description": "An invalid identifier was provided"
+ }), 400
diff --git a/gn_auth/auth/authorisation/users/admin/models.py b/gn_auth/auth/authorisation/users/admin/models.py
index 36f3c09..3d68932 100644
--- a/gn_auth/auth/authorisation/users/admin/models.py
+++ b/gn_auth/auth/authorisation/users/admin/models.py
@@ -1,23 +1,55 @@
"""Major function for handling admin users."""
+import warnings
+
from gn_auth.auth.db import sqlite3 as db
from gn_auth.auth.authentication.users import User
+from gn_auth.auth.authorisation.roles.models import Role, db_rows_to_roles
-def make_sys_admin(cursor: db.DbCursor, user: User) -> User:
- """Make a given user into an system admin."""
+
+def sysadmin_role(conn: db.DbConnection) -> Role:
+ """Fetch the `system-administrator` role details."""
+ with db.cursor(conn) as cursor:
+ cursor.execute(
+ "SELECT roles.*, privileges.* "
+ "FROM roles INNER JOIN role_privileges "
+ "ON roles.role_id=role_privileges.role_id "
+ "INNER JOIN privileges "
+ "ON role_privileges.privilege_id=privileges.privilege_id "
+ "WHERE role_name='system-administrator'")
+ results = db_rows_to_roles(cursor.fetchall())
+
+ assert len(results) == 1, (
+ "There should only ever be one 'system-administrator' role.")
+ return results[0]
+
+
+def grant_sysadmin_role(cursor: db.DbCursor, user: User) -> User:
+ """Grant `system-administrator` role to `user`."""
cursor.execute(
"SELECT * FROM roles WHERE role_name='system-administrator'")
admin_role = cursor.fetchone()
- cursor.execute(
- "SELECT * FROM resources AS r "
- "INNER JOIN resource_categories AS rc "
- "ON r.resource_category_id=rc.resource_category_id "
- "WHERE resource_category_key='system'")
- the_system = cursor.fetchone()
- cursor.execute(
+ cursor.execute("SELECT resources.resource_id FROM resources")
+ cursor.executemany(
"INSERT INTO user_roles VALUES (:user_id, :role_id, :resource_id)",
- {
+ tuple({
"user_id": str(user.user_id),
"role_id": admin_role["role_id"],
- "resource_id": the_system["resource_id"]
- })
+ "resource_id": resource_id
+ } for resource_id in cursor.fetchall()))
return user
+
+
+def make_sys_admin(cursor: db.DbCursor, user: User) -> User:
+ """Make a given user into an system admin."""
+ warnings.warn(
+ DeprecationWarning(
+ f"The function `{__name__}.make_sys_admin` will be removed soon"),
+ stacklevel=1)
+ return grant_sysadmin_role(cursor, user)
+
+
+def revoke_sysadmin_role(conn: db.DbConnection, user: User):
+ """Revoke `system-administrator` role from `user`."""
+ with db.cursor(conn) as cursor:
+ cursor.execute("DELETE FROM user_roles WHERE user_id=? AND role_id=?",
+ (str(user.user_id), str(sysadmin_role(conn).role_id)))
diff --git a/gn_auth/auth/authorisation/users/masquerade/views.py b/gn_auth/auth/authorisation/users/masquerade/views.py
index 8b897f2..12a8c97 100644
--- a/gn_auth/auth/authorisation/users/masquerade/views.py
+++ b/gn_auth/auth/authorisation/users/masquerade/views.py
@@ -1,14 +1,14 @@
"""Endpoints for user masquerade"""
from dataclasses import asdict
from uuid import UUID
-from functools import partial
-from flask import request, jsonify, Response, Blueprint
+from flask import request, jsonify, Response, Blueprint, current_app
from gn_auth.auth.errors import InvalidData
+from gn_auth.auth.authorisation.resources.groups.models import user_group
+from ....db import sqlite3 as db
from ...checks import require_json
-from ....db.sqlite3 import with_db_connection
from ....authentication.users import user_by_id
from ....authentication.oauth2.resource_server import require_oauth
@@ -21,13 +21,13 @@ masq = Blueprint("masquerade", __name__)
@require_json
def masquerade() -> Response:
"""Masquerade as a particular user."""
- with require_oauth.acquire("profile user masquerade") as token:
+ with (require_oauth.acquire("profile user masquerade") as token,
+ db.connection(current_app.config["AUTH_DB"]) as conn):
masqueradee_id = UUID(request.json["masquerade_as"])#type: ignore[index]
if masqueradee_id == token.user.user_id:
raise InvalidData("You are not allowed to masquerade as yourself.")
- masq_user = with_db_connection(partial(
- user_by_id, user_id=masqueradee_id))
+ masq_user = user_by_id(conn, user_id=masqueradee_id)
def __masq__(conn):
new_token = masquerade_as(conn, original_token=token, masqueradee=masq_user)
@@ -39,6 +39,8 @@ def masquerade() -> Response:
},
"masquerade_as": {
"user": asdict(masq_user),
- "token": with_db_connection(__masq__)
+ "token": __masq__(conn),
+ **(user_group(conn, masq_user).maybe(# type: ignore[misc]
+ {}, lambda grp: {"group": grp}))
}
})