diff options
| author | John Nduli | 2024-10-18 15:06:34 +0300 |
|---|---|---|
| committer | Frederick Muriuki Muriithi | 2024-10-18 09:07:16 -0500 |
| commit | 1aeb61f50567e2400c3cc1a18eeef1e59bdc68ac (patch) | |
| tree | 7a4624659f735980345cf10aae101f9e6ec94deb | |
| parent | 0820295202c2fe747c05b93ce0f1c5a604442f69 (diff) | |
| download | genenetwork3-1aeb61f50567e2400c3cc1a18eeef1e59bdc68ac.tar.gz | |
refactor: remove unused gn3.auth modules
30 files changed, 0 insertions, 4081 deletions
diff --git a/gn3/auth/authorisation/checks.py b/gn3/auth/authorisation/checks.py deleted file mode 100644 index 17daca4..0000000 --- a/gn3/auth/authorisation/checks.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Functions to check for authorisation.""" -from functools import wraps -from typing import Callable - -from flask import request, current_app as app - -from gn3.auth import db -from gn3.auth.authorisation.oauth2.resource_server import require_oauth - -from . import privileges as auth_privs -from .errors import InvalidData, AuthorisationError - -def __system_privileges_in_roles__(conn, user): - """ - This really is a hack since groups are not treated as resources at the - moment of writing this. - - We need a way of allowing the user to have the system:group:* privileges. - """ - query = ( - "SELECT DISTINCT p.* FROM users AS u " - "INNER JOIN group_user_roles_on_resources AS guror " - "ON u.user_id=guror.user_id " - "INNER JOIN roles AS r ON guror.role_id=r.role_id " - "INNER JOIN role_privileges AS rp ON r.role_id=rp.role_id " - "INNER JOIN privileges AS p ON rp.privilege_id=p.privilege_id " - "WHERE u.user_id=? AND p.privilege_id LIKE 'system:%'") - with db.cursor(conn) as cursor: - cursor.execute(query, (str(user.user_id),)) - return (row["privilege_id"] for row in cursor.fetchall()) - -def authorised_p( - privileges: tuple[str, ...], - error_description: str = ( - "You lack authorisation to perform requested action"), - oauth2_scope = "profile"): - """Authorisation decorator.""" - assert len(privileges) > 0, "You must provide at least one privilege" - def __build_authoriser__(func: Callable): - @wraps(func) - def __authoriser__(*args, **kwargs): - # the_user = user or (hasattr(g, "user") and g.user) - with require_oauth.acquire(oauth2_scope) as the_token: - the_user = the_token.user - if the_user: - with db.connection(app.config["AUTH_DB"]) as conn: - user_privileges = tuple( - priv.privilege_id for priv in - auth_privs.user_privileges(conn, the_user)) + tuple( - priv_id for priv_id in - __system_privileges_in_roles__(conn, the_user)) - - not_assigned = [ - priv for priv in privileges if priv not in user_privileges] - if len(not_assigned) == 0: - return func(*args, **kwargs) - - raise AuthorisationError(error_description) - return __authoriser__ - return __build_authoriser__ - -def require_json(func): - """Ensure the request has JSON data.""" - @wraps(func) - def __req_json__(*args, **kwargs): - if bool(request.json): - return func(*args, **kwargs) - raise InvalidData("Expected JSON data in the request.") - return __req_json__ diff --git a/gn3/auth/authorisation/data/__init__.py b/gn3/auth/authorisation/data/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/gn3/auth/authorisation/data/__init__.py +++ /dev/null diff --git a/gn3/auth/authorisation/data/genotypes.py b/gn3/auth/authorisation/data/genotypes.py deleted file mode 100644 index 8f901a5..0000000 --- a/gn3/auth/authorisation/data/genotypes.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Handle linking of Genotype data to the Auth(entic|oris)ation system.""" -import uuid -from typing import Iterable - -from MySQLdb.cursors import DictCursor - -import gn3.auth.db as authdb -import gn3.db_utils as gn3db -from gn3.auth.dictify import dictify -from gn3.auth.authorisation.checks import authorised_p -from gn3.auth.authorisation.groups.models import Group - -def linked_genotype_data(conn: authdb.DbConnection) -> Iterable[dict]: - """Retrive genotype data that is linked to user groups.""" - with authdb.cursor(conn) as cursor: - cursor.execute("SELECT * FROM linked_genotype_data") - return (dict(row) for row in cursor.fetchall()) - -@authorised_p(("system:data:link-to-group",), - error_description=( - "You do not have sufficient privileges to link data to (a) " - "group(s)."), - oauth2_scope="profile group resource") -def ungrouped_genotype_data(# pylint: disable=[too-many-arguments] - authconn: authdb.DbConnection, gn3conn: gn3db.Connection, - search_query: str, selected: tuple[dict, ...] = tuple(), - limit: int = 10000, offset: int = 0) -> tuple[ - dict, ...]: - """Retrieve genotype data that is not linked to any user group.""" - params = tuple( - (row["SpeciesId"], row["InbredSetId"], row["GenoFreezeId"]) - for row in linked_genotype_data(authconn)) + tuple( - (row["SpeciesId"], row["InbredSetId"], row["GenoFreezeId"]) - for row in selected) - query = ( - "SELECT s.SpeciesId, iset.InbredSetId, iset.InbredSetName, " - "gf.Id AS GenoFreezeId, gf.Name AS dataset_name, " - "gf.FullName AS dataset_fullname, " - "gf.ShortName AS dataset_shortname " - "FROM Species AS s INNER JOIN InbredSet AS iset " - "ON s.SpeciesId=iset.SpeciesId INNER JOIN GenoFreeze AS gf " - "ON iset.InbredSetId=gf.InbredSetId ") - - if len(params) > 0 or bool(search_query): - query = query + "WHERE " - - if len(params) > 0: - paramstr = ", ".join(["(%s, %s, %s)"] * len(params)) - query = query + ( - "(s.SpeciesId, iset.InbredSetId, gf.Id) " - f"NOT IN ({paramstr}) " - ) + ("AND " if bool(search_query) else "") - - if bool(search_query): - query = query + ( - "CONCAT(gf.Name, ' ', gf.FullName, ' ', gf.ShortName) LIKE %s ") - params = params + ((f"%{search_query}%",),)# type: ignore[operator] - - query = query + f"LIMIT {int(limit)} OFFSET {int(offset)}" - with gn3conn.cursor(DictCursor) as cursor: - cursor.execute( - query, tuple(item for sublist in params for item in sublist)) - return tuple(row for row in cursor.fetchall()) - -@authorised_p( - ("system:data:link-to-group",), - error_description=( - "You do not have sufficient privileges to link data to (a) " - "group(s)."), - oauth2_scope="profile group resource") -def link_genotype_data( - conn: authdb.DbConnection, group: Group, datasets: dict) -> dict: - """Link genotye `datasets` to `group`.""" - with authdb.cursor(conn) as cursor: - cursor.executemany( - "INSERT INTO linked_genotype_data VALUES " - "(:data_link_id, :group_id, :SpeciesId, :InbredSetId, " - ":GenoFreezeId, :dataset_name, :dataset_fullname, " - ":dataset_shortname) " - "ON CONFLICT (SpeciesId, InbredSetId, GenoFreezeId) DO NOTHING", - tuple({ - "data_link_id": str(uuid.uuid4()), - "group_id": str(group.group_id), - **{ - key: value for key,value in dataset.items() if key in ( - "GenoFreezeId", "InbredSetId", "SpeciesId", - "dataset_fullname", "dataset_name", "dataset_shortname") - } - } for dataset in datasets)) - return { - "description": ( - f"Successfully linked {len(datasets)} to group " - f"'{group.group_name}'."), - "group": dictify(group), - "datasets": datasets - } diff --git a/gn3/auth/authorisation/data/mrna.py b/gn3/auth/authorisation/data/mrna.py deleted file mode 100644 index bdfc5c1..0000000 --- a/gn3/auth/authorisation/data/mrna.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Handle linking of mRNA Assay data to the Auth(entic|oris)ation system.""" -import uuid -from typing import Iterable -from MySQLdb.cursors import DictCursor - -import gn3.auth.db as authdb -import gn3.db_utils as gn3db -from gn3.auth.dictify import dictify -from gn3.auth.authorisation.checks import authorised_p -from gn3.auth.authorisation.groups.models import Group - -def linked_mrna_data(conn: authdb.DbConnection) -> Iterable[dict]: - """Retrieve mRNA Assay data that is linked to user groups.""" - with authdb.cursor(conn) as cursor: - cursor.execute("SELECT * FROM linked_mrna_data") - return (dict(row) for row in cursor.fetchall()) - -@authorised_p(("system:data:link-to-group",), - error_description=( - "You do not have sufficient privileges to link data to (a) " - "group(s)."), - oauth2_scope="profile group resource") -def ungrouped_mrna_data(# pylint: disable=[too-many-arguments] - authconn: authdb.DbConnection, gn3conn: gn3db.Connection, - search_query: str, selected: tuple[dict, ...] = tuple(), - limit: int = 10000, offset: int = 0) -> tuple[ - dict, ...]: - """Retrieve mrna data that is not linked to any user group.""" - params = tuple( - (row["SpeciesId"], row["InbredSetId"], row["ProbeFreezeId"], - row["ProbeSetFreezeId"]) - for row in linked_mrna_data(authconn)) + tuple( - (row["SpeciesId"], row["InbredSetId"], row["ProbeFreezeId"], - row["ProbeSetFreezeId"]) - for row in selected) - query = ( - "SELECT s.SpeciesId, iset.InbredSetId, iset.InbredSetName, " - "pf.ProbeFreezeId, pf.Name AS StudyName, psf.Id AS ProbeSetFreezeId, " - "psf.Name AS dataset_name, psf.FullName AS dataset_fullname, " - "psf.ShortName AS dataset_shortname " - "FROM Species AS s INNER JOIN InbredSet AS iset " - "ON s.SpeciesId=iset.SpeciesId INNER JOIN ProbeFreeze AS pf " - "ON iset.InbredSetId=pf.InbredSetId INNER JOIN ProbeSetFreeze AS psf " - "ON pf.ProbeFreezeId=psf.ProbeFreezeId ") + ( - "WHERE " if (len(params) > 0 or bool(search_query)) else "") - - if len(params) > 0: - paramstr = ", ".join(["(%s, %s, %s, %s)"] * len(params)) - query = query + ( - "(s.SpeciesId, iset.InbredSetId, pf.ProbeFreezeId, psf.Id) " - f"NOT IN ({paramstr}) " - ) + ("AND " if bool(search_query) else "") - - if bool(search_query): - query = query + ( - "CONCAT(pf.Name, psf.Name, ' ', psf.FullName, ' ', psf.ShortName) " - "LIKE %s ") - params = params + ((f"%{search_query}%",),)# type: ignore[operator] - - query = query + f"LIMIT {int(limit)} OFFSET {int(offset)}" - with gn3conn.cursor(DictCursor) as cursor: - cursor.execute( - query, tuple(item for sublist in params for item in sublist)) - return tuple(row for row in cursor.fetchall()) - -@authorised_p( - ("system:data:link-to-group",), - error_description=( - "You do not have sufficient privileges to link data to (a) " - "group(s)."), - oauth2_scope="profile group resource") -def link_mrna_data( - conn: authdb.DbConnection, group: Group, datasets: dict) -> dict: - """Link genotye `datasets` to `group`.""" - with authdb.cursor(conn) as cursor: - cursor.executemany( - "INSERT INTO linked_mrna_data VALUES " - "(:data_link_id, :group_id, :SpeciesId, :InbredSetId, " - ":ProbeFreezeId, :ProbeSetFreezeId, :dataset_name, " - ":dataset_fullname, :dataset_shortname) " - "ON CONFLICT " - "(SpeciesId, InbredSetId, ProbeFreezeId, ProbeSetFreezeId) " - "DO NOTHING", - tuple({ - "data_link_id": str(uuid.uuid4()), - "group_id": str(group.group_id), - **{ - key: value for key,value in dataset.items() if key in ( - "SpeciesId", "InbredSetId", "ProbeFreezeId", - "ProbeSetFreezeId", "dataset_fullname", "dataset_name", - "dataset_shortname") - } - } for dataset in datasets)) - return { - "description": ( - f"Successfully linked {len(datasets)} to group " - f"'{group.group_name}'."), - "group": dictify(group), - "datasets": datasets - } diff --git a/gn3/auth/authorisation/data/phenotypes.py b/gn3/auth/authorisation/data/phenotypes.py deleted file mode 100644 index ff98295..0000000 --- a/gn3/auth/authorisation/data/phenotypes.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Handle linking of Phenotype data to the Auth(entic|oris)ation system.""" -import uuid -from typing import Any, Iterable - -from MySQLdb.cursors import DictCursor - -import gn3.auth.db as authdb -import gn3.db_utils as gn3db -from gn3.auth.dictify import dictify -from gn3.auth.authorisation.checks import authorised_p -from gn3.auth.authorisation.groups.models import Group - -def linked_phenotype_data( - authconn: authdb.DbConnection, gn3conn: gn3db.Connection, - species: str = "") -> Iterable[dict[str, Any]]: - """Retrieve phenotype data linked to user groups.""" - authkeys = ("SpeciesId", "InbredSetId", "PublishFreezeId", "PublishXRefId") - with (authdb.cursor(authconn) as authcursor, - gn3conn.cursor(DictCursor) as gn3cursor): - authcursor.execute("SELECT * FROM linked_phenotype_data") - linked = tuple(tuple(row[key] for key in authkeys) - for row in authcursor.fetchall()) - if len(linked) <= 0: - return iter(()) - paramstr = ", ".join(["(%s, %s, %s, %s)"] * len(linked)) - query = ( - "SELECT spc.SpeciesId, spc.Name AS SpeciesName, iset.InbredSetId, " - "iset.InbredSetName, pf.Id AS PublishFreezeId, " - "pf.Name AS dataset_name, pf.FullName AS dataset_fullname, " - "pf.ShortName AS dataset_shortname, pxr.Id AS PublishXRefId " - "FROM " - "Species AS spc " - "INNER JOIN InbredSet AS iset " - "ON spc.SpeciesId=iset.SpeciesId " - "INNER JOIN PublishFreeze AS pf " - "ON iset.InbredSetId=pf.InbredSetId " - "INNER JOIN PublishXRef AS pxr " - "ON pf.InbredSetId=pxr.InbredSetId") + ( - " WHERE" if (len(linked) > 0 or bool(species)) else "") + ( - (" (spc.SpeciesId, iset.InbredSetId, pf.Id, pxr.Id) " - f"IN ({paramstr})") if len(linked) > 0 else "") + ( - " AND"if len(linked) > 0 else "") + ( - " spc.SpeciesName=%s" if bool(species) else "") - params = tuple(item for sublist in linked for item in sublist) + ( - (species,) if bool(species) else tuple()) - gn3cursor.execute(query, params) - return (item for item in gn3cursor.fetchall()) - -@authorised_p(("system:data:link-to-group",), - error_description=( - "You do not have sufficient privileges to link data to (a) " - "group(s)."), - oauth2_scope="profile group resource") -def ungrouped_phenotype_data( - authconn: authdb.DbConnection, gn3conn: gn3db.Connection): - """Retrieve phenotype data that is not linked to any user group.""" - with gn3conn.cursor() as cursor: - params = tuple( - (row["SpeciesId"], row["InbredSetId"], row["PublishFreezeId"], - row["PublishXRefId"]) - for row in linked_phenotype_data(authconn, gn3conn)) - paramstr = ", ".join(["(?, ?, ?, ?)"] * len(params)) - query = ( - "SELECT spc.SpeciesId, spc.SpeciesName, iset.InbredSetId, " - "iset.InbredSetName, pf.Id AS PublishFreezeId, " - "pf.Name AS dataset_name, pf.FullName AS dataset_fullname, " - "pf.ShortName AS dataset_shortname, pxr.Id AS PublishXRefId " - "FROM " - "Species AS spc " - "INNER JOIN InbredSet AS iset " - "ON spc.SpeciesId=iset.SpeciesId " - "INNER JOIN PublishFreeze AS pf " - "ON iset.InbredSetId=pf.InbredSetId " - "INNER JOIN PublishXRef AS pxr " - "ON pf.InbredSetId=pxr.InbredSetId") - if len(params) > 0: - query = query + ( - f" WHERE (iset.InbredSetId, pf.Id, pxr.Id) NOT IN ({paramstr})") - - cursor.execute(query, params) - return tuple(dict(row) for row in cursor.fetchall()) - - return tuple() - -def __traits__(gn3conn: gn3db.Connection, params: tuple[dict, ...]) -> tuple[dict, ...]: - """An internal utility function. Don't use outside of this module.""" - if len(params) < 1: - return tuple() - paramstr = ", ".join(["(%s, %s, %s, %s)"] * len(params)) - with gn3conn.cursor(DictCursor) as cursor: - cursor.execute( - "SELECT spc.SpeciesId, iset.InbredSetId, pf.Id AS PublishFreezeId, " - "pf.Name AS dataset_name, pf.FullName AS dataset_fullname, " - "pf.ShortName AS dataset_shortname, pxr.Id AS PublishXRefId " - "FROM " - "Species AS spc " - "INNER JOIN InbredSet AS iset " - "ON spc.SpeciesId=iset.SpeciesId " - "INNER JOIN PublishFreeze AS pf " - "ON iset.InbredSetId=pf.InbredSetId " - "INNER JOIN PublishXRef AS pxr " - "ON pf.InbredSetId=pxr.InbredSetId " - "WHERE (spc.SpeciesName, iset.InbredSetName, pf.Name, pxr.Id) " - f"IN ({paramstr})", - tuple( - itm for sublist in ( - (item["species"], item["group"], item["dataset"], item["name"]) - for item in params) - for itm in sublist)) - return cursor.fetchall() - -@authorised_p(("system:data:link-to-group",), - error_description=( - "You do not have sufficient privileges to link data to (a) " - "group(s)."), - oauth2_scope="profile group resource") -def link_phenotype_data( - authconn:authdb.DbConnection, gn3conn: gn3db.Connection, group: Group, - traits: tuple[dict, ...]) -> dict: - """Link phenotype traits to a user group.""" - with authdb.cursor(authconn) as cursor: - params = tuple({ - "data_link_id": str(uuid.uuid4()), - "group_id": str(group.group_id), - **item - } for item in __traits__(gn3conn, traits)) - cursor.executemany( - "INSERT INTO linked_phenotype_data " - "VALUES (" - ":data_link_id, :group_id, :SpeciesId, :InbredSetId, " - ":PublishFreezeId, :dataset_name, :dataset_fullname, " - ":dataset_shortname, :PublishXRefId" - ")", - params) - return { - "description": ( - f"Successfully linked {len(traits)} traits to group."), - "group": dictify(group), - "traits": params - } diff --git a/gn3/auth/authorisation/data/views.py b/gn3/auth/authorisation/data/views.py deleted file mode 100644 index 81811dd..0000000 --- a/gn3/auth/authorisation/data/views.py +++ /dev/null @@ -1,310 +0,0 @@ -"""Handle data endpoints.""" -import sys -import uuid -import json -from typing import Any -from functools import partial - -import redis -from MySQLdb.cursors import DictCursor -from authlib.integrations.flask_oauth2.errors import _HTTPException -from flask import request, jsonify, Response, Blueprint, current_app as app - -import gn3.db_utils as gn3db -from gn3 import jobs -from gn3.commands import run_async_cmd -from gn3.db.traits import build_trait_name - -from gn3.auth import db -from gn3.auth.db_utils import with_db_connection - -from gn3.auth.authorisation.checks import require_json -from gn3.auth.authorisation.errors import InvalidData, NotFoundError - -from gn3.auth.authorisation.groups.models import group_by_id - -from gn3.auth.authorisation.users.models import user_resource_roles - -from gn3.auth.authorisation.resources.checks import authorised_for -from gn3.auth.authorisation.resources.models import ( - user_resources, public_resources, attach_resources_data) - -from gn3.auth.authorisation.oauth2.resource_server import require_oauth - -from gn3.auth.authorisation.users import User -from gn3.auth.authorisation.data.phenotypes import link_phenotype_data -from gn3.auth.authorisation.data.mrna import link_mrna_data, ungrouped_mrna_data -from gn3.auth.authorisation.data.genotypes import ( - link_genotype_data, ungrouped_genotype_data) - -data = Blueprint("data", __name__) - -@data.route("species") -def list_species() -> Response: - """List all available species information.""" - with (gn3db.database_connection(app.config["SQL_URI"]) as gn3conn, - gn3conn.cursor(DictCursor) as cursor): - cursor.execute("SELECT * FROM Species") - return jsonify(tuple(dict(row) for row in cursor.fetchall())) - -@data.route("/authorisation", methods=["POST"]) -@require_json -def authorisation() -> Response: - """Retrive the authorisation level for datasets/traits for the user.""" - # Access endpoint with something like: - # curl -X POST http://127.0.0.1:8080/api/oauth2/data/authorisation \ - # -H "Content-Type: application/json" \ - # -d '{"traits": ["HC_M2_0606_P::1442370_at", "BXDGeno::01.001.695", - # "BXDPublish::10001"]}' - db_uri = app.config["AUTH_DB"] - privileges = {} - user = User(uuid.uuid4(), "anon@ymous.user", "Anonymous User") - with db.connection(db_uri) as auth_conn: - try: - with require_oauth.acquire("profile group resource") as the_token: - user = the_token.user - resources = attach_resources_data( - auth_conn, user_resources(auth_conn, the_token.user)) - resources_roles = user_resource_roles(auth_conn, the_token.user) - privileges = { - resource_id: tuple( - privilege.privilege_id - for roles in resources_roles[resource_id] - for privilege in roles.privileges)#("group:resource:view-resource",) - for resource_id, is_authorised - in authorised_for( - auth_conn, the_token.user, - ("group:resource:view-resource",), tuple( - resource.resource_id for resource in resources)).items() - if is_authorised - } - except _HTTPException as exc: - err_msg = json.loads(exc.body) - if err_msg["error"] == "missing_authorization": - resources = attach_resources_data( - auth_conn, public_resources(auth_conn)) - else: - raise exc from None - - def __gen_key__(resource, data_item): - if resource.resource_category.resource_category_key.lower() == "phenotype": - return ( - f"{resource.resource_category.resource_category_key.lower()}::" - f"{data_item['dataset_name']}::{data_item['PublishXRefId']}") - return ( - f"{resource.resource_category.resource_category_key.lower()}::" - f"{data_item['dataset_name']}") - - data_to_resource_map = { - __gen_key__(resource, data_item): resource.resource_id - for resource in resources - for data_item in resource.resource_data - } - privileges = { - **{ - resource.resource_id: ("system:resource:public-read",) - for resource in resources if resource.public - }, - **privileges} - - args = request.get_json() - traits_names = args["traits"] # type: ignore[index] - def __translate__(val): - return { - "Temp": "Temp", - "ProbeSet": "mRNA", - "Geno": "Genotype", - "Publish": "Phenotype" - }[val] - - def __trait_key__(trait): - dataset_type = __translate__(trait['db']['dataset_type']).lower() - dataset_name = trait["db"]["dataset_name"] - if dataset_type == "phenotype": - return f"{dataset_type}::{dataset_name}::{trait['trait_name']}" - return f"{dataset_type}::{dataset_name}" - - return jsonify(tuple( - { - "user": user._asdict(), - **{key:trait[key] for key in ("trait_fullname", "trait_name")}, - "dataset_name": trait["db"]["dataset_name"], - "dataset_type": __translate__(trait["db"]["dataset_type"]), - "resource_id": data_to_resource_map.get(__trait_key__(trait)), - "privileges": privileges.get( - data_to_resource_map.get( - __trait_key__(trait), - uuid.UUID("4afa415e-94cb-4189-b2c6-f9ce2b6a878d")), - tuple()) + ( - # Temporary traits do not exist in db: Set them - # as public-read - ("system:resource:public-read",) - if trait["db"]["dataset_type"] == "Temp" - else tuple()) - } for trait in - (build_trait_name(trait_fullname) - for trait_fullname in traits_names))) - -def __search_mrna__(): - query = __request_key__("query", "") - limit = int(__request_key__("limit", 10000)) - offset = int(__request_key__("offset", 0)) - with gn3db.database_connection(app.config["SQL_URI"]) as gn3conn: - __ungrouped__ = partial( - ungrouped_mrna_data, gn3conn=gn3conn, search_query=query, - selected=__request_key_list__("selected"), - limit=limit, offset=offset) - return jsonify(with_db_connection(__ungrouped__)) - -def __request_key__(key: str, default: Any = ""): - if bool(request.json): - return request.json.get(#type: ignore[union-attr] - key, request.args.get(key, request.form.get(key, default))) - return request.args.get(key, request.form.get(key, default)) - -def __request_key_list__(key: str, default: tuple[Any, ...] = tuple()): - if bool(request.json): - return (request.json.get(key,[])#type: ignore[union-attr] - or request.args.getlist(key) or request.form.getlist(key) - or list(default)) - return (request.args.getlist(key) - or request.form.getlist(key) or list(default)) - -def __search_genotypes__(): - query = __request_key__("query", "") - limit = int(__request_key__("limit", 10000)) - offset = int(__request_key__("offset", 0)) - with gn3db.database_connection(app.config["SQL_URI"]) as gn3conn: - __ungrouped__ = partial( - ungrouped_genotype_data, gn3conn=gn3conn, search_query=query, - selected=__request_key_list__("selected"), - limit=limit, offset=offset) - return jsonify(with_db_connection(__ungrouped__)) - -def __search_phenotypes__(): - # launch the external process to search for phenotypes - redisuri = app.config["REDIS_URI"] - with redis.Redis.from_url(redisuri, decode_responses=True) as redisconn: - job_id = uuid.uuid4() - selected = __request_key__("selected_traits", []) - command =[ - sys.executable, "-m", "scripts.search_phenotypes", - __request_key__("species_name"), - __request_key__("query"), - str(job_id), - f"--host={__request_key__('gn3_server_uri')}", - f"--auth-db-uri={app.config['AUTH_DB']}", - f"--gn3-db-uri={app.config['SQL_URI']}", - f"--redis-uri={redisuri}", - f"--per-page={__request_key__('per_page')}"] +( - [f"--selected={json.dumps(selected)}"] - if len(selected) > 0 else []) - jobs.create_job(redisconn, { - "job_id": job_id, "command": command, "status": "queued", - "search_results": tuple()}) - return jsonify({ - "job_id": job_id, - "command_id": run_async_cmd( - redisconn, app.config.get("REDIS_JOB_QUEUE"), command), - "command": command - }) - -@data.route("/search", methods=["GET"]) -@require_oauth("profile group resource") -def search_unlinked_data(): - """Search for various unlinked data.""" - dataset_type = request.json["dataset_type"] - search_fns = { - "mrna": __search_mrna__, - "genotype": __search_genotypes__, - "phenotype": __search_phenotypes__ - } - return search_fns[dataset_type]() - -@data.route("/search/phenotype/<uuid:job_id>", methods=["GET"]) -def pheno_search_results(job_id: uuid.UUID) -> Response: - """Get the search results from the external script""" - def __search_error__(err): - raise NotFoundError(err["error_description"]) - redisuri = app.config["REDIS_URI"] - with redis.Redis.from_url(redisuri, decode_responses=True) as redisconn: - return jobs.job(redisconn, job_id).either( - __search_error__, jsonify) - -@data.route("/link/genotype", methods=["POST"]) -def link_genotypes() -> Response: - """Link genotype data to group.""" - def __values__(form) -> dict[str, Any]: - if not bool(form.get("species_name", "").strip()): - raise InvalidData("Expected 'species_name' not provided.") - if not bool(form.get("group_id")): - raise InvalidData("Expected 'group_id' not provided.",) - try: - _group_id = uuid.UUID(form.get("group_id")) - except TypeError as terr: - raise InvalidData("Expected a UUID for 'group_id' value.") from terr - if not bool(form.get("selected")): - raise InvalidData("Expected at least one dataset to be provided.") - return { - "group_id": uuid.UUID(form.get("group_id")), - "datasets": form.get("selected") - } - - def __link__(conn: db.DbConnection, group_id: uuid.UUID, datasets: dict): - return link_genotype_data(conn, group_by_id(conn, group_id), datasets) - - return jsonify(with_db_connection( - partial(__link__, **__values__(request.json)))) - -@data.route("/link/mrna", methods=["POST"]) -def link_mrna() -> Response: - """Link mrna data to group.""" - def __values__(form) -> dict[str, Any]: - if not bool(form.get("species_name", "").strip()): - raise InvalidData("Expected 'species_name' not provided.") - if not bool(form.get("group_id")): - raise InvalidData("Expected 'group_id' not provided.",) - try: - _group_id = uuid.UUID(form.get("group_id")) - except TypeError as terr: - raise InvalidData("Expected a UUID for 'group_id' value.") from terr - if not bool(form.get("selected")): - raise InvalidData("Expected at least one dataset to be provided.") - return { - "group_id": uuid.UUID(form.get("group_id")), - "datasets": form.get("selected") - } - - def __link__(conn: db.DbConnection, group_id: uuid.UUID, datasets: dict): - return link_mrna_data(conn, group_by_id(conn, group_id), datasets) - - return jsonify(with_db_connection( - partial(__link__, **__values__(request.json)))) - -@data.route("/link/phenotype", methods=["POST"]) -def link_phenotype() -> Response: - """Link phenotype data to group.""" - def __values__(form): - if not bool(form.get("species_name", "").strip()): - raise InvalidData("Expected 'species_name' not provided.") - if not bool(form.get("group_id")): - raise InvalidData("Expected 'group_id' not provided.",) - try: - _group_id = uuid.UUID(form.get("group_id")) - except TypeError as terr: - raise InvalidData("Expected a UUID for 'group_id' value.") from terr - if not bool(form.get("selected")): - raise InvalidData("Expected at least one dataset to be provided.") - return { - "group_id": uuid.UUID(form["group_id"]), - "traits": form["selected"] - } - - with gn3db.database_connection(app.config["SQL_URI"]) as gn3conn: - def __link__(conn: db.DbConnection, group_id: uuid.UUID, - traits: tuple[dict, ...]) -> dict: - return link_phenotype_data( - conn, gn3conn, group_by_id(conn, group_id), traits) - - return jsonify(with_db_connection( - partial(__link__, **__values__(request.json)))) diff --git a/gn3/auth/authorisation/groups/__init__.py b/gn3/auth/authorisation/groups/__init__.py deleted file mode 100644 index 1cb0bba..0000000 --- a/gn3/auth/authorisation/groups/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Initialise the `gn3.auth.authorisation.groups` package""" - -from .models import Group, GroupRole diff --git a/gn3/auth/authorisation/groups/data.py b/gn3/auth/authorisation/groups/data.py deleted file mode 100644 index ee6f70e..0000000 --- a/gn3/auth/authorisation/groups/data.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Handles the resource objects' data.""" -from MySQLdb.cursors import DictCursor - -from gn3 import db_utils as gn3db -from gn3.auth import db as authdb -from gn3.auth.authorisation.groups import Group -from gn3.auth.authorisation.checks import authorised_p -from gn3.auth.authorisation.errors import NotFoundError - -def __fetch_mrna_data_by_ids__( - conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[ - dict, ...]: - """Fetch mRNA Assay data by ID.""" - with conn.cursor(DictCursor) as cursor: - paramstr = ", ".join(["%s"] * len(dataset_ids)) - cursor.execute( - "SELECT psf.Id, psf.Name AS dataset_name, " - "psf.FullName AS dataset_fullname, " - "ifiles.GN_AccesionId AS accession_id FROM ProbeSetFreeze AS psf " - "INNER JOIN InfoFiles AS ifiles ON psf.Name=ifiles.InfoPageName " - f"WHERE psf.Id IN ({paramstr})", - dataset_ids) - res = cursor.fetchall() - if res: - return tuple(dict(row) for row in res) - raise NotFoundError("Could not find mRNA Assay data with the given ID.") - -def __fetch_geno_data_by_ids__( - conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[ - dict, ...]: - """Fetch genotype data by ID.""" - with conn.cursor(DictCursor) as cursor: - paramstr = ", ".join(["%s"] * len(dataset_ids)) - cursor.execute( - "SELECT gf.Id, gf.Name AS dataset_name, " - "gf.FullName AS dataset_fullname, " - "ifiles.GN_AccesionId AS accession_id FROM GenoFreeze AS gf " - "INNER JOIN InfoFiles AS ifiles ON gf.Name=ifiles.InfoPageName " - f"WHERE gf.Id IN ({paramstr})", - dataset_ids) - res = cursor.fetchall() - if res: - return tuple(dict(row) for row in res) - raise NotFoundError("Could not find Genotype data with the given ID.") - -def __fetch_pheno_data_by_ids__( - conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[ - dict, ...]: - """Fetch phenotype data by ID.""" - with conn.cursor(DictCursor) as cursor: - paramstr = ", ".join(["%s"] * len(dataset_ids)) - cursor.execute( - "SELECT pxf.Id, iset.InbredSetName, pf.Id AS dataset_id, " - "pf.Name AS dataset_name, pf.FullName AS dataset_fullname, " - "ifiles.GN_AccesionId AS accession_id " - "FROM PublishXRef AS pxf " - "INNER JOIN InbredSet AS iset ON pxf.InbredSetId=iset.InbredSetId " - "INNER JOIN PublishFreeze AS pf ON iset.InbredSetId=pf.InbredSetId " - "INNER JOIN InfoFiles AS ifiles ON pf.Name=ifiles.InfoPageName " - f"WHERE pxf.Id IN ({paramstr})", - dataset_ids) - res = cursor.fetchall() - if res: - return tuple(dict(row) for row in res) - raise NotFoundError( - "Could not find Phenotype/Publish data with the given IDs.") - -def __fetch_data_by_id( - conn: gn3db.Connection, dataset_type: str, - dataset_ids: tuple[str, ...]) -> tuple[dict, ...]: - """Fetch data from MySQL by IDs.""" - fetch_fns = { - "mrna": __fetch_mrna_data_by_ids__, - "genotype": __fetch_geno_data_by_ids__, - "phenotype": __fetch_pheno_data_by_ids__ - } - return fetch_fns[dataset_type](conn, dataset_ids) - -@authorised_p(("system:data:link-to-group",), - error_description=( - "You do not have sufficient privileges to link data to (a) " - "group(s)."), - oauth2_scope="profile group resource") -def link_data_to_group( - 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.""" - the_data = __fetch_data_by_id(gn3conn, dataset_type, dataset_ids) - with authdb.cursor(authconn) as cursor: - params = tuple({ - "group_id": str(group.group_id), "dataset_type": { - "mrna": "mRNA", "genotype": "Genotype", - "phenotype": "Phenotype" - }[dataset_type], - "dataset_or_trait_id": item["Id"], - "dataset_name": item["dataset_name"], - "dataset_fullname": item["dataset_fullname"], - "accession_id": item["accession_id"] - } for item in the_data) - cursor.executemany( - "INSERT INTO linked_group_data VALUES" - "(:group_id, :dataset_type, :dataset_or_trait_id, :dataset_name, " - ":dataset_fullname, :accession_id)", - params) - return params diff --git a/gn3/auth/authorisation/groups/models.py b/gn3/auth/authorisation/groups/models.py deleted file mode 100644 index 7212a78..0000000 --- a/gn3/auth/authorisation/groups/models.py +++ /dev/null @@ -1,400 +0,0 @@ -"""Handle the management of resource/user groups.""" -import json -from uuid import UUID, uuid4 -from functools import reduce -from typing import Any, Sequence, Iterable, Optional, NamedTuple - -from flask import g -from pymonad.maybe import Just, Maybe, Nothing - -from gn3.auth import db -from gn3.auth.dictify import dictify -from gn3.auth.authorisation.users import User, user_by_id - -from ..checks import authorised_p -from ..privileges import Privilege -from ..errors import NotFoundError, AuthorisationError, InconsistencyError -from ..roles.models import ( - Role, create_role, check_user_editable, revoke_user_role_by_name, - assign_user_role_by_name) - -class Group(NamedTuple): - """Class representing a group.""" - group_id: UUID - group_name: str - group_metadata: dict[str, Any] - - def dictify(self): - """Return a dict representation of `Group` objects.""" - return { - "group_id": self.group_id, "group_name": self.group_name, - "group_metadata": self.group_metadata - } - -DUMMY_GROUP = Group( - group_id=UUID("77cee65b-fe29-4383-ae41-3cb3b480cc70"), - group_name="GN3_DUMMY_GROUP", - group_metadata={ - "group-description": "This is a dummy group to use as a placeholder" - }) - -class GroupRole(NamedTuple): - """Class representing a role tied/belonging to a group.""" - group_role_id: UUID - group: Group - role: Role - - def dictify(self) -> dict[str, Any]: - """Return a dict representation of `GroupRole` objects.""" - return { - "group_role_id": self.group_role_id, "group": dictify(self.group), - "role": dictify(self.role) - } - -class GroupCreationError(AuthorisationError): - """Raised whenever a group creation fails""" - -class MembershipError(AuthorisationError): - """Raised when there is an error with a user's membership to a group.""" - - def __init__(self, user: User, groups: Sequence[Group]): - """Initialise the `MembershipError` exception object.""" - groups_str = ", ".join(group.group_name for group in groups) - error_description = ( - f"User '{user.name} ({user.email})' is a member of {len(groups)} " - f"groups ({groups_str})") - super().__init__(f"{type(self).__name__}: {error_description}.") - -def user_membership(conn: db.DbConnection, user: User) -> Sequence[Group]: - """Returns all the groups that a member belongs to""" - query = ( - "SELECT groups.group_id, group_name, groups.group_metadata " - "FROM group_users INNER JOIN groups " - "ON group_users.group_id=groups.group_id " - "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])) - for row in cursor.fetchall()) - - return groups - -@authorised_p( - privileges = ("system:group:create-group",), - error_description = ( - "You do not have the appropriate privileges to enable you to " - "create a new group."), - oauth2_scope = "profile group") -def create_group( - conn: db.DbConnection, group_name: str, group_leader: User, - group_description: Optional[str] = None) -> Group: - """Create a new group.""" - user_groups = user_membership(conn, group_leader) - if len(user_groups) > 0: - raise MembershipError(group_leader, user_groups) - - with db.cursor(conn) as cursor: - new_group = save_group( - cursor, group_name,( - {"group_description": group_description} - if group_description else {})) - 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, "group-leader") - return new_group - -@authorised_p(("group:role:create-role",), - error_description="Could not create the group role") -def create_group_role( - conn: db.DbConnection, group: Group, role_name: str, - privileges: Iterable[Privilege]) -> GroupRole: - """Create a role attached to a group.""" - with db.cursor(conn) as cursor: - group_role_id = uuid4() - role = create_role(cursor, role_name, privileges) - cursor.execute( - ("INSERT INTO group_roles(group_role_id, group_id, role_id) " - "VALUES(?, ?, ?)"), - (str(group_role_id), str(group.group_id), str(role.role_id))) - - return GroupRole(group_role_id, group, role) - -def authenticated_user_group(conn) -> Maybe: - """ - Returns the currently authenticated user's group. - - Look into returning a Maybe object. - """ - user = g.user - with db.cursor(conn) as cursor: - cursor.execute( - ("SELECT groups.* FROM group_users " - "INNER JOIN groups ON group_users.group_id=groups.group_id " - "WHERE group_users.user_id = ?"), - (str(user.user_id),)) - groups = tuple(Group(UUID(row[0]), row[1], json.loads(row[2] or "{}")) - for row in cursor.fetchall()) - - if len(groups) > 1: - raise MembershipError(user, groups) - - if len(groups) == 1: - return Just(groups[0]) - - return Nothing - -def user_group(conn: db.DbConnection, user: User) -> Maybe[Group]: - """Returns the given user's group""" - with db.cursor(conn) as cursor: - cursor.execute( - ("SELECT groups.group_id, groups.group_name, groups.group_metadata " - "FROM group_users " - "INNER JOIN groups ON group_users.group_id=groups.group_id " - "WHERE group_users.user_id = ?"), - (str(user.user_id),)) - groups = tuple( - Group(UUID(row[0]), row[1], json.loads(row[2] or "{}")) - for row in cursor.fetchall()) - - if len(groups) > 1: - raise MembershipError(user, groups) - - if len(groups) == 1: - return Just(groups[0]) - - return Nothing - -def is_group_leader(conn: db.DbConnection, user: User, group: Group) -> bool: - """Check whether the given `user` is the leader of `group`.""" - - ugroup = user_group(conn, user).maybe( - False, lambda val: val) # type: ignore[arg-type, misc] - if not group: - # User cannot be a group leader if not a member of ANY group - return False - - if not ugroup == group: - # User cannot be a group leader if not a member of THIS group - return False - - with db.cursor(conn) as cursor: - cursor.execute( - ("SELECT roles.role_name FROM user_roles LEFT JOIN roles " - "ON user_roles.role_id = roles.role_id WHERE user_id = ?"), - (str(user.user_id),)) - role_names = tuple(row[0] for row in cursor.fetchall()) - - return "group-leader" in role_names - -def all_groups(conn: db.DbConnection) -> Maybe[Sequence[Group]]: - """Retrieve all existing groups""" - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM groups") - 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 Nothing - -def save_group( - cursor: db.DbCursor, group_name: str, - group_metadata: dict[str, Any]) -> Group: - """Save a group to db""" - the_group = Group(uuid4(), group_name, group_metadata) - cursor.execute( - ("INSERT INTO groups " - "VALUES(:group_id, :group_name, :group_metadata) " - "ON CONFLICT (group_id) DO UPDATE SET " - "group_name=:group_name, group_metadata=:group_metadata"), - {"group_id": str(the_group.group_id), "group_name": the_group.group_name, - "group_metadata": json.dumps(the_group.group_metadata)}) - return the_group - -def add_user_to_group(cursor: db.DbCursor, the_group: Group, user: User): - """Add `user` to `the_group` as a member.""" - cursor.execute( - ("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)}) - -@authorised_p( - privileges = ("system:group:view-group",), - error_description = ( - "You do not have the appropriate privileges to access the list of users" - " in the group.")) -def group_users(conn: db.DbConnection, group_id: UUID) -> Iterable[User]: - """Retrieve all users that are members of group with id `group_id`.""" - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT u.* FROM group_users AS gu INNER JOIN users AS u " - "ON gu.user_id = u.user_id WHERE gu.group_id=:group_id", - {"group_id": str(group_id)}) - results = cursor.fetchall() - - return (User(UUID(row["user_id"]), row["email"], row["name"]) - for row in results) - -@authorised_p( - privileges = ("system:group:view-group",), - error_description = ( - "You do not have the appropriate privileges to access the group.")) -def group_by_id(conn: db.DbConnection, group_id: UUID) -> Group: - """Retrieve a group by its ID""" - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM groups WHERE group_id=:group_id", - {"group_id": str(group_id)}) - row = cursor.fetchone() - if row: - return Group( - UUID(row["group_id"]), - row["group_name"], - json.loads(row["group_metadata"])) - - raise NotFoundError(f"Could not find group with ID '{group_id}'.") - -@authorised_p(("system:group:view-group", "system:group:edit-group"), - error_description=("You do not have the appropriate authorisation" - " to act upon the join requests."), - oauth2_scope="profile group") -def join_requests(conn: db.DbConnection, user: User): - """List all the join requests for the user's group.""" - with db.cursor(conn) as cursor: - group = user_group(conn, user).maybe(DUMMY_GROUP, lambda grp: grp)# type: ignore[misc] - if group != DUMMY_GROUP and is_group_leader(conn, user, group): - cursor.execute( - "SELECT gjr.*, u.email, u.name FROM group_join_requests AS gjr " - "INNER JOIN users AS u ON gjr.requester_id=u.user_id " - "WHERE gjr.group_id=? AND gjr.status='PENDING'", - (str(group.group_id),)) - 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.") - -@authorised_p(("system:group:view-group", "system:group:edit-group"), - error_description=("You do not have the appropriate authorisation" - " to act upon the join requests."), - oauth2_scope="profile group") -def accept_reject_join_request( - conn: db.DbConnection, request_id: UUID, user: User, status: str) -> dict: - """Accept/Reject a join request.""" - assert status in ("ACCEPTED", "REJECTED"), f"Invalid status '{status}'." - with db.cursor(conn) as cursor: - group = user_group(conn, user).maybe(DUMMY_GROUP, lambda grp: grp) # type: ignore[misc] - cursor.execute("SELECT * FROM group_join_requests WHERE request_id=?", - (str(request_id),)) - row = cursor.fetchone() - if row: - if group.group_id == UUID(row["group_id"]): - try: - the_user = user_by_id(conn, UUID(row["requester_id"])) - if status == "ACCEPTED": - add_user_to_group(cursor, group, the_user) - revoke_user_role_by_name(cursor, the_user, "group-creator") - cursor.execute( - "UPDATE group_join_requests SET status=? " - "WHERE request_id=?", - (status, str(request_id))) - return {"request_id": request_id, "status": status} - except NotFoundError as nfe: - raise InconsistencyError( - "Could not find user associated with join request." - ) from nfe - raise AuthorisationError( - "You cannot act on other groups join requests") - raise NotFoundError(f"Could not find request with ID '{request_id}'") - -def __organise_privileges__(acc, row): - role_id = UUID(row["role_id"]) - role = acc.get(role_id, False) - if role: - return { - **acc, - role_id: Role( - role.role_id, role.role_name, - bool(int(row["user_editable"])), - role.privileges + ( - Privilege(row["privilege_id"], - row["privilege_description"]),)) - } - return { - **acc, - role_id: Role( - UUID(row["role_id"]), row["role_name"], - bool(int(row["user_editable"])), - (Privilege(row["privilege_id"], row["privilege_description"]),)) - } - -# @authorised_p(("group:role:view",), -# "Insufficient privileges to view role", -# oauth2_scope="profile group role") -def group_role_by_id( - conn: db.DbConnection, group: Group, group_role_id: UUID) -> GroupRole: - """Retrieve GroupRole from id by its `group_role_id`.""" - ## TODO: do privileges check before running actual query - ## the check commented out above doesn't work correctly - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT gr.group_role_id, r.*, p.* " - "FROM group_roles AS gr " - "INNER JOIN roles AS r ON gr.role_id=r.role_id " - "INNER JOIN role_privileges AS rp ON rp.role_id=r.role_id " - "INNER JOIN privileges AS p ON p.privilege_id=rp.privilege_id " - "WHERE gr.group_role_id=? AND gr.group_id=?", - (str(group_role_id), str(group.group_id))) - rows = cursor.fetchall() - if rows: - roles: tuple[Role,...] = tuple(reduce( - __organise_privileges__, rows, {}).values()) - assert len(roles) == 1 - return GroupRole(group_role_id, group, roles[0]) - raise NotFoundError( - f"Group role with ID '{group_role_id}' does not exist.") - -@authorised_p(("group:role:edit-role",), - "You do not have the privilege to edit a role.", - oauth2_scope="profile group role") -def add_privilege_to_group_role(conn: db.DbConnection, group_role: GroupRole, - privilege: Privilege) -> GroupRole: - """Add `privilege` to `group_role`.""" - ## TODO: do privileges check. - check_user_editable(group_role.role) - with db.cursor(conn) as cursor: - cursor.execute( - "INSERT INTO role_privileges(role_id,privilege_id) " - "VALUES (?, ?) ON CONFLICT (role_id, privilege_id) " - "DO NOTHING", - (str(group_role.role.role_id), str(privilege.privilege_id))) - return GroupRole( - group_role.group_role_id, - group_role.group, - Role(group_role.role.role_id, - group_role.role.role_name, - group_role.role.user_editable, - group_role.role.privileges + (privilege,))) - -@authorised_p(("group:role:edit-role",), - "You do not have the privilege to edit a role.", - oauth2_scope="profile group role") -def delete_privilege_from_group_role( - conn: db.DbConnection, group_role: GroupRole, - privilege: Privilege) -> GroupRole: - """Delete `privilege` to `group_role`.""" - ## TODO: do privileges check. - check_user_editable(group_role.role) - with db.cursor(conn) as cursor: - cursor.execute( - "DELETE FROM role_privileges WHERE " - "role_id=? AND privilege_id=?", - (str(group_role.role.role_id), str(privilege.privilege_id))) - return GroupRole( - group_role.group_role_id, - group_role.group, - Role(group_role.role.role_id, - group_role.role.role_name, - group_role.role.user_editable, - tuple(priv for priv in group_role.role.privileges - if priv != privilege))) diff --git a/gn3/auth/authorisation/groups/views.py b/gn3/auth/authorisation/groups/views.py deleted file mode 100644 index a849a73..0000000 --- a/gn3/auth/authorisation/groups/views.py +++ /dev/null @@ -1,430 +0,0 @@ -"""The views/routes for the `gn3.auth.authorisation.groups` package.""" -import uuid -import datetime -from typing import Iterable -from functools import partial - -from MySQLdb.cursors import DictCursor -from flask import request, jsonify, Response, Blueprint, current_app - -from gn3.auth import db -from gn3 import db_utils as gn3db - -from gn3.auth.dictify import dictify -from gn3.auth.db_utils import with_db_connection -from gn3.auth.authorisation.users import User -from gn3.auth.authorisation.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, create_group_role as _create_group_role) - -from ..roles.models import Role -from ..roles.models import user_roles - -from ..checks import authorised_p -from ..privileges import Privilege, privileges_by_ids -from ..errors import InvalidData, NotFoundError, AuthorisationError - -groups = Blueprint("groups", __name__) - -@groups.route("/list", methods=["GET"]) -@require_oauth("profile group") -def list_groups(): - """Return the list of groups that exist.""" - with db.connection(current_app.config["AUTH_DB"]) as conn: - the_groups = all_groups(conn) - - return jsonify(the_groups.maybe( - [], lambda grps: [dictify(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() - if not bool(group_name): - raise GroupCreationError("Could not create the group.") - - 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")) - return jsonify({ - **dictify(new_group), "group_leader": dictify(user) - }) - -@groups.route("/members/<uuid:group_id>", methods=["GET"]) -@require_oauth("profile group") -def group_members(group_id: uuid.UUID) -> Response: - """Retrieve all the members of a group.""" - with require_oauth.acquire("profile group") as the_token:# pylint: disable=[unused-variable] - db_uri = current_app.config["AUTH_DB"] - ## Check that user has appropriate privileges and remove the pylint disable above - with db.connection(db_uri) as conn: - return jsonify(tuple( - dictify(user) for user in _group_users(conn, group_id))) - -@groups.route("/requests/join/<uuid:group_id>", methods=["POST"]) -@require_oauth("profile group") -def request_to_join(group_id: uuid.UUID) -> Response: - """Request to join a group.""" - def __request__(conn: db.DbConnection, user: User, group_id: uuid.UUID, - message: str): - with db.cursor(conn) as cursor: - group = user_group(conn, user).maybe(# type: ignore[misc] - False, lambda grp: grp)# type: ignore[arg-type] - if group: - error = AuthorisationError( - "You cannot request to join a new group while being a " - "member of an existing group.") - error.error_code = 400 - raise error - request_id = uuid.uuid4() - cursor.execute( - "INSERT INTO group_join_requests VALUES " - "(:request_id, :group_id, :user_id, :ts, :status, :msg)", - { - "request_id": str(request_id), - "group_id": str(group_id), - "user_id": str(user.user_id), - "ts": datetime.datetime.now().timestamp(), - "status": "PENDING", - "msg": message - }) - return { - "request_id": request_id, - "message": "Successfully sent the join request." - } - - with require_oauth.acquire("profile group") as the_token: - form = request.form - 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."))) - return jsonify(results) - -@groups.route("/requests/join/list", methods=["GET"]) -@require_oauth("profile group") -def list_join_requests() -> Response: - """List the pending join requests.""" - with require_oauth.acquire("profile group") as the_token: - return jsonify(with_db_connection(partial( - join_requests, user=the_token.user))) - -@groups.route("/requests/join/accept", methods=["POST"]) -@require_oauth("profile group") -def accept_join_requests() -> Response: - """Accept a join request.""" - with require_oauth.acquire("profile group") as the_token: - form = request.form - request_id = uuid.UUID(form.get("request_id")) - return jsonify(with_db_connection(partial( - accept_reject_join_request, request_id=request_id, - user=the_token.user, status="ACCEPTED"))) - -@groups.route("/requests/join/reject", methods=["POST"]) -@require_oauth("profile group") -def reject_join_requests() -> Response: - """Reject a join request.""" - with require_oauth.acquire("profile group") as the_token: - form = request.form - request_id = uuid.UUID(form.get("request_id")) - return jsonify(with_db_connection(partial( - accept_reject_join_request, request_id=request_id, - user=the_token.user, status="REJECTED"))) - -def unlinked_mrna_data( - conn: db.DbConnection, group: Group) -> tuple[dict, ...]: - """ - Retrieve all mRNA Assay data linked to a group but not linked to any - resource. - """ - query = ( - "SELECT lmd.* FROM linked_mrna_data lmd " - "LEFT JOIN mrna_resources mr ON lmd.data_link_id=mr.data_link_id " - "WHERE lmd.group_id=? AND mr.data_link_id IS NULL") - with db.cursor(conn) as cursor: - cursor.execute(query, (str(group.group_id),)) - return tuple(dict(row) for row in cursor.fetchall()) - -def unlinked_genotype_data( - conn: db.DbConnection, group: Group) -> tuple[dict, ...]: - """ - Retrieve all genotype data linked to a group but not linked to any resource. - """ - query = ( - "SELECT lgd.* FROM linked_genotype_data lgd " - "LEFT JOIN genotype_resources gr ON lgd.data_link_id=gr.data_link_id " - "WHERE lgd.group_id=? AND gr.data_link_id IS NULL") - with db.cursor(conn) as cursor: - cursor.execute(query, (str(group.group_id),)) - return tuple(dict(row) for row in cursor.fetchall()) - -def unlinked_phenotype_data( - authconn: db.DbConnection, gn3conn: gn3db.Connection, - group: Group) -> tuple[dict, ...]: - """ - Retrieve all phenotype data linked to a group but not linked to any - resource. - """ - with db.cursor(authconn) as authcur, gn3conn.cursor(DictCursor) as gn3cur: - authcur.execute( - "SELECT lpd.* FROM linked_phenotype_data AS lpd " - "LEFT JOIN phenotype_resources AS pr " - "ON lpd.data_link_id=pr.data_link_id " - "WHERE lpd.group_id=? AND pr.data_link_id IS NULL", - (str(group.group_id),)) - results = authcur.fetchall() - ids: dict[tuple[str, ...], str] = { - ( - row["SpeciesId"], row["InbredSetId"], row["PublishFreezeId"], - row["PublishXRefId"]): row["data_link_id"] - for row in results - } - if len(ids.keys()) < 1: - return tuple() - paramstr = ", ".join(["(%s, %s, %s, %s)"] * len(ids.keys())) - gn3cur.execute( - "SELECT spc.SpeciesId, spc.SpeciesName, iset.InbredSetId, " - "iset.InbredSetName, pf.Id AS PublishFreezeId, " - "pf.Name AS dataset_name, pf.FullName AS dataset_fullname, " - "pf.ShortName AS dataset_shortname, pxr.Id AS PublishXRefId, " - "pub.PubMed_ID, pub.Title, pub.Year, " - "phen.Pre_publication_description, " - "phen.Post_publication_description, phen.Original_description " - "FROM " - "Species AS spc " - "INNER JOIN InbredSet AS iset " - "ON spc.SpeciesId=iset.SpeciesId " - "INNER JOIN PublishFreeze AS pf " - "ON iset.InbredSetId=pf.InbredSetId " - "INNER JOIN PublishXRef AS pxr " - "ON pf.InbredSetId=pxr.InbredSetId " - "INNER JOIN Publication AS pub " - "ON pxr.PublicationId=pub.Id " - "INNER JOIN Phenotype AS phen " - "ON pxr.PhenotypeId=phen.Id " - "WHERE (spc.SpeciesId, iset.InbredSetId, pf.Id, pxr.Id) " - f"IN ({paramstr})", - tuple(item for sublist in ids.keys() for item in sublist)) - return tuple({ - **{key: value for key, value in row.items() if key not in - ("Post_publication_description", "Pre_publication_description", - "Original_description")}, - "description": ( - row["Post_publication_description"] or - row["Pre_publication_description"] or - row["Original_description"]), - "data_link_id": ids[tuple(str(row[key]) for key in ( - "SpeciesId", "InbredSetId", "PublishFreezeId", - "PublishXRefId"))] - } for row in gn3cur.fetchall()) - -@groups.route("/<string:resource_type>/unlinked-data") -@require_oauth("profile group resource") -def unlinked_data(resource_type: str) -> Response: - """View data linked to the group but not linked to any resource.""" - if resource_type not in ("all", "mrna", "genotype", "phenotype"): - raise AuthorisationError(f"Invalid resource type {resource_type}") - - with require_oauth.acquire("profile group resource") as the_token: - db_uri = current_app.config["AUTH_DB"] - gn3db_uri = current_app.config["SQL_URI"] - with (db.connection(db_uri) as authconn, - gn3db.database_connection(gn3db_uri) as gn3conn): - ugroup = user_group(authconn, the_token.user).maybe(# type: ignore[misc] - DUMMY_GROUP, lambda grp: grp) - if ugroup == DUMMY_GROUP: - return jsonify(tuple()) - - unlinked_fns = { - "mrna": unlinked_mrna_data, - "genotype": unlinked_genotype_data, - "phenotype": lambda conn, grp: partial( - unlinked_phenotype_data, gn3conn=gn3conn)( - authconn=conn, group=grp) - } - return jsonify(tuple( - dict(row) for row in unlinked_fns[resource_type]( - authconn, ugroup))) - - return jsonify(tuple()) - -@groups.route("/data/link", methods=["POST"]) -@require_oauth("profile group resource") -def link_data() -> Response: - """Link selected data to specified group.""" - with require_oauth.acquire("profile group resource") as _the_token: - form = request.form - group_id = uuid.UUID(form["group_id"]) - dataset_ids = form.getlist("dataset_ids") - dataset_type = form.get("dataset_type") - if dataset_type not in ("mrna", "genotype", "phenotype"): - raise InvalidData("Unexpected dataset type requested!") - def __link__(conn: db.DbConnection): - group = group_by_id(conn, group_id) - with gn3db.database_connection(current_app.config["SQL_URI"]) as gn3conn: - return link_data_to_group( - conn, gn3conn, dataset_type, dataset_ids, group) - - return jsonify(with_db_connection(__link__)) - -@groups.route("/roles", methods=["GET"]) -@require_oauth("profile group") -def group_roles(): - """Return a list of all available group roles.""" - with require_oauth.acquire("profile group role") as the_token: - def __list_roles__(conn: db.DbConnection): - ## TODO: Check that user has appropriate privileges - with db.cursor(conn) as cursor: - group = user_group(conn, the_token.user).maybe(# type: ignore[misc] - DUMMY_GROUP, lambda grp: grp) - if group == DUMMY_GROUP: - return tuple() - cursor.execute( - "SELECT gr.group_role_id, r.* " - "FROM group_roles AS gr INNER JOIN roles AS r " - "ON gr.role_id=r.role_id " - "WHERE group_id=?", - (str(group.group_id),)) - return tuple( - GroupRole(uuid.UUID(row["group_role_id"]), - group, - Role(uuid.UUID(row["role_id"]), - row["role_name"], - bool(int(row["user_editable"])), - tuple())) - for row in cursor.fetchall()) - return jsonify(tuple( - dictify(role) for role in with_db_connection(__list_roles__))) - -@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()) - return tuple(privilege for arole in this_user_roles - for privilege in arole.privileges) + group_level_roles - return jsonify(tuple( - dictify(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") -def view_group_role(group_role_id: uuid.UUID): - """Return the details of the given role.""" - with require_oauth.acquire("profile group role") as the_token: - def __group_role__(conn: db.DbConnection) -> GroupRole: - 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 view group roles.") - return group_role_by_id(conn, group, group_role_id) - return jsonify(dictify(with_db_connection(__group_role__))) - -def __add_remove_priv_to_from_role__(conn: db.DbConnection, - group_role_id: uuid.UUID, - direction: str, - user: User) -> GroupRole: - assert direction in ("ADD", "DELETE") - group = user_group(conn, user).maybe(# type: ignore[misc] - DUMMY_GROUP, lambda grp: grp) - - if group == DUMMY_GROUP: - raise AuthorisationError( - "You need to be a member of a group to edit roles.") - try: - privilege_id = request.form.get("privilege_id", "") - assert bool(privilege_id), "Privilege to add must be provided." - privileges = privileges_by_ids(conn, (privilege_id,)) - if len(privileges) == 0: - raise NotFoundError("Privilege not found.") - dir_fns = { - "ADD": add_privilege_to_group_role, - "DELETE": delete_privilege_from_group_role - } - return dir_fns[direction]( - conn, - group_role_by_id(conn, group, group_role_id), - privileges[0]) - except AssertionError as aerr: - raise InvalidData(aerr.args[0]) from aerr - -@groups.route("/role/<uuid:group_role_id>/privilege/add", methods=["POST"]) -@require_oauth("profile group") -def add_priv_to_role(group_role_id: uuid.UUID) -> Response: - """Add privilege to group role.""" - with require_oauth.acquire("profile group role") as the_token: - return jsonify({ - **dictify(with_db_connection(partial( - __add_remove_priv_to_from_role__, group_role_id=group_role_id, - direction="ADD", user=the_token.user))), - "description": "Privilege added successfully" - }) - -@groups.route("/role/<uuid:group_role_id>/privilege/delete", methods=["POST"]) -@require_oauth("profile group") -def delete_priv_from_role(group_role_id: uuid.UUID) -> Response: - """Delete privilege from group role.""" - with require_oauth.acquire("profile group role") as the_token: - return jsonify({ - **dictify(with_db_connection(partial( - __add_remove_priv_to_from_role__, group_role_id=group_role_id, - direction="DELETE", user=the_token.user))), - "description": "Privilege deleted successfully" - }) diff --git a/gn3/auth/authorisation/oauth2/__init__.py b/gn3/auth/authorisation/oauth2/__init__.py deleted file mode 100644 index d083773..0000000 --- a/gn3/auth/authorisation/oauth2/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""OAuth2 modules.""" diff --git a/gn3/auth/authorisation/oauth2/oauth2client.py b/gn3/auth/authorisation/oauth2/oauth2client.py deleted file mode 100644 index dc54a41..0000000 --- a/gn3/auth/authorisation/oauth2/oauth2client.py +++ /dev/null @@ -1,234 +0,0 @@ -"""OAuth2 Client model.""" -import json -import datetime -from uuid import UUID -from typing import Sequence, Optional, NamedTuple - -from pymonad.maybe import Just, Maybe, Nothing - -from gn3.auth import db - -from gn3.auth.authorisation.errors import NotFoundError -from gn3.auth.authorisation.users import User, users, user_by_id, same_password - -class OAuth2Client(NamedTuple): - """ - Client to the OAuth2 Server. - - This is defined according to the mixin at - https://docs.authlib.org/en/latest/specs/rfc6749.html#authlib.oauth2.rfc6749.ClientMixin - """ - client_id: UUID - client_secret: str - client_id_issued_at: datetime.datetime - client_secret_expires_at: datetime.datetime - client_metadata: dict - user: User - - def check_client_secret(self, client_secret: str) -> bool: - """Check whether the `client_secret` matches this client.""" - return same_password(client_secret, self.client_secret) - - @property - def token_endpoint_auth_method(self) -> str: - """Return the token endpoint authorisation method.""" - return self.client_metadata.get("token_endpoint_auth_method", ["none"]) - - @property - def client_type(self) -> str: - """ - Return the token endpoint authorisation method. - - Acceptable client types: - * public: Unable to use registered client secrets, e.g. browsers, apps - on mobile devices. - * confidential: able to securely authenticate with authorisation server - e.g. being able to keep their registered client secret safe. - """ - return self.client_metadata.get("client_type", "public") - - def check_endpoint_auth_method(self, method: str, endpoint: str) -> bool: - """ - Check if the client supports the given method for the given endpoint. - - Acceptable methods: - * none: Client is a public client and does not have a client secret - * client_secret_post: Client uses the HTTP POST parameters - * client_secret_basic: Client uses HTTP Basic - """ - if endpoint == "token": - return (method in self.token_endpoint_auth_method - and method == "client_secret_post") - if endpoint in ("introspection", "revoke"): - return (method in self.token_endpoint_auth_method - and method == "client_secret_basic") - return False - - @property - def id(self):# pylint: disable=[invalid-name] - """Return the client_id.""" - return self.client_id - - @property - def grant_types(self) -> Sequence[str]: - """ - Return the grant types that this client supports. - - Valid grant types: - * authorisation_code - * implicit - * client_credentials - * password - """ - return self.client_metadata.get("grant_types", []) - - def check_grant_type(self, grant_type: str) -> bool: - """ - Validate that client can handle the given grant types - """ - return grant_type in self.grant_types - - @property - def redirect_uris(self) -> Sequence[str]: - """Return the redirect_uris that this client supports.""" - return self.client_metadata.get('redirect_uris', []) - - def check_redirect_uri(self, redirect_uri: str) -> bool: - """ - Check whether the given `redirect_uri` is one of the expected ones. - """ - return redirect_uri in self.redirect_uris - - @property - def response_types(self) -> Sequence[str]: - """Return the response_types that this client supports.""" - return self.client_metadata.get("response_type", []) - - def check_response_type(self, response_type: str) -> bool: - """Check whether this client supports `response_type`.""" - return response_type in self.response_types - - @property - def scope(self) -> Sequence[str]: - """Return valid scopes for this client.""" - return tuple(set(self.client_metadata.get("scope", []))) - - def get_allowed_scope(self, scope: str) -> str: - """Return list of scopes in `scope` that are supported by this client.""" - if not bool(scope): - return "" - requested = scope.split() - return " ".join(sorted(set( - scp for scp in requested if scp in self.scope))) - - def get_client_id(self): - """Return this client's identifier.""" - return self.client_id - - def get_default_redirect_uri(self) -> str: - """Return the default redirect uri""" - return self.client_metadata.get("default_redirect_uri", "") - -def client(conn: db.DbConnection, client_id: UUID, - user: Optional[User] = None) -> Maybe: - """Retrieve a client by its ID""" - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT * FROM oauth2_clients WHERE client_id=?", (str(client_id),)) - result = cursor.fetchone() - the_user = user - if result: - if not bool(the_user): - try: - the_user = user_by_id(conn, result["user_id"]) - except NotFoundError as _nfe: - the_user = None - - return Just( - OAuth2Client(UUID(result["client_id"]), - result["client_secret"], - datetime.datetime.fromtimestamp( - result["client_id_issued_at"]), - datetime.datetime.fromtimestamp( - result["client_secret_expires_at"]), - json.loads(result["client_metadata"]), - the_user))# type: ignore[arg-type] - - return Nothing - -def client_by_id_and_secret(conn: db.DbConnection, client_id: UUID, - client_secret: str) -> OAuth2Client: - """Retrieve a client by its ID and secret""" - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT * FROM oauth2_clients WHERE client_id=?", - (str(client_id),)) - row = cursor.fetchone() - if bool(row) and same_password(client_secret, row["client_secret"]): - return OAuth2Client( - client_id, client_secret, - datetime.datetime.fromtimestamp(row["client_id_issued_at"]), - datetime.datetime.fromtimestamp( - row["client_secret_expires_at"]), - json.loads(row["client_metadata"]), - user_by_id(conn, UUID(row["user_id"]))) - - raise NotFoundError("Could not find client with the given credentials.") - -def save_client(conn: db.DbConnection, the_client: OAuth2Client) -> OAuth2Client: - """Persist the client details into the database.""" - with db.cursor(conn) as cursor: - query = ( - "INSERT INTO oauth2_clients " - "(client_id, client_secret, client_id_issued_at, " - "client_secret_expires_at, client_metadata, user_id) " - "VALUES " - "(:client_id, :client_secret, :client_id_issued_at, " - ":client_secret_expires_at, :client_metadata, :user_id) " - "ON CONFLICT (client_id) DO UPDATE SET " - "client_secret=:client_secret, " - "client_id_issued_at=:client_id_issued_at, " - "client_secret_expires_at=:client_secret_expires_at, " - "client_metadata=:client_metadata, user_id=:user_id") - cursor.execute( - query, - { - "client_id": str(the_client.client_id), - "client_secret": the_client.client_secret, - "client_id_issued_at": ( - the_client.client_id_issued_at.timestamp()), - "client_secret_expires_at": ( - the_client.client_secret_expires_at.timestamp()), - "client_metadata": json.dumps(the_client.client_metadata), - "user_id": str(the_client.user.user_id) - }) - return the_client - -def oauth2_clients(conn: db.DbConnection) -> tuple[OAuth2Client, ...]: - """Fetch a list of all OAuth2 clients.""" - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM oauth2_clients") - clients_rs = cursor.fetchall() - the_users = { - usr.user_id: usr for usr in users( - conn, tuple({UUID(result["user_id"]) for result in clients_rs})) - } - return tuple(OAuth2Client(UUID(result["client_id"]), - result["client_secret"], - datetime.datetime.fromtimestamp( - result["client_id_issued_at"]), - datetime.datetime.fromtimestamp( - result["client_secret_expires_at"]), - json.loads(result["client_metadata"]), - the_users[UUID(result["user_id"])]) - for result in clients_rs) - -def delete_client(conn: db.DbConnection, the_client: OAuth2Client) -> OAuth2Client: - """Delete the given client from the database""" - with db.cursor(conn) as cursor: - params = (str(the_client.client_id),) - cursor.execute("DELETE FROM authorisation_code WHERE client_id=?", - params) - cursor.execute("DELETE FROM oauth2_tokens WHERE client_id=?", params) - cursor.execute("DELETE FROM oauth2_clients WHERE client_id=?", params) - return the_client diff --git a/gn3/auth/authorisation/oauth2/oauth2token.py b/gn3/auth/authorisation/oauth2/oauth2token.py deleted file mode 100644 index bb19039..0000000 --- a/gn3/auth/authorisation/oauth2/oauth2token.py +++ /dev/null @@ -1,133 +0,0 @@ -"""OAuth2 Token""" -import uuid -import datetime -from typing import NamedTuple, Optional - -from pymonad.maybe import Just, Maybe, Nothing - -from gn3.auth import db - -from gn3.auth.authorisation.errors import NotFoundError -from gn3.auth.authorisation.users import User, user_by_id - -from .oauth2client import client, OAuth2Client - -class OAuth2Token(NamedTuple): - """Implement Tokens for OAuth2.""" - token_id: uuid.UUID - client: OAuth2Client - token_type: str - access_token: str - refresh_token: Optional[str] - scope: str - revoked: bool - issued_at: datetime.datetime - expires_in: int - user: User - - @property - def expires_at(self) -> datetime.datetime: - """Return the time when the token expires.""" - return self.issued_at + datetime.timedelta(seconds=self.expires_in) - - def check_client(self, client: OAuth2Client) -> bool:# pylint: disable=[redefined-outer-name] - """Check whether the token is issued to given `client`.""" - return client.client_id == self.client.client_id - - def get_expires_in(self) -> int: - """Return the `expires_in` value for the token.""" - return self.expires_in - - def get_scope(self) -> str: - """Return the valid scope for the token.""" - return self.scope - - def is_expired(self) -> bool: - """Check whether the token is expired.""" - return self.expires_at < datetime.datetime.now() - - def is_revoked(self): - """Check whether the token has been revoked.""" - return self.revoked - -def __token_from_resultset__(conn: db.DbConnection, rset) -> Maybe: - def __identity__(val): - return val - try: - the_user = user_by_id(conn, uuid.UUID(rset["user_id"])) - except NotFoundError as _nfe: - the_user = None - the_client = client(conn, uuid.UUID(rset["client_id"]), the_user) - - if the_client.is_just() and bool(the_user): - return Just(OAuth2Token(token_id=uuid.UUID(rset["token_id"]), - client=the_client.maybe(None, __identity__), - token_type=rset["token_type"], - access_token=rset["access_token"], - refresh_token=rset["refresh_token"], - scope=rset["scope"], - revoked=(rset["revoked"] == 1), - issued_at=datetime.datetime.fromtimestamp( - rset["issued_at"]), - expires_in=rset["expires_in"], - user=the_user))# type: ignore[arg-type] - - return Nothing - -def token_by_access_token(conn: db.DbConnection, token_str: str) -> Maybe: - """Retrieve token by its token string""" - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM oauth2_tokens WHERE access_token=?", - (token_str,)) - res = cursor.fetchone() - if res: - return __token_from_resultset__(conn, res) - - return Nothing - -def token_by_refresh_token(conn: db.DbConnection, token_str: str) -> Maybe: - """Retrieve token by its token string""" - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT * FROM oauth2_tokens WHERE refresh_token=?", - (token_str,)) - res = cursor.fetchone() - if res: - return __token_from_resultset__(conn, res) - - return Nothing - -def revoke_token(token: OAuth2Token) -> OAuth2Token: - """ - Return a new token derived from `token` with the `revoked` field set to - `True`. - """ - return OAuth2Token( - token_id=token.token_id, client=token.client, - token_type=token.token_type, access_token=token.access_token, - refresh_token=token.refresh_token, scope=token.scope, revoked=True, - issued_at=token.issued_at, expires_in=token.expires_in, user=token.user) - -def save_token(conn: db.DbConnection, token: OAuth2Token) -> None: - """Save/Update the token.""" - with db.cursor(conn) as cursor: - cursor.execute( - ("INSERT INTO oauth2_tokens VALUES (:token_id, :client_id, " - ":token_type, :access_token, :refresh_token, :scope, :revoked, " - ":issued_at, :expires_in, :user_id) " - "ON CONFLICT (token_id) DO UPDATE SET " - "refresh_token=:refresh_token, revoked=:revoked, " - "expires_in=:expires_in " - "WHERE token_id=:token_id"), - { - "token_id": str(token.token_id), - "client_id": str(token.client.client_id), - "token_type": token.token_type, - "access_token": token.access_token, - "refresh_token": token.refresh_token, - "scope": token.scope, - "revoked": 1 if token.revoked else 0, - "issued_at": int(token.issued_at.timestamp()), - "expires_in": token.expires_in, - "user_id": str(token.user.user_id) - }) diff --git a/gn3/auth/authorisation/oauth2/resource_server.py b/gn3/auth/authorisation/oauth2/resource_server.py deleted file mode 100644 index e806dc5..0000000 --- a/gn3/auth/authorisation/oauth2/resource_server.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Protect the resources endpoints""" - -from flask import current_app as app -from authlib.oauth2.rfc6750 import BearerTokenValidator as _BearerTokenValidator -from authlib.integrations.flask_oauth2 import ResourceProtector - -from gn3.auth import db -from gn3.auth.authorisation.oauth2.oauth2token import token_by_access_token - -class BearerTokenValidator(_BearerTokenValidator): - """Extends `authlib.oauth2.rfc6750.BearerTokenValidator`""" - def authenticate_token(self, token_string: str): - with db.connection(app.config["AUTH_DB"]) as conn: - return token_by_access_token(conn, token_string).maybe(# type: ignore[misc] - None, lambda tok: tok) - -require_oauth = ResourceProtector() - -require_oauth.register_token_validator(BearerTokenValidator()) diff --git a/gn3/auth/authorisation/privileges.py b/gn3/auth/authorisation/privileges.py deleted file mode 100644 index 7907d76..0000000 --- a/gn3/auth/authorisation/privileges.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Handle privileges""" -from typing import Any, Iterable, NamedTuple - -from gn3.auth import db -from gn3.auth.authorisation.users import User - -class Privilege(NamedTuple): - """Class representing a privilege: creates immutable objects.""" - privilege_id: str - privilege_description: str - - def dictify(self) -> dict[str, Any]: - """Return a dict representation of `Privilege` objects.""" - return { - "privilege_id": self.privilege_id, - "privilege_description": self.privilege_description - } - -def user_privileges(conn: db.DbConnection, user: User) -> Iterable[Privilege]: - """Fetch the user's privileges from the database.""" - with db.cursor(conn) as cursor: - cursor.execute( - ("SELECT p.privilege_id, p.privilege_description " - "FROM user_roles AS ur " - "INNER JOIN role_privileges AS rp ON ur.role_id=rp.role_id " - "INNER JOIN privileges AS p ON rp.privilege_id=p.privilege_id " - "WHERE ur.user_id=?"), - (str(user.user_id),)) - results = cursor.fetchall() - - return (Privilege(row[0], row[1]) for row in results) - -def privileges_by_ids( - conn: db.DbConnection, privileges_ids: tuple[str, ...]) -> tuple[ - Privilege, ...]: - """Fetch privileges by their ids.""" - if len(privileges_ids) == 0: - return tuple() - - with db.cursor(conn) as cursor: - clause = ", ".join(["?"] * len(privileges_ids)) - cursor.execute( - f"SELECT * FROM privileges WHERE privilege_id IN ({clause})", - privileges_ids) - return tuple( - Privilege(row["privilege_id"], row["privilege_description"]) - for row in cursor.fetchall()) diff --git a/gn3/auth/authorisation/resources/__init__.py b/gn3/auth/authorisation/resources/__init__.py deleted file mode 100644 index 869ab60..0000000 --- a/gn3/auth/authorisation/resources/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Initialise the `gn3.auth.authorisation.resources` package.""" -from .models import Resource, ResourceCategory diff --git a/gn3/auth/authorisation/resources/checks.py b/gn3/auth/authorisation/resources/checks.py deleted file mode 100644 index 1f5a0f9..0000000 --- a/gn3/auth/authorisation/resources/checks.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Handle authorisation checks for resources""" -from uuid import UUID -from functools import reduce -from typing import Sequence - -from gn3.auth import db -from gn3.auth.authorisation.users import User - -def __organise_privileges_by_resource_id__(rows): - def __organise__(privs, row): - resource_id = UUID(row["resource_id"]) - return { - **privs, - resource_id: (row["privilege_id"],) + privs.get( - resource_id, tuple()) - } - return reduce(__organise__, rows, {}) - -def authorised_for(conn: db.DbConnection, user: User, privileges: tuple[str], - resource_ids: Sequence[UUID]) -> dict[UUID, bool]: - """ - Check whether `user` is authorised to access `resources` according to given - `privileges`. - """ - with db.cursor(conn) as cursor: - cursor.execute( - ("SELECT guror.*, rp.privilege_id FROM " - "group_user_roles_on_resources AS guror " - "INNER JOIN group_roles AS gr ON " - "(guror.group_id=gr.group_id AND guror.role_id=gr.role_id) " - "INNER JOIN roles AS r ON gr.role_id=r.role_id " - "INNER JOIN role_privileges AS rp ON r.role_id=rp.role_id " - "WHERE guror.user_id=? " - f"AND guror.resource_id IN ({', '.join(['?']*len(resource_ids))})" - f"AND rp.privilege_id IN ({', '.join(['?']*len(privileges))})"), - ((str(user.user_id),) + tuple( - str(r_id) for r_id in resource_ids) + tuple(privileges))) - resource_privileges = __organise_privileges_by_resource_id__( - cursor.fetchall()) - authorised = tuple(resource_id for resource_id, res_privileges - in resource_privileges.items() - if all(priv in res_privileges - for priv in privileges)) - return { - resource_id: resource_id in authorised - for resource_id in resource_ids - } diff --git a/gn3/auth/authorisation/resources/models.py b/gn3/auth/authorisation/resources/models.py deleted file mode 100644 index cf7769e..0000000 --- a/gn3/auth/authorisation/resources/models.py +++ /dev/null @@ -1,579 +0,0 @@ -"""Handle the management of resources.""" -import json -import sqlite3 -from uuid import UUID, uuid4 -from functools import reduce, partial -from typing import Any, Dict, Sequence, Optional, NamedTuple - -from gn3.auth import db -from gn3.auth.dictify import dictify -from gn3.auth.authorisation.users import User -from gn3.auth.db_utils import with_db_connection - -from .checks import authorised_for - -from ..checks import authorised_p -from ..errors import NotFoundError, AuthorisationError -from ..groups.models import ( - Group, GroupRole, user_group, group_by_id, is_group_leader) - -class MissingGroupError(AuthorisationError): - """Raised for any resource operation without a group.""" - -class ResourceCategory(NamedTuple): - """Class representing a resource category.""" - resource_category_id: UUID - resource_category_key: str - resource_category_description: str - - def dictify(self) -> dict[str, Any]: - """Return a dict representation of `ResourceCategory` objects.""" - return { - "resource_category_id": self.resource_category_id, - "resource_category_key": self.resource_category_key, - "resource_category_description": self.resource_category_description - } - -class Resource(NamedTuple): - """Class representing a resource.""" - group: Group - resource_id: UUID - resource_name: str - resource_category: ResourceCategory - public: bool - resource_data: Sequence[dict[str, Any]] = tuple() - - def dictify(self) -> dict[str, Any]: - """Return a dict representation of `Resource` objects.""" - return { - "group": dictify(self.group), "resource_id": self.resource_id, - "resource_name": self.resource_name, - "resource_category": dictify(self.resource_category), - "public": self.public, - "resource_data": self.resource_data - } - -def __assign_resource_owner_role__(cursor, resource, user): - """Assign `user` the 'Resource Owner' role for `resource`.""" - cursor.execute( - "SELECT gr.* FROM group_roles AS gr INNER JOIN roles AS r " - "ON gr.role_id=r.role_id WHERE r.role_name='resource-owner' " - "AND gr.group_id=?", - (str(resource.group.group_id),)) - role = cursor.fetchone() - if not role: - cursor.execute("SELECT * FROM roles WHERE role_name='resource-owner'") - role = cursor.fetchone() - cursor.execute( - "INSERT INTO group_roles VALUES " - "(:group_role_id, :group_id, :role_id)", - {"group_role_id": str(uuid4()), - "group_id": str(resource.group.group_id), - "role_id": role["role_id"]}) - - cursor.execute( - "INSERT INTO group_user_roles_on_resources " - "VALUES (" - ":group_id, :user_id, :role_id, :resource_id" - ")", - {"group_id": str(resource.group.group_id), - "user_id": str(user.user_id), - "role_id": role["role_id"], - "resource_id": str(resource.resource_id)}) - -@authorised_p(("group:resource:create-resource",), - error_description="Insufficient privileges to create a resource", - oauth2_scope="profile resource") -def create_resource( - conn: db.DbConnection, resource_name: str, - resource_category: ResourceCategory, user: User, - public: bool) -> Resource: - """Create a resource item.""" - with db.cursor(conn) as cursor: - group = user_group(conn, user).maybe( - False, lambda grp: grp)# type: ignore[misc, arg-type] - if not group: - raise MissingGroupError( - "User with no group cannot create a resource.") - resource = Resource( - group, uuid4(), resource_name, resource_category, public) - cursor.execute( - "INSERT INTO resources VALUES (?, ?, ?, ?, ?)", - (str(resource.group.group_id), str(resource.resource_id), - resource_name, - str(resource.resource_category.resource_category_id), - 1 if resource.public else 0)) - __assign_resource_owner_role__(cursor, resource, user) - - return resource - -def resource_category_by_id( - conn: db.DbConnection, category_id: UUID) -> ResourceCategory: - """Retrieve a resource category by its ID.""" - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT * FROM resource_categories WHERE " - "resource_category_id=?", - (str(category_id),)) - results = cursor.fetchone() - if results: - return ResourceCategory( - UUID(results["resource_category_id"]), - results["resource_category_key"], - results["resource_category_description"]) - - raise NotFoundError( - f"Could not find a ResourceCategory with ID '{category_id}'") - -def resource_categories(conn: db.DbConnection) -> Sequence[ResourceCategory]: - """Retrieve all available resource categories""" - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM resource_categories") - return tuple( - ResourceCategory(UUID(row[0]), row[1], row[2]) - for row in cursor.fetchall()) - return tuple() - -def public_resources(conn: db.DbConnection) -> Sequence[Resource]: - """List all resources marked as public""" - categories = { - str(cat.resource_category_id): cat for cat in resource_categories(conn) - } - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM resources WHERE public=1") - results = cursor.fetchall() - group_uuids = tuple(row[0] for row in results) - query = ("SELECT * FROM groups WHERE group_id IN " - f"({', '.join(['?'] * len(group_uuids))})") - cursor.execute(query, group_uuids) - groups = { - row[0]: Group( - UUID(row[0]), row[1], json.loads(row[2] or "{}")) - for row in cursor.fetchall() - } - return tuple( - Resource(groups[row[0]], UUID(row[1]), row[2], categories[row[3]], - bool(row[4])) - for row in results) - -def group_leader_resources( - conn: db.DbConnection, user: User, group: Group, - res_categories: Dict[UUID, ResourceCategory]) -> Sequence[Resource]: - """Return all the resources available to the group leader""" - if is_group_leader(conn, user, group): - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM resources WHERE group_id=?", - (str(group.group_id),)) - return tuple( - Resource(group, UUID(row[1]), row[2], - res_categories[UUID(row[3])], bool(row[4])) - for row in cursor.fetchall()) - return tuple() - -def user_resources(conn: db.DbConnection, user: User) -> Sequence[Resource]: - """List the resources available to the user""" - categories = { # Repeated in `public_resources` function - cat.resource_category_id: cat for cat in resource_categories(conn) - } - with db.cursor(conn) as cursor: - def __all_resources__(group) -> Sequence[Resource]: - gl_resources = group_leader_resources(conn, user, group, categories) - - cursor.execute( - ("SELECT resources.* FROM group_user_roles_on_resources " - "LEFT JOIN resources " - "ON group_user_roles_on_resources.resource_id=resources.resource_id " - "WHERE group_user_roles_on_resources.group_id = ? " - "AND group_user_roles_on_resources.user_id = ?"), - (str(group.group_id), str(user.user_id))) - rows = cursor.fetchall() - private_res = tuple( - Resource(group, UUID(row[1]), row[2], categories[UUID(row[3])], - bool(row[4])) - for row in rows) - return tuple({ - res.resource_id: res - for res in - (private_res + gl_resources + public_resources(conn))# type: ignore[operator] - }.values()) - - # Fix the typing here - return user_group(conn, user).map(__all_resources__).maybe(# type: ignore[arg-type,misc] - public_resources(conn), lambda res: res)# type: ignore[arg-type,return-value] - -def resource_data(conn, resource, offset: int = 0, limit: Optional[int] = None) -> tuple[dict, ...]: - """ - Retrieve the data for `resource`, optionally limiting the number of items. - """ - resource_data_function = { - "mrna": mrna_resource_data, - "genotype": genotype_resource_data, - "phenotype": phenotype_resource_data - } - with db.cursor(conn) as cursor: - return tuple( - dict(data_row) for data_row in - resource_data_function[ - resource.resource_category.resource_category_key]( - cursor, resource.resource_id, offset, limit)) - -def attach_resource_data(cursor: db.DbCursor, resource: Resource) -> Resource: - """Attach the linked data to the resource""" - resource_data_function = { - "mrna": mrna_resource_data, - "genotype": genotype_resource_data, - "phenotype": phenotype_resource_data - } - category = resource.resource_category - data_rows = tuple( - dict(data_row) for data_row in - resource_data_function[category.resource_category_key]( - cursor, resource.resource_id)) - return Resource( - resource.group, resource.resource_id, resource.resource_name, - resource.resource_category, resource.public, data_rows) - -def mrna_resource_data(cursor: db.DbCursor, - resource_id: UUID, - offset: int = 0, - limit: Optional[int] = None) -> Sequence[sqlite3.Row]: - """Fetch data linked to a mRNA resource""" - cursor.execute( - (("SELECT * FROM mrna_resources AS mr " - "INNER JOIN linked_mrna_data AS lmr " - "ON mr.data_link_id=lmr.data_link_id " - "WHERE mr.resource_id=?") + ( - f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")), - (str(resource_id),)) - return cursor.fetchall() - -def genotype_resource_data( - cursor: db.DbCursor, - resource_id: UUID, - offset: int = 0, - limit: Optional[int] = None) -> Sequence[sqlite3.Row]: - """Fetch data linked to a Genotype resource""" - cursor.execute( - (("SELECT * FROM genotype_resources AS gr " - "INNER JOIN linked_genotype_data AS lgd " - "ON gr.data_link_id=lgd.data_link_id " - "WHERE gr.resource_id=?") + ( - f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")), - (str(resource_id),)) - return cursor.fetchall() - -def phenotype_resource_data( - cursor: db.DbCursor, - resource_id: UUID, - offset: int = 0, - limit: Optional[int] = None) -> Sequence[sqlite3.Row]: - """Fetch data linked to a Phenotype resource""" - cursor.execute( - ("SELECT * FROM phenotype_resources AS pr " - "INNER JOIN linked_phenotype_data AS lpd " - "ON pr.data_link_id=lpd.data_link_id " - "WHERE pr.resource_id=?") + ( - f" LIMIT {limit} OFFSET {offset}" if bool(limit) else ""), - (str(resource_id),)) - return cursor.fetchall() - -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]: - raise AuthorisationError( - "You are not authorised to access resource with id " - f"'{resource_id}'.") - - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM resources WHERE resource_id=:id", - {"id": str(resource_id)}) - row = cursor.fetchone() - if row: - return Resource( - group_by_id(conn, UUID(row["group_id"])), - UUID(row["resource_id"]), row["resource_name"], - resource_category_by_id(conn, row["resource_category_id"]), - bool(int(row["public"]))) - - raise NotFoundError(f"Could not find a resource with id '{resource_id}'") - -def __link_mrna_data_to_resource__( - conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: - """Link mRNA Assay data with a resource.""" - with db.cursor(conn) as cursor: - params = { - "group_id": str(resource.group.group_id), - "resource_id": str(resource.resource_id), - "data_link_id": str(data_link_id) - } - cursor.execute( - "INSERT INTO mrna_resources VALUES" - "(:group_id, :resource_id, :data_link_id)", - params) - return params - -def __link_geno_data_to_resource__( - conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: - """Link Genotype data with a resource.""" - with db.cursor(conn) as cursor: - params = { - "group_id": str(resource.group.group_id), - "resource_id": str(resource.resource_id), - "data_link_id": str(data_link_id) - } - cursor.execute( - "INSERT INTO genotype_resources VALUES" - "(:group_id, :resource_id, :data_link_id)", - params) - return params - -def __link_pheno_data_to_resource__( - conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: - """Link Phenotype data with a resource.""" - with db.cursor(conn) as cursor: - params = { - "group_id": str(resource.group.group_id), - "resource_id": str(resource.resource_id), - "data_link_id": str(data_link_id) - } - cursor.execute( - "INSERT INTO phenotype_resources VALUES" - "(:group_id, :resource_id, :data_link_id)", - params) - return params - -def link_data_to_resource( - conn: db.DbConnection, user: User, resource_id: UUID, dataset_type: str, - data_link_id: UUID) -> dict: - """Link data to resource.""" - if not authorised_for( - conn, user, ("group:resource:edit-resource",), - (resource_id,))[resource_id]: - raise AuthorisationError( - "You are not authorised to link data to resource with id " - f"{resource_id}") - - resource = with_db_connection(partial( - resource_by_id, user=user, resource_id=resource_id)) - return { - "mrna": __link_mrna_data_to_resource__, - "genotype": __link_geno_data_to_resource__, - "phenotype": __link_pheno_data_to_resource__, - }[dataset_type.lower()](conn, resource, data_link_id) - -def __unlink_mrna_data_to_resource__( - conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: - """Unlink data from mRNA Assay resources""" - with db.cursor(conn) as cursor: - cursor.execute("DELETE FROM mrna_resources " - "WHERE resource_id=? AND data_link_id=?", - (str(resource.resource_id), str(data_link_id))) - return { - "resource_id": str(resource.resource_id), - "dataset_type": resource.resource_category.resource_category_key, - "data_link_id": data_link_id - } - -def __unlink_geno_data_to_resource__( - conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: - """Unlink data from Genotype resources""" - with db.cursor(conn) as cursor: - cursor.execute("DELETE FROM genotype_resources " - "WHERE resource_id=? AND data_link_id=?", - (str(resource.resource_id), str(data_link_id))) - return { - "resource_id": str(resource.resource_id), - "dataset_type": resource.resource_category.resource_category_key, - "data_link_id": data_link_id - } - -def __unlink_pheno_data_to_resource__( - conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict: - """Unlink data from Phenotype resources""" - with db.cursor(conn) as cursor: - cursor.execute("DELETE FROM phenotype_resources " - "WHERE resource_id=? AND data_link_id=?", - (str(resource.resource_id), str(data_link_id))) - return { - "resource_id": str(resource.resource_id), - "dataset_type": resource.resource_category.resource_category_key, - "data_link_id": str(data_link_id) - } - -def unlink_data_from_resource( - conn: db.DbConnection, user: User, resource_id: UUID, data_link_id: UUID): - """Unlink data from resource.""" - if not authorised_for( - conn, user, ("group:resource:edit-resource",), - (resource_id,))[resource_id]: - raise AuthorisationError( - "You are not authorised to link data to resource with id " - f"{resource_id}") - - resource = with_db_connection(partial( - resource_by_id, user=user, resource_id=resource_id)) - dataset_type = resource.resource_category.resource_category_key - return { - "mrna": __unlink_mrna_data_to_resource__, - "genotype": __unlink_geno_data_to_resource__, - "phenotype": __unlink_pheno_data_to_resource__, - }[dataset_type.lower()](conn, resource, data_link_id) - -def organise_resources_by_category(resources: Sequence[Resource]) -> dict[ - ResourceCategory, tuple[Resource]]: - """Organise the `resources` by their categories.""" - def __organise__(accumulator, resource): - category = resource.resource_category - return { - **accumulator, - category: accumulator.get(category, tuple()) + (resource,) - } - return reduce(__organise__, resources, {}) - -def __attach_data__( - data_rows: Sequence[sqlite3.Row], - resources: Sequence[Resource]) -> Sequence[Resource]: - def __organise__(acc, row): - resource_id = UUID(row["resource_id"]) - return { - **acc, - resource_id: acc.get(resource_id, tuple()) + (dict(row),) - } - organised: dict[UUID, tuple[dict, ...]] = reduce(__organise__, data_rows, {}) - return tuple( - Resource( - resource.group, resource.resource_id, resource.resource_name, - resource.resource_category, resource.public, - organised.get(resource.resource_id, tuple())) - for resource in resources) - -def attach_mrna_resources_data( - cursor, resources: Sequence[Resource]) -> Sequence[Resource]: - """Attach linked data to mRNA Assay resources""" - placeholders = ", ".join(["?"] * len(resources)) - cursor.execute( - "SELECT * FROM mrna_resources AS mr INNER JOIN linked_mrna_data AS lmd" - " ON mr.data_link_id=lmd.data_link_id " - f"WHERE mr.resource_id IN ({placeholders})", - tuple(str(resource.resource_id) for resource in resources)) - return __attach_data__(cursor.fetchall(), resources) - -def attach_genotype_resources_data( - cursor, resources: Sequence[Resource]) -> Sequence[Resource]: - """Attach linked data to Genotype resources""" - placeholders = ", ".join(["?"] * len(resources)) - cursor.execute( - "SELECT * FROM genotype_resources AS gr " - "INNER JOIN linked_genotype_data AS lgd " - "ON gr.data_link_id=lgd.data_link_id " - f"WHERE gr.resource_id IN ({placeholders})", - tuple(str(resource.resource_id) for resource in resources)) - return __attach_data__(cursor.fetchall(), resources) - -def attach_phenotype_resources_data( - cursor, resources: Sequence[Resource]) -> Sequence[Resource]: - """Attach linked data to Phenotype resources""" - placeholders = ", ".join(["?"] * len(resources)) - cursor.execute( - "SELECT * FROM phenotype_resources AS pr " - "INNER JOIN linked_phenotype_data AS lpd " - "ON pr.data_link_id=lpd.data_link_id " - f"WHERE pr.resource_id IN ({placeholders})", - tuple(str(resource.resource_id) for resource in resources)) - return __attach_data__(cursor.fetchall(), resources) - -def attach_resources_data( - conn: db.DbConnection, resources: Sequence[Resource]) -> Sequence[ - Resource]: - """Attach linked data for each resource in `resources`""" - resource_data_function = { - "mrna": attach_mrna_resources_data, - "genotype": attach_genotype_resources_data, - "phenotype": attach_phenotype_resources_data - } - organised = organise_resources_by_category(resources) - with db.cursor(conn) as cursor: - return tuple( - resource for categories in - (resource_data_function[category.resource_category_key]( - cursor, rscs) - for category, rscs in organised.items()) - for resource in categories) - -@authorised_p( - ("group:user:assign-role",), - "You cannot assign roles to users for this group.", - oauth2_scope="profile group role resource") -def assign_resource_user( - conn: db.DbConnection, resource: Resource, user: User, - role: GroupRole) -> dict: - """Assign `role` to `user` for the specific `resource`.""" - with db.cursor(conn) as cursor: - cursor.execute( - "INSERT INTO " - "group_user_roles_on_resources(group_id, user_id, role_id, " - "resource_id) " - "VALUES (?, ?, ?, ?) " - "ON CONFLICT (group_id, user_id, role_id, resource_id) " - "DO NOTHING", - (str(resource.group.group_id), str(user.user_id), - str(role.role.role_id), str(resource.resource_id))) - return { - "resource": dictify(resource), - "user": dictify(user), - "role": dictify(role), - "description": ( - f"The user '{user.name}'({user.email}) was assigned the " - f"'{role.role.role_name}' role on resource with ID " - f"'{resource.resource_id}'.")} - -@authorised_p( - ("group:user:assign-role",), - "You cannot assign roles to users for this group.", - oauth2_scope="profile group role resource") -def unassign_resource_user( - conn: db.DbConnection, resource: Resource, user: User, - role: GroupRole) -> dict: - """Assign `role` to `user` for the specific `resource`.""" - with db.cursor(conn) as cursor: - cursor.execute( - "DELETE FROM group_user_roles_on_resources " - "WHERE group_id=? AND user_id=? AND role_id=? AND resource_id=?", - (str(resource.group.group_id), str(user.user_id), - str(role.role.role_id), str(resource.resource_id))) - return { - "resource": dictify(resource), - "user": dictify(user), - "role": dictify(role), - "description": ( - f"The user '{user.name}'({user.email}) had the " - f"'{role.role.role_name}' role on resource with ID " - f"'{resource.resource_id}' taken away.")} - -def save_resource( - conn: db.DbConnection, user: User, resource: Resource) -> Resource: - """Update an existing resource.""" - resource_id = resource.resource_id - authorised = authorised_for( - conn, user, ("group:resource:edit-resource",), (resource_id,)) - if authorised[resource_id]: - with db.cursor(conn) as cursor: - cursor.execute( - "UPDATE resources SET " - "resource_name=:resource_name, " - "public=:public " - "WHERE group_id=:group_id " - "AND resource_id=:resource_id", - { - "resource_name": resource.resource_name, - "public": 1 if resource.public else 0, - "group_id": str(resource.group.group_id), - "resource_id": str(resource.resource_id) - }) - return resource - - raise AuthorisationError( - "You do not have the appropriate privileges to edit this resource.") diff --git a/gn3/auth/authorisation/resources/views.py b/gn3/auth/authorisation/resources/views.py deleted file mode 100644 index bda67cd..0000000 --- a/gn3/auth/authorisation/resources/views.py +++ /dev/null @@ -1,272 +0,0 @@ -"""The views/routes for the resources package""" -import uuid -import json -import sqlite3 -from functools import reduce - -from flask import request, jsonify, Response, Blueprint, current_app as app - -from gn3.auth.db_utils import with_db_connection -from gn3.auth.authorisation.oauth2.resource_server import require_oauth -from gn3.auth.authorisation.users import User, user_by_id, user_by_email - -from .checks import authorised_for -from .models import ( - Resource, save_resource, resource_data, resource_by_id, resource_categories, - assign_resource_user, link_data_to_resource, unassign_resource_user, - resource_category_by_id, unlink_data_from_resource, - create_resource as _create_resource) - -from ..roles import Role -from ..errors import InvalidData, InconsistencyError, AuthorisationError -from ..groups.models import Group, GroupRole, group_role_by_id - -from ... import db -from ...dictify import dictify - -resources = Blueprint("resources", __name__) - -@resources.route("/categories", methods=["GET"]) -@require_oauth("profile group resource") -def list_resource_categories() -> Response: - """Retrieve all resource categories""" - db_uri = app.config["AUTH_DB"] - with db.connection(db_uri) as conn: - return jsonify(tuple( - dictify(category) for category in resource_categories(conn))) - -@resources.route("/create", methods=["POST"]) -@require_oauth("profile group resource") -def create_resource() -> Response: - """Create a new resource""" - with require_oauth.acquire("profile group resource") as the_token: - form = request.form - resource_name = form.get("resource_name") - resource_category_id = uuid.UUID(form.get("resource_category")) - db_uri = app.config["AUTH_DB"] - with db.connection(db_uri) as conn: - try: - resource = _create_resource( - conn, - resource_name, - resource_category_by_id(conn, resource_category_id), - the_token.user, - (form.get("public") == "on")) - return jsonify(dictify(resource)) - except sqlite3.IntegrityError as sql3ie: - if sql3ie.args[0] == ("UNIQUE constraint failed: " - "resources.resource_name"): - raise InconsistencyError( - "You cannot have duplicate resource names.") from sql3ie - app.logger.debug( - f"{type(sql3ie)=}: {sql3ie=}") - raise - -@resources.route("/view/<uuid:resource_id>") -@require_oauth("profile group resource") -def view_resource(resource_id: uuid.UUID) -> Response: - """View a particular resource's details.""" - with require_oauth.acquire("profile group resource") as the_token: - db_uri = app.config["AUTH_DB"] - with db.connection(db_uri) as conn: - return jsonify(dictify(resource_by_id( - conn, the_token.user, resource_id))) - -def __safe_get_requests_page__(key: str = "page") -> int: - """Get the results page if it exists or default to the first page.""" - try: - return abs(int(request.args.get(key, "1"), base=10)) - except ValueError as _valerr: - return 1 - -def __safe_get_requests_count__(key: str = "count_per_page") -> int: - """Get the results page if it exists or default to the first page.""" - try: - count = request.args.get(key, "0") - if count != 0: - return abs(int(count, base=10)) - return 0 - except ValueError as _valerr: - return 0 - -@resources.route("/view/<uuid:resource_id>/data") -@require_oauth("profile group resource") -def view_resource_data(resource_id: uuid.UUID) -> Response: - """Retrieve a particular resource's data.""" - with require_oauth.acquire("profile group resource") as the_token: - db_uri = app.config["AUTH_DB"] - count_per_page = __safe_get_requests_count__("count_per_page") - offset = (__safe_get_requests_page__("page") - 1) - with db.connection(db_uri) as conn: - resource = resource_by_id(conn, the_token.user, resource_id) - return jsonify(resource_data( - conn, - resource, - ((offset * count_per_page) if bool(count_per_page) else offset), - count_per_page)) - -@resources.route("/data/link", methods=["POST"]) -@require_oauth("profile group resource") -def link_data(): - """Link group data to a specific resource.""" - try: - form = request.form - assert "resource_id" in form, "Resource ID not provided." - assert "data_link_id" in form, "Data Link ID not provided." - assert "dataset_type" in form, "Dataset type not specified" - assert form["dataset_type"].lower() in ( - "mrna", "genotype", "phenotype"), "Invalid dataset type provided." - - with require_oauth.acquire("profile group resource") as the_token: - def __link__(conn: db.DbConnection): - return link_data_to_resource( - conn, the_token.user, uuid.UUID(form["resource_id"]), - form["dataset_type"], uuid.UUID(form["data_link_id"])) - - return jsonify(with_db_connection(__link__)) - except AssertionError as aserr: - raise InvalidData(aserr.args[0]) from aserr - - - -@resources.route("/data/unlink", methods=["POST"]) -@require_oauth("profile group resource") -def unlink_data(): - """Unlink data bound to a specific resource.""" - try: - form = request.form - assert "resource_id" in form, "Resource ID not provided." - assert "data_link_id" in form, "Data Link ID not provided." - - with require_oauth.acquire("profile group resource") as the_token: - def __unlink__(conn: db.DbConnection): - return unlink_data_from_resource( - conn, the_token.user, uuid.UUID(form["resource_id"]), - uuid.UUID(form["data_link_id"])) - return jsonify(with_db_connection(__unlink__)) - except AssertionError as aserr: - raise InvalidData(aserr.args[0]) from aserr - -@resources.route("<uuid:resource_id>/user/list", methods=["GET"]) -@require_oauth("profile group resource") -def resource_users(resource_id: uuid.UUID): - """Retrieve all users with access to the given resource.""" - with require_oauth.acquire("profile group resource") as the_token: - def __the_users__(conn: db.DbConnection): - resource = resource_by_id(conn, the_token.user, resource_id) - authorised = authorised_for( - conn, the_token.user, ("group:resource:edit-resource",), - (resource_id,)) - if authorised.get(resource_id, False): - with db.cursor(conn) as cursor: - def __organise_users_n_roles__(users_n_roles, row): - user_id = uuid.UUID(row["user_id"]) - user = users_n_roles.get(user_id, {}).get( - "user", User(user_id, row["email"], row["name"])) - role = GroupRole( - uuid.UUID(row["group_role_id"]), - resource.group, - Role(uuid.UUID(row["role_id"]), row["role_name"], - bool(int(row["user_editable"])), tuple())) - return { - **users_n_roles, - user_id: { - "user": user, - "user_group": Group( - uuid.UUID(row["group_id"]), row["group_name"], - json.loads(row["group_metadata"])), - "roles": users_n_roles.get( - user_id, {}).get("roles", tuple()) + (role,) - } - } - cursor.execute( - "SELECT g.*, u.*, r.*, gr.group_role_id " - "FROM groups AS g INNER JOIN " - "group_users AS gu ON g.group_id=gu.group_id " - "INNER JOIN users AS u ON gu.user_id=u.user_id " - "INNER JOIN group_user_roles_on_resources AS guror " - "ON u.user_id=guror.user_id INNER JOIN roles AS r " - "ON guror.role_id=r.role_id " - "INNER JOIN group_roles AS gr ON r.role_id=gr.role_id " - "WHERE guror.resource_id=?", - (str(resource_id),)) - return reduce(__organise_users_n_roles__, cursor.fetchall(), {}) - raise AuthorisationError( - "You do not have sufficient privileges to view the resource " - "users.") - results = ( - { - "user": dictify(row["user"]), - "user_group": dictify(row["user_group"]), - "roles": tuple(dictify(role) for role in row["roles"]) - } for row in ( - user_row for user_id, user_row - in with_db_connection(__the_users__).items())) - return jsonify(tuple(results)) - -@resources.route("<uuid:resource_id>/user/assign", methods=["POST"]) -@require_oauth("profile group resource role") -def assign_role_to_user(resource_id: uuid.UUID) -> Response: - """Assign a role on the specified resource to a user.""" - with require_oauth.acquire("profile group resource role") as the_token: - try: - form = request.form - group_role_id = form.get("group_role_id", "") - user_email = form.get("user_email", "") - assert bool(group_role_id), "The role must be provided." - assert bool(user_email), "The user email must be provided." - - def __assign__(conn: db.DbConnection) -> dict: - resource = resource_by_id(conn, the_token.user, resource_id) - user = user_by_email(conn, user_email) - return assign_resource_user( - conn, resource, user, - group_role_by_id(conn, resource.group, - uuid.UUID(group_role_id))) - except AssertionError as aserr: - raise AuthorisationError(aserr.args[0]) from aserr - - return jsonify(with_db_connection(__assign__)) - -@resources.route("<uuid:resource_id>/user/unassign", methods=["POST"]) -@require_oauth("profile group resource role") -def unassign_role_to_user(resource_id: uuid.UUID) -> Response: - """Unassign a role on the specified resource from a user.""" - with require_oauth.acquire("profile group resource role") as the_token: - try: - form = request.form - group_role_id = form.get("group_role_id", "") - user_id = form.get("user_id", "") - assert bool(group_role_id), "The role must be provided." - assert bool(user_id), "The user id must be provided." - - def __assign__(conn: db.DbConnection) -> dict: - resource = resource_by_id(conn, the_token.user, resource_id) - return unassign_resource_user( - conn, resource, user_by_id(conn, uuid.UUID(user_id)), - group_role_by_id(conn, resource.group, - uuid.UUID(group_role_id))) - except AssertionError as aserr: - raise AuthorisationError(aserr.args[0]) from aserr - - return jsonify(with_db_connection(__assign__)) - -@resources.route("<uuid:resource_id>/toggle-public", methods=["POST"]) -@require_oauth("profile group resource role") -def toggle_public(resource_id: uuid.UUID) -> Response: - """Make a resource public if it is private, or private if public.""" - with require_oauth.acquire("profile group resource") as the_token: - def __toggle__(conn: db.DbConnection) -> Resource: - old_rsc = resource_by_id(conn, the_token.user, resource_id) - return save_resource( - conn, the_token.user, Resource( - old_rsc.group, old_rsc.resource_id, old_rsc.resource_name, - old_rsc.resource_category, not old_rsc.public, - old_rsc.resource_data)) - - resource = with_db_connection(__toggle__) - return jsonify({ - "resource": dictify(resource), - "description": ( - "Made resource public" if resource.public - else "Made resource private")}) diff --git a/gn3/auth/authorisation/roles/__init__.py b/gn3/auth/authorisation/roles/__init__.py deleted file mode 100644 index 293a12f..0000000 --- a/gn3/auth/authorisation/roles/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Initialise the `gn3.auth.authorisation.roles` package""" - -from .models import Role diff --git a/gn3/auth/authorisation/roles/models.py b/gn3/auth/authorisation/roles/models.py deleted file mode 100644 index 890d33b..0000000 --- a/gn3/auth/authorisation/roles/models.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Handle management of roles""" -from uuid import UUID, uuid4 -from functools import reduce -from typing import Any, Sequence, Iterable, NamedTuple - -from pymonad.either import Left, Right, Either - -from gn3.auth import db -from gn3.auth.dictify import dictify -from gn3.auth.authorisation.users import User -from gn3.auth.authorisation.errors import AuthorisationError - -from ..checks import authorised_p -from ..privileges import Privilege -from ..errors import NotFoundError - -class Role(NamedTuple): - """Class representing a role: creates immutable objects.""" - role_id: UUID - role_name: str - user_editable: bool - privileges: tuple[Privilege, ...] - - def dictify(self) -> dict[str, Any]: - """Return a dict representation of `Role` objects.""" - return { - "role_id": self.role_id, "role_name": self.role_name, - "user_editable": self.user_editable, - "privileges": tuple(dictify(priv) for priv in self.privileges) - } - -def check_user_editable(role: Role): - """Raise an exception if `role` is not user editable.""" - if not role.user_editable: - raise AuthorisationError( - f"The role `{role.role_name}` is not user editable.") - -@authorised_p( - privileges = ("group:role:create-role",), - error_description="Could not create role") -def create_role( - cursor: db.DbCursor, role_name: str, - privileges: Iterable[Privilege]) -> Role: - """ - Create a new generic role. - - PARAMS: - * cursor: A database cursor object - This function could be used as part of - a transaction, hence the use of a cursor rather than a connection - object. - * role_name: The name of the role - * privileges: A 'list' of privileges to assign the new role - - RETURNS: An immutable `gn3.auth.authorisation.roles.Role` object - """ - role = Role(uuid4(), role_name, True, tuple(privileges)) - - cursor.execute( - "INSERT INTO roles(role_id, role_name, user_editable) VALUES (?, ?, ?)", - (str(role.role_id), role.role_name, (1 if role.user_editable else 0))) - cursor.executemany( - "INSERT INTO role_privileges(role_id, privilege_id) VALUES (?, ?)", - tuple((str(role.role_id), str(priv.privilege_id)) - for priv in privileges)) - - return role - -def __organise_privileges__(roles_dict, privilege_row): - """Organise the privileges into their roles.""" - role_id_str = privilege_row["role_id"] - if role_id_str in roles_dict: - return { - **roles_dict, - role_id_str: Role( - UUID(role_id_str), - privilege_row["role_name"], - bool(int(privilege_row["user_editable"])), - roles_dict[role_id_str].privileges + ( - Privilege(privilege_row["privilege_id"], - privilege_row["privilege_description"]),)) - } - - return { - **roles_dict, - role_id_str: Role( - UUID(role_id_str), - privilege_row["role_name"], - bool(int(privilege_row["user_editable"])), - (Privilege(privilege_row["privilege_id"], - privilege_row["privilege_description"]),)) - } - -def user_roles(conn: db.DbConnection, user: User) -> Sequence[Role]: - """Retrieve non-resource roles assigned to the user.""" - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT r.*, p.* FROM user_roles AS ur INNER JOIN roles AS r " - "ON ur.role_id=r.role_id INNER JOIN role_privileges AS rp " - "ON r.role_id=rp.role_id INNER JOIN privileges AS p " - "ON rp.privilege_id=p.privilege_id WHERE ur.user_id=?", - (str(user.user_id),)) - - return tuple( - reduce(__organise_privileges__, cursor.fetchall(), {}).values()) - return tuple() - -def user_role(conn: db.DbConnection, user: User, role_id: UUID) -> Either: - """Retrieve a specific non-resource role assigned to the user.""" - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT r.*, p.* FROM user_roles AS ur INNER JOIN roles AS r " - "ON ur.role_id=r.role_id INNER JOIN role_privileges AS rp " - "ON r.role_id=rp.role_id INNER JOIN privileges AS p " - "ON rp.privilege_id=p.privilege_id " - "WHERE ur.user_id=? AND ur.role_id=?", - (str(user.user_id), str(role_id))) - - results = cursor.fetchall() - if results: - return Right(tuple( - reduce(__organise_privileges__, results, {}).values())[0]) - return Left(NotFoundError( - f"Could not find role with id '{role_id}'",)) - -def assign_default_roles(cursor: db.DbCursor, user: User): - """Assign `user` some default roles.""" - cursor.execute( - 'SELECT role_id FROM roles WHERE role_name IN ' - '("group-creator")') - role_ids = cursor.fetchall() - str_user_id = str(user.user_id) - params = tuple( - {"user_id": str_user_id, "role_id": row["role_id"]} for row in role_ids) - cursor.executemany( - ("INSERT INTO user_roles VALUES (:user_id, :role_id)"), - params) - -def revoke_user_role_by_name(cursor: db.DbCursor, user: User, role_name: str): - """Revoke a role from `user` by the role's name""" - cursor.execute( - "SELECT role_id FROM roles WHERE role_name=:role_name", - {"role_name": role_name}) - role = cursor.fetchone() - if role: - cursor.execute( - ("DELETE FROM user_roles " - "WHERE user_id=:user_id AND role_id=:role_id"), - {"user_id": str(user.user_id), "role_id": role["role_id"]}) - -def assign_user_role_by_name(cursor: db.DbCursor, user: User, role_name: str): - """Revoke a role from `user` by the role's name""" - cursor.execute( - "SELECT role_id FROM roles WHERE role_name=:role_name", - {"role_name": role_name}) - role = cursor.fetchone() - - if role: - cursor.execute( - ("INSERT INTO user_roles VALUES(:user_id, :role_id) " - "ON CONFLICT DO NOTHING"), - {"user_id": str(user.user_id), "role_id": role["role_id"]}) diff --git a/gn3/auth/authorisation/roles/views.py b/gn3/auth/authorisation/roles/views.py deleted file mode 100644 index d00e596..0000000 --- a/gn3/auth/authorisation/roles/views.py +++ /dev/null @@ -1,25 +0,0 @@ -"""The views/routes for the `gn3.auth.authorisation.roles` package.""" -import uuid - -from flask import jsonify, Response, Blueprint, current_app - -from gn3.auth import db -from gn3.auth.dictify import dictify -from gn3.auth.authorisation.oauth2.resource_server import require_oauth - -from .models import user_role - -roles = Blueprint("roles", __name__) - -@roles.route("/view/<uuid:role_id>", methods=["GET"]) -@require_oauth("profile role") -def view_role(role_id: uuid.UUID) -> Response: - """Retrieve a user role with id `role_id`""" - def __error__(exc: Exception): - raise exc - with require_oauth.acquire("profile role") as the_token: - db_uri = current_app.config["AUTH_DB"] - with db.connection(db_uri) as conn: - the_role = user_role(conn, the_token.user, role_id) - return the_role.either( - __error__, lambda a_role: jsonify(dictify(a_role))) diff --git a/gn3/auth/authorisation/users/__init__.py b/gn3/auth/authorisation/users/__init__.py deleted file mode 100644 index 5f0c89c..0000000 --- a/gn3/auth/authorisation/users/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Initialise the users' package.""" -from .base import ( - User, - users, - save_user, - user_by_id, - # valid_login, - user_by_email, - hash_password, # only used in tests... maybe make gn-auth a GN3 dependency - same_password, - set_user_password -) diff --git a/gn3/auth/authorisation/users/base.py b/gn3/auth/authorisation/users/base.py deleted file mode 100644 index 0e72ed2..0000000 --- a/gn3/auth/authorisation/users/base.py +++ /dev/null @@ -1,128 +0,0 @@ -"""User-specific code and data structures.""" -from uuid import UUID, uuid4 -from typing import Any, Tuple, NamedTuple - -from argon2 import PasswordHasher -from argon2.exceptions import VerifyMismatchError - -from gn3.auth import db -from gn3.auth.authorisation.errors import NotFoundError - -class User(NamedTuple): - """Class representing a user.""" - user_id: UUID - email: str - name: str - - def get_user_id(self): - """Return the user's UUID. Mostly for use with Authlib.""" - return self.user_id - - def dictify(self) -> dict[str, Any]: - """Return a dict representation of `User` objects.""" - return {"user_id": self.user_id, "email": self.email, "name": self.name} - -DUMMY_USER = User(user_id=UUID("a391cf60-e8b7-4294-bd22-ddbbda4b3530"), - email="gn3@dummy.user", - name="Dummy user to use as placeholder") - -def user_by_email(conn: db.DbConnection, email: str) -> User: - """Retrieve user from database by their email address""" - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM users WHERE email=?", (email,)) - row = cursor.fetchone() - - if row: - return User(UUID(row["user_id"]), row["email"], row["name"]) - - raise NotFoundError(f"Could not find user with email {email}") - -def user_by_id(conn: db.DbConnection, user_id: UUID) -> User: - """Retrieve user from database by their user id""" - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM users WHERE user_id=?", (str(user_id),)) - row = cursor.fetchone() - - if row: - return User(UUID(row["user_id"]), row["email"], row["name"]) - - raise NotFoundError(f"Could not find user with ID {user_id}") - -def same_password(password: str, hashed: str) -> bool: - """Check that `raw_password` is hashed to `hash`""" - try: - return hasher().verify(hashed, password) - except VerifyMismatchError as _vme: - return False - -def valid_login(conn: db.DbConnection, user: User, password: str) -> bool: - """Check the validity of the provided credentials for login.""" - with db.cursor(conn) as cursor: - cursor.execute( - ("SELECT * FROM users LEFT JOIN user_credentials " - "ON users.user_id=user_credentials.user_id " - "WHERE users.user_id=?"), - (str(user.user_id),)) - row = cursor.fetchone() - - if row is None: - return False - - return same_password(password, row["password"]) - -def save_user(cursor: db.DbCursor, email: str, name: str) -> User: - """ - Create and persist a user. - - The user creation could be done during a transaction, therefore the function - takes a cursor object rather than a connection. - - The newly created and persisted user is then returned. - """ - user_id = uuid4() - cursor.execute("INSERT INTO users VALUES (?, ?, ?)", - (str(user_id), email, name)) - return User(user_id, email, name) - -def hasher(): - """Retrieve PasswordHasher object""" - # TODO: Maybe tune the parameters here... - # Tuneable Parameters: - # - time_cost (default: 2) - # - memory_cost (default: 102400) - # - parallelism (default: 8) - # - hash_len (default: 16) - # - salt_len (default: 16) - # - encoding (default: 'utf-8') - # - type (default: <Type.ID: 2>) - return PasswordHasher() - -def hash_password(password): - """Hash the password.""" - return hasher().hash(password) - -def set_user_password( - cursor: db.DbCursor, user: User, password: str) -> Tuple[User, bytes]: - """Set the given user's password in the database.""" - hashed_password = hash_password(password) - cursor.execute( - ("INSERT INTO user_credentials VALUES (:user_id, :hash) " - "ON CONFLICT (user_id) DO UPDATE SET password=:hash"), - {"user_id": str(user.user_id), "hash": hashed_password}) - return user, hashed_password - -def users(conn: db.DbConnection, - ids: tuple[UUID, ...] = tuple()) -> tuple[User, ...]: - """ - Fetch all users with the given `ids`. If `ids` is empty, return ALL users. - """ - params = ", ".join(["?"] * len(ids)) - with db.cursor(conn) as cursor: - query = "SELECT * FROM users" + ( - f" WHERE user_id IN ({params})" - if len(ids) > 0 else "") - print(query) - cursor.execute(query, tuple(str(the_id) for the_id in ids)) - return tuple(User(UUID(row["user_id"]), row["email"], row["name"]) - for row in cursor.fetchall()) - return tuple() diff --git a/gn3/auth/authorisation/users/collections/__init__.py b/gn3/auth/authorisation/users/collections/__init__.py deleted file mode 100644 index 88ab040..0000000 --- a/gn3/auth/authorisation/users/collections/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package dealing with user collections.""" diff --git a/gn3/auth/authorisation/users/collections/models.py b/gn3/auth/authorisation/users/collections/models.py deleted file mode 100644 index 7577fa8..0000000 --- a/gn3/auth/authorisation/users/collections/models.py +++ /dev/null @@ -1,269 +0,0 @@ -"""Handle user collections.""" -import json -from uuid import UUID, uuid4 -from datetime import datetime - -from redis import Redis -from email_validator import validate_email, EmailNotValidError - -from gn3.auth.authorisation.errors import InvalidData, NotFoundError - -from ..models import User - -__OLD_REDIS_COLLECTIONS_KEY__ = "collections" -__REDIS_COLLECTIONS_KEY__ = "collections2" - -class CollectionJSONEncoder(json.JSONEncoder): - """Serialise collection objects into JSON.""" - def default(self, obj):# pylint: disable=[arguments-renamed] - if isinstance(obj, UUID): - return str(obj) - if isinstance(obj, datetime): - return obj.strftime("%b %d %Y %I:%M%p") - return json.JSONEncoder.default(self, obj) - -def __valid_email__(email:str) -> bool: - """Check for email validity.""" - try: - validate_email(email, check_deliverability=True) - except EmailNotValidError as _enve: - return False - return True - -def __toggle_boolean_field__( - rconn: Redis, email: str, field: str): - """Toggle the valuen of a boolean field""" - mig_dict = json.loads(rconn.hget("migratable-accounts", email) or "{}") - if bool(mig_dict): - rconn.hset("migratable-accounts", email, - json.dumps({**mig_dict, field: not mig_dict.get(field, True)})) - -def __build_email_uuid_bridge__(rconn: Redis): - """ - Build a connection between new accounts and old user accounts. - - The only thing that is common between the two is the email address, - therefore, we use that to link the two items. - """ - old_accounts = { - account["email_address"]: { - "user_id": account["user_id"], - "collections-migrated": False, - "resources_migrated": False - } for account in ( - acct for acct in - (json.loads(usr) for usr in rconn.hgetall("users").values()) - if (bool(acct.get("email_address", False)) and - __valid_email__(acct["email_address"]))) - } - if bool(old_accounts): - rconn.hset("migratable-accounts", mapping={ - key: json.dumps(value) for key,value in old_accounts.items() - }) - return old_accounts - -def __retrieve_old_accounts__(rconn: Redis) -> dict: - accounts = rconn.hgetall("migratable-accounts") - if accounts: - return { - key: json.loads(value) for key, value in accounts.items() - } - return __build_email_uuid_bridge__(rconn) - -def parse_collection(coll: dict) -> dict: - """Parse the collection as persisted in redis to a usable python object.""" - created = coll.get("created", coll.get("created_timestamp")) - changed = coll.get("changed", coll.get("changed_timestamp")) - return { - "id": UUID(coll["id"]), - "name": coll["name"], - "created": datetime.strptime(created, "%b %d %Y %I:%M%p"), - "changed": datetime.strptime(changed, "%b %d %Y %I:%M%p"), - "num_members": int(coll["num_members"]), - "members": coll["members"] - } - -def dump_collection(pythoncollection: dict) -> str: - """Convert the collection from a python object to a json string.""" - return json.dumps(pythoncollection, cls=CollectionJSONEncoder) - -def __retrieve_old_user_collections__(rconn: Redis, old_user_id: UUID) -> tuple: - """Retrieve any old collections relating to the user.""" - return tuple(parse_collection(coll) for coll in - json.loads(rconn.hget( - __OLD_REDIS_COLLECTIONS_KEY__, str(old_user_id)) or "[]")) - -def user_collections(rconn: Redis, user: User) -> tuple[dict, ...]: - """Retrieve current user collections.""" - collections = tuple(parse_collection(coll) for coll in json.loads( - rconn.hget(__REDIS_COLLECTIONS_KEY__, str(user.user_id)) or - "[]")) - old_accounts = __retrieve_old_accounts__(rconn) - if (user.email in old_accounts and - not old_accounts[user.email]["collections-migrated"]): - old_user_id = old_accounts[user.email]["user_id"] - collections = tuple({ - coll["id"]: coll for coll in ( - collections + __retrieve_old_user_collections__( - rconn, UUID(old_user_id))) - }.values()) - __toggle_boolean_field__(rconn, user.email, "collections-migrated") - rconn.hset( - __REDIS_COLLECTIONS_KEY__, - key=str(user.user_id), - value=json.dumps(collections, cls=CollectionJSONEncoder)) - return collections - -def save_collections(rconn: Redis, user: User, collections: tuple[dict, ...]) -> tuple[dict, ...]: - """Save the `collections` to redis.""" - rconn.hset( - __REDIS_COLLECTIONS_KEY__, - str(user.user_id), - json.dumps(collections, cls=CollectionJSONEncoder)) - return collections - -def add_to_user_collections(rconn: Redis, user: User, collection: dict) -> dict: - """Add `collection` to list of user collections.""" - ucolls = user_collections(rconn, user) - save_collections(rconn, user, ucolls + (collection,)) - return collection - -def create_collection(rconn: Redis, user: User, name: str, traits: tuple) -> dict: - """Create a new collection.""" - now = datetime.utcnow() - return add_to_user_collections(rconn, user, { - "id": uuid4(), - "name": name, - "created": now, - "changed": now, - "num_members": len(traits), - "members": traits - }) - -def get_collection(rconn: Redis, user: User, collection_id: UUID) -> dict: - """Retrieve the collection with ID `collection_id`.""" - colls = tuple(coll for coll in user_collections(rconn, user) - if coll["id"] == collection_id) - if len(colls) == 0: - raise NotFoundError( - f"Could not find a collection with ID `{collection_id}` for user " - f"with ID `{user.user_id}`") - if len(colls) > 1: - err = InvalidData( - "More than one collection was found having the ID " - f"`{collection_id}` for user with ID `{user.user_id}`.") - err.error_code = 513 - raise err - return colls[0] - -def __raise_if_collections_empty__(user: User, collections: tuple[dict, ...]): - """Raise an exception if no collections are found for `user`.""" - if len(collections) < 1: - raise NotFoundError(f"No collections found for user `{user.user_id}`") - -def __raise_if_not_single_collection__( - user: User, collection_id: UUID, collections: tuple[dict, ...]): - """ - Raise an exception there is zero, or more than one collection for `user`. - """ - if len(collections) == 0: - raise NotFoundError(f"No collections found for user `{user.user_id}` " - f"with ID `{collection_id}`.") - if len(collections) > 1: - err = InvalidData( - "More than one collection was found having the ID " - f"`{collection_id}` for user with ID `{user.user_id}`.") - err.error_code = 513 - raise err - -def delete_collections(rconn: Redis, - user: User, - collection_ids: tuple[UUID, ...]) -> tuple[dict, ...]: - """ - Delete collections with the given `collection_ids` returning the deleted - collections. - """ - ucolls = user_collections(rconn, user) - save_collections( - rconn, - user, - tuple(coll for coll in ucolls if coll["id"] not in collection_ids)) - return tuple(coll for coll in ucolls if coll["id"] in collection_ids) - -def add_traits(rconn: Redis, - user: User, - collection_id: UUID, - traits: tuple[str, ...]) -> dict: - """ - Add `traits` to the `user` collection identified by `collection_id`. - - Returns: The collection with the new traits added. - """ - ucolls = user_collections(rconn, user) - __raise_if_collections_empty__(user, ucolls) - - mod_col = tuple(coll for coll in ucolls if coll["id"] == collection_id) - __raise_if_not_single_collection__(user, collection_id, mod_col) - new_members = tuple(set(tuple(mod_col[0]["members"]) + traits)) - new_coll = { - **mod_col[0], - "members": new_members, - "num_members": len(new_members) - } - save_collections( - rconn, - user, - (tuple(coll for coll in ucolls if coll["id"] != collection_id) + - (new_coll,))) - return new_coll - -def remove_traits(rconn: Redis, - user: User, - collection_id: UUID, - traits: tuple[str, ...]) -> dict: - """ - Remove `traits` from the `user` collection identified by `collection_id`. - - Returns: The collection with the specified `traits` removed. - """ - ucolls = user_collections(rconn, user) - __raise_if_collections_empty__(user, ucolls) - - mod_col = tuple(coll for coll in ucolls if coll["id"] == collection_id) - __raise_if_not_single_collection__(user, collection_id, mod_col) - new_members = tuple( - trait for trait in mod_col[0]["members"] if trait not in traits) - new_coll = { - **mod_col[0], - "members": new_members, - "num_members": len(new_members) - } - save_collections( - rconn, - user, - (tuple(coll for coll in ucolls if coll["id"] != collection_id) + - (new_coll,))) - return new_coll - -def change_name(rconn: Redis, - user: User, - collection_id: UUID, - new_name: str) -> dict: - """ - Change the collection's name. - - Returns: The collection with the new name. - """ - ucolls = user_collections(rconn, user) - __raise_if_collections_empty__(user, ucolls) - - mod_col = tuple(coll for coll in ucolls if coll["id"] == collection_id) - __raise_if_not_single_collection__(user, collection_id, mod_col) - - new_coll = {**mod_col[0], "name": new_name} - save_collections( - rconn, - user, - (tuple(coll for coll in ucolls if coll["id"] != collection_id) + - (new_coll,))) - return new_coll diff --git a/gn3/auth/authorisation/users/collections/views.py b/gn3/auth/authorisation/users/collections/views.py deleted file mode 100644 index 775e8bc..0000000 --- a/gn3/auth/authorisation/users/collections/views.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Views regarding user collections.""" -from uuid import UUID - -from redis import Redis -from flask import jsonify, request, Response, Blueprint, current_app - -from gn3.auth import db -from gn3.auth.db_utils import with_db_connection -from gn3.auth.authorisation.checks import require_json -from gn3.auth.authorisation.errors import NotFoundError - -from gn3.auth.authorisation.users import User, user_by_id -from gn3.auth.authorisation.oauth2.resource_server import require_oauth - -from .models import ( - add_traits, - change_name, - remove_traits, - get_collection, - user_collections, - save_collections, - create_collection, - delete_collections as _delete_collections) - -collections = Blueprint("collections", __name__) - -@collections.route("/list") -@require_oauth("profile user") -def list_user_collections() -> Response: - """Retrieve the user ids""" - with (require_oauth.acquire("profile user") as the_token, - Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - return jsonify(user_collections(redisconn, the_token.user)) - -@collections.route("/<uuid:anon_id>/list") -def list_anonymous_collections(anon_id: UUID) -> Response: - """Fetch anonymous collections""" - with Redis.from_url( - current_app.config["REDIS_URI"], decode_responses=True) as redisconn: - def __list__(conn: db.DbConnection) -> tuple: - try: - _user = user_by_id(conn, anon_id) - current_app.logger.warning( - "Fetch collections for authenticated user using the " - "`list_user_collections()` endpoint.") - return tuple() - except NotFoundError as _nfe: - return user_collections( - redisconn, User(anon_id, "anon@ymous.user", "Anonymous User")) - - return jsonify(with_db_connection(__list__)) - -@require_oauth("profile user") -def __new_collection_as_authenticated_user__(redisconn, name, traits): - """Create a new collection as an authenticated user.""" - with require_oauth.acquire("profile user") as token: - return create_collection(redisconn, token.user, name, traits) - -def __new_collection_as_anonymous_user__(redisconn, name, traits): - """Create a new collection as an anonymous user.""" - return create_collection(redisconn, - User(UUID(request.json.get("anon_id")), - "anon@ymous.user", - "Anonymous User"), - name, - traits) - -@collections.route("/new", methods=["POST"]) -@require_json -def new_user_collection() -> Response: - """Create a new collection.""" - with (Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - traits = tuple(request.json.get("traits", tuple()))# type: ignore[union-attr] - name = request.json.get("name")# type: ignore[union-attr] - if bool(request.headers.get("Authorization")): - return jsonify(__new_collection_as_authenticated_user__( - redisconn, name, traits)) - return jsonify(__new_collection_as_anonymous_user__( - redisconn, name, traits)) - -@collections.route("/<uuid:collection_id>/view", methods=["POST"]) -@require_json -def view_collection(collection_id: UUID) -> Response: - """View a particular collection""" - with (Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - if bool(request.headers.get("Authorization")): - with require_oauth.acquire("profile user") as token: - return jsonify(get_collection(redisconn, - token.user, - collection_id)) - return jsonify(get_collection( - redisconn, - User( - UUID(request.json.get("anon_id")),#type: ignore[union-attr] - "anon@ymous.user", - "Anonymous User"), - collection_id)) - -@collections.route("/anonymous/import", methods=["POST"]) -@require_json -@require_oauth("profile user") -def import_anonymous() -> Response: - """Import anonymous collections.""" - with (require_oauth.acquire("profile user") as token, - Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - anon_id = UUID(request.json.get("anon_id"))#type: ignore[union-attr] - anon_colls = user_collections(redisconn, User( - anon_id, "anon@ymous.user", "Anonymous User")) - save_collections( - redisconn, - token.user, - (user_collections(redisconn, token.user) + - anon_colls)) - redisconn.hdel("collections", str(anon_id)) - return jsonify({ - "message": f"Import of {len(anon_colls)} was successful." - }) - -@collections.route("/anonymous/delete", methods=["POST"]) -@require_json -@require_oauth("profile user") -def delete_anonymous() -> Response: - """Delete anonymous collections.""" - with (require_oauth.acquire("profile user") as _token, - Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - anon_id = UUID(request.json.get("anon_id"))#type: ignore[union-attr] - anon_colls = user_collections(redisconn, User( - anon_id, "anon@ymous.user", "Anonymous User")) - redisconn.hdel("collections", str(anon_id)) - return jsonify({ - "message": f"Deletion of {len(anon_colls)} was successful." - }) - -@collections.route("/delete", methods=["POST"]) -@require_json -def delete_collections(): - """Delete specified collections.""" - with (Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - coll_ids = tuple(UUID(cid) for cid in request.json["collection_ids"]) - deleted = _delete_collections( - redisconn, - User(request.json["anon_id"], "anon@ymous.user", "Anonymous User"), - coll_ids) - if bool(request.headers.get("Authorization")): - with require_oauth.acquire("profile user") as token: - deleted = deleted + _delete_collections( - redisconn, token.user, coll_ids) - - return jsonify({ - "message": f"Deleted {len(deleted)} collections."}) - -@collections.route("/<uuid:collection_id>/traits/remove", methods=["POST"]) -@require_json -def remove_traits_from_collection(collection_id: UUID) -> Response: - """Remove specified traits from collection with ID `collection_id`.""" - if len(request.json["traits"]) < 1:#type: ignore[index] - return jsonify({"message": "No trait to remove from collection."}) - - the_traits = tuple(request.json["traits"])#type: ignore[index] - with (Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - if not bool(request.headers.get("Authorization")): - coll = remove_traits( - redisconn, - User(request.json["anon_id"],#type: ignore[index] - "anon@ymous.user", - "Anonymous User"), - collection_id, - the_traits) - else: - with require_oauth.acquire("profile user") as token: - coll = remove_traits( - redisconn, token.user, collection_id, the_traits) - - return jsonify({ - "message": f"Deleted {len(the_traits)} traits from collection.", - "collection": coll - }) - -@collections.route("/<uuid:collection_id>/traits/add", methods=["POST"]) -@require_json -def add_traits_to_collection(collection_id: UUID) -> Response: - """Add specified traits to collection with ID `collection_id`.""" - if len(request.json["traits"]) < 1:#type: ignore[index] - return jsonify({"message": "No trait to add to collection."}) - - the_traits = tuple(request.json["traits"])#type: ignore[index] - with (Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - if not bool(request.headers.get("Authorization")): - coll = add_traits( - redisconn, - User(request.json["anon_id"],#type: ignore[index] - "anon@ymous.user", - "Anonymous User"), - collection_id, - the_traits) - else: - with require_oauth.acquire("profile user") as token: - coll = add_traits( - redisconn, token.user, collection_id, the_traits) - - return jsonify({ - "message": f"Added {len(the_traits)} traits to collection.", - "collection": coll - }) - -@collections.route("/<uuid:collection_id>/rename", methods=["POST"]) -@require_json -def rename_collection(collection_id: UUID) -> Response: - """Rename the given collection""" - if not bool(request.json["new_name"]):#type: ignore[index] - return jsonify({"message": "No new name to change to."}) - - new_name = request.json["new_name"]#type: ignore[index] - with (Redis.from_url(current_app.config["REDIS_URI"], - decode_responses=True) as redisconn): - if not bool(request.headers.get("Authorization")): - coll = change_name(redisconn, - User(UUID(request.json["anon_id"]),#type: ignore[index] - "anon@ymous.user", - "Anonymous User"), - collection_id, - new_name) - else: - with require_oauth.acquire("profile user") as token: - coll = change_name( - redisconn, token.user, collection_id, new_name) - - return jsonify({ - "message": "Collection rename successful.", - "collection": coll - }) diff --git a/gn3/auth/authorisation/users/models.py b/gn3/auth/authorisation/users/models.py deleted file mode 100644 index 0157154..0000000 --- a/gn3/auth/authorisation/users/models.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Functions for acting on users.""" -import uuid -from functools import reduce - -from gn3.auth import db -from gn3.auth.authorisation.roles.models import Role -from gn3.auth.authorisation.checks import authorised_p -from gn3.auth.authorisation.privileges import Privilege - -from .base import User - -@authorised_p( - ("system:user:list",), - "You do not have the appropriate privileges to list users.", - oauth2_scope="profile user") -def list_users(conn: db.DbConnection) -> tuple[User, ...]: - """List out all users.""" - with db.cursor(conn) as cursor: - cursor.execute("SELECT * FROM users") - return tuple( - User(uuid.UUID(row["user_id"]), row["email"], row["name"]) - for row in cursor.fetchall()) - -def __build_resource_roles__(rows): - def __build_roles__(roles, row): - role_id = uuid.UUID(row["role_id"]) - priv = Privilege(row["privilege_id"], row["privilege_description"]) - role = roles.get(role_id, Role( - role_id, row["role_name"], bool(row["user_editable"]), tuple())) - return { - **roles, - role_id: Role(role_id, role.role_name, role.user_editable, role.privileges + (priv,)) - } - def __build__(acc, row): - resource_id = uuid.UUID(row["resource_id"]) - return { - **acc, - resource_id: __build_roles__(acc.get(resource_id, {}), row) - } - return { - resource_id: tuple(roles.values()) - for resource_id, roles in reduce(__build__, rows, {}).items() - } - -# @authorised_p( -# ("",), -# ("You do not have the appropriate privileges to view a user's roles on " -# "resources.")) -def user_resource_roles(conn: db.DbConnection, user: User) -> dict[uuid.UUID, tuple[Role, ...]]: - """Fetch all the user's roles on resources.""" - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT res.*, rls.*, p.*" - "FROM resources AS res INNER JOIN " - "group_user_roles_on_resources AS guror " - "ON res.resource_id=guror.resource_id " - "LEFT JOIN roles AS rls " - "ON guror.role_id=rls.role_id " - "LEFT JOIN role_privileges AS rp " - "ON rls.role_id=rp.role_id " - "LEFT JOIN privileges AS p " - "ON rp.privilege_id=p.privilege_id " - "WHERE guror.user_id = ?", - (str(user.user_id),)) - return __build_resource_roles__( - (dict(row) for row in cursor.fetchall())) diff --git a/gn3/auth/authorisation/users/views.py b/gn3/auth/authorisation/users/views.py deleted file mode 100644 index f75b51e..0000000 --- a/gn3/auth/authorisation/users/views.py +++ /dev/null @@ -1,173 +0,0 @@ -"""User authorisation endpoints.""" -import traceback -from typing import Any -from functools import partial - -import sqlite3 -from email_validator import validate_email, EmailNotValidError -from flask import request, jsonify, Response, Blueprint, current_app - -from gn3.auth import db -from gn3.auth.dictify import dictify -from gn3.auth.db_utils import with_db_connection -from gn3.auth.authorisation.oauth2.resource_server import require_oauth -from gn3.auth.authorisation.users import User, save_user, set_user_password -from gn3.auth.authorisation.oauth2.oauth2token import token_by_access_token - -from .models import list_users -from .collections.views import collections - -from ..groups.models import user_group as _user_group -from ..resources.models import user_resources as _user_resources -from ..roles.models import assign_default_roles, user_roles as _user_roles -from ..errors import ( - NotFoundError, UsernameError, PasswordError, UserRegistrationError) - -users = Blueprint("users", __name__) -users.register_blueprint(collections, url_prefix="/collections") - -@users.route("/", methods=["GET"]) -@require_oauth("profile") -def user_details() -> Response: - """Return user's details.""" - with require_oauth.acquire("profile") as the_token: - user = the_token.user - user_dets = { - "user_id": user.user_id, "email": user.email, "name": user.name, - "group": False - } - with db.connection(current_app.config["AUTH_DB"]) as conn: - the_group = _user_group(conn, user).maybe(# type: ignore[misc] - False, lambda grp: grp)# type: ignore[arg-type] - return jsonify({ - **user_dets, - "group": dictify(the_group) if the_group else False - }) - -@users.route("/roles", methods=["GET"]) -@require_oauth("role") -def user_roles() -> Response: - """Return the non-resource roles assigned to the user.""" - with require_oauth.acquire("role") as token: - with db.connection(current_app.config["AUTH_DB"]) as conn: - return jsonify(tuple( - dictify(role) for role in _user_roles(conn, token.user))) - -def validate_password(password, confirm_password) -> str: - """Validate the provided password.""" - if len(password) < 8: - raise PasswordError("The password must be at least 8 characters long.") - - if password != confirm_password: - raise PasswordError("Mismatched password values") - - return password - -def validate_username(name: str) -> str: - """Validate the provides name.""" - if name == "": - raise UsernameError("User's name not provided.") - - return name - -def __assert_not_logged_in__(conn: db.DbConnection): - bearer = request.headers.get('Authorization') - if bearer: - token = token_by_access_token(conn, bearer.split(None)[1]).maybe(# type: ignore[misc] - False, lambda tok: tok) - if token: - raise UserRegistrationError( - "Cannot register user while authenticated") - -@users.route("/register", methods=["POST"]) -def register_user() -> Response: - """Register a user.""" - with db.connection(current_app.config["AUTH_DB"]) as conn: - __assert_not_logged_in__(conn) - - try: - form = request.form - email = validate_email(form.get("email", "").strip(), - check_deliverability=True) - password = validate_password( - form.get("password", "").strip(), - form.get("confirm_password", "").strip()) - user_name = validate_username(form.get("user_name", "").strip()) - with db.cursor(conn) as cursor: - user, _hashed_password = set_user_password( - cursor, save_user( - cursor, email["email"], user_name), password) - assign_default_roles(cursor, user) - return jsonify( - { - "user_id": user.user_id, - "email": user.email, - "name": user.name - }) - except sqlite3.IntegrityError as sq3ie: - current_app.logger.debug(traceback.format_exc()) - raise UserRegistrationError( - "A user with that email already exists") from sq3ie - except EmailNotValidError as enve: - current_app.logger.debug(traceback.format_exc()) - raise(UserRegistrationError(f"Email Error: {str(enve)}")) from enve - - raise Exception( - "unknown_error", "The system experienced an unexpected error.") - -@users.route("/group", methods=["GET"]) -@require_oauth("profile group") -def user_group() -> Response: - """Retrieve the group in which the user is a member.""" - with require_oauth.acquire("profile group") as the_token: - db_uri = current_app.config["AUTH_DB"] - with db.connection(db_uri) as conn: - group = _user_group(conn, the_token.user).maybe(# type: ignore[misc] - False, lambda grp: grp)# type: ignore[arg-type] - - if group: - return jsonify(dictify(group)) - raise NotFoundError("User is not a member of any group.") - -@users.route("/resources", methods=["GET"]) -@require_oauth("profile resource") -def user_resources() -> Response: - """Retrieve the resources a user has access to.""" - with require_oauth.acquire("profile resource") as the_token: - db_uri = current_app.config["AUTH_DB"] - with db.connection(db_uri) as conn: - return jsonify([ - dictify(resource) for resource in - _user_resources(conn, the_token.user)]) - -@users.route("group/join-request", methods=["GET"]) -@require_oauth("profile group") -def user_join_request_exists(): - """Check whether a user has an active group join request.""" - def __request_exists__(conn: db.DbConnection, user: User) -> dict[str, Any]: - with db.cursor(conn) as cursor: - cursor.execute( - "SELECT * FROM group_join_requests WHERE requester_id=? AND " - "status = 'PENDING'", - (str(user.user_id),)) - res = cursor.fetchone() - if res: - return { - "request_id": res["request_id"], - "exists": True - } - return{ - "status": "Not found", - "exists": False - } - with require_oauth.acquire("profile group") as the_token: - return jsonify(with_db_connection(partial( - __request_exists__, user=the_token.user))) - -@users.route("/list", methods=["GET"]) -@require_oauth("profile user") -def list_all_users() -> Response: - """List all the users.""" - with require_oauth.acquire("profile group") as _the_token: - return jsonify(tuple( - dictify(user) for user in with_db_connection(list_users))) diff --git a/gn3/auth/views.py b/gn3/auth/views.py deleted file mode 100644 index da64049..0000000 --- a/gn3/auth/views.py +++ /dev/null @@ -1,16 +0,0 @@ -"""The Auth(oris|entic)ation routes/views""" -from flask import Blueprint - -from .authorisation.data.views import data -from .authorisation.users.views import users -from .authorisation.roles.views import roles -from .authorisation.groups.views import groups -from .authorisation.resources.views import resources - -oauth2 = Blueprint("oauth2", __name__) - -oauth2.register_blueprint(data, url_prefix="/data") -oauth2.register_blueprint(users, url_prefix="/user") -oauth2.register_blueprint(roles, url_prefix="/role") -oauth2.register_blueprint(groups, url_prefix="/group") -oauth2.register_blueprint(resources, url_prefix="/resource") |
