diff options
Diffstat (limited to 'gn_auth/auth/authorisation')
-rw-r--r-- | gn_auth/auth/authorisation/resources/models.py | 8 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/resources/phenotype.py | 68 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/resources/phenotypes/__init__.py | 1 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/resources/phenotypes/models.py | 142 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/resources/phenotypes/views.py | 77 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/resources/views.py | 15 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/users/admin/ui.py | 4 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/users/admin/views.py | 41 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/users/masquerade/models.py | 43 | ||||
-rw-r--r-- | gn_auth/auth/authorisation/users/masquerade/views.py | 13 |
10 files changed, 306 insertions, 106 deletions
diff --git a/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py index d3df6dc..c1748f1 100644 --- a/gn_auth/auth/authorisation/resources/models.py +++ b/gn_auth/auth/authorisation/resources/models.py @@ -29,7 +29,7 @@ from .genotypes.models import ( attach_resources_data as genotype_attach_resources_data, link_data_to_resource as genotype_link_data_to_resource, unlink_data_from_resource as genotype_unlink_data_from_resource) -from .phenotype import ( +from .phenotypes.models import ( resource_data as phenotype_resource_data, attach_resources_data as phenotype_attach_resources_data, link_data_to_resource as phenotype_link_data_to_resource, @@ -133,8 +133,10 @@ def user_resources(conn: db.DbConnection, user: User) -> Sequence[Resource]: """List the resources available to the user""" with db.cursor(conn) as cursor: cursor.execute( - ("SELECT r.*, rc.resource_category_key, " - "rc.resource_category_description FROM user_roles AS ur " + ("SELECT DISTINCT(r.resource_id), r.resource_name, " + "r.resource_category_id, r.public, rc.resource_category_key, " + "rc.resource_category_description " + "FROM user_roles AS ur " "INNER JOIN resources AS r ON ur.resource_id=r.resource_id " "INNER JOIN resource_categories AS rc " "ON r.resource_category_id=rc.resource_category_id " diff --git a/gn_auth/auth/authorisation/resources/phenotype.py b/gn_auth/auth/authorisation/resources/phenotype.py deleted file mode 100644 index 7005db3..0000000 --- a/gn_auth/auth/authorisation/resources/phenotype.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Phenotype data resources functions and utilities.""" -import uuid -from typing import Optional, Sequence - -import sqlite3 - -import gn_auth.auth.db.sqlite3 as db - -from .base import Resource -from .data import __attach_data__ - -def resource_data( - cursor: db.DbCursor, - resource_id: uuid.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 link_data_to_resource( - conn: db.DbConnection, - resource: Resource, - data_link_id: uuid.UUID) -> dict: - """Link Phenotype data with a resource.""" - with db.cursor(conn) as cursor: - params = { - "resource_id": str(resource.resource_id), - "data_link_id": str(data_link_id) - } - cursor.execute( - "INSERT INTO phenotype_resources VALUES" - "(:resource_id, :data_link_id)", - params) - return params - -def unlink_data_from_resource( - conn: db.DbConnection, - resource: Resource, - data_link_id: uuid.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 attach_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) diff --git a/gn_auth/auth/authorisation/resources/phenotypes/__init__.py b/gn_auth/auth/authorisation/resources/phenotypes/__init__.py new file mode 100644 index 0000000..0d4dbfa --- /dev/null +++ b/gn_auth/auth/authorisation/resources/phenotypes/__init__.py @@ -0,0 +1 @@ +"""The phenotypes package.""" diff --git a/gn_auth/auth/authorisation/resources/phenotypes/models.py b/gn_auth/auth/authorisation/resources/phenotypes/models.py new file mode 100644 index 0000000..d4a516a --- /dev/null +++ b/gn_auth/auth/authorisation/resources/phenotypes/models.py @@ -0,0 +1,142 @@ +"""Phenotype data resources functions and utilities.""" +import uuid +from functools import reduce +from typing import Optional, Sequence + +import sqlite3 +from pymonad.maybe import Just, Maybe, Nothing +from pymonad.tools import monad_from_none_or_value + +import gn_auth.auth.db.sqlite3 as db +from gn_auth.auth.authorisation.resources.data import __attach_data__ +from gn_auth.auth.authorisation.resources.base import Resource, resource_from_dbrow + +def resource_data( + cursor: db.DbCursor, + resource_id: uuid.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 link_data_to_resource( + conn: db.DbConnection, + resource: Resource, + data_link_id: uuid.UUID) -> dict: + """Link Phenotype data with a resource.""" + with db.cursor(conn) as cursor: + params = { + "resource_id": str(resource.resource_id), + "data_link_id": str(data_link_id) + } + cursor.execute( + "INSERT INTO phenotype_resources VALUES" + "(:resource_id, :data_link_id)", + params) + return params + +def unlink_data_from_resource( + conn: db.DbConnection, + resource: Resource, + data_link_id: uuid.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 attach_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 individual_linked_resource( + conn: db.DbConnection, + species_id: int, + population_id: int, + dataset_id: int, + xref_id: str) -> Maybe: + """Given the data details, return the linked resource, if one is defined.""" + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT " + "rsc.*, rc.*, lpd.SpeciesId AS species_id, " + "lpd.InbredSetId AS population_id, lpd.PublishXRefId AS xref_id, " + "lpd.dataset_name, lpd.dataset_fullname, lpd.dataset_shortname " + "FROM linked_phenotype_data AS lpd " + "INNER JOIN phenotype_resources AS pr " + "ON lpd.data_link_id=pr.data_link_id " + "INNER JOIN resources AS rsc ON pr.resource_id=rsc.resource_id " + "INNER JOIN resource_categories AS rc " + "ON rsc.resource_category_id=rc.resource_category_id " + "WHERE " + "(lpd.SpeciesId, lpd.InbredSetId, lpd.PublishFreezeId, lpd.PublishXRefId) = " + "(?, ?, ?, ?)", + (species_id, population_id, dataset_id, xref_id)) + return monad_from_none_or_value( + Nothing, Just, cursor.fetchone()).then(resource_from_dbrow) + + +def all_linked_resources( + conn: db.DbConnection, + species_id: int, + population_id: int, + dataset_id: int) -> Maybe: + """Given the data details, return the linked resource, if one is defined.""" + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT rsc.*, rc.resource_category_key, " + "rc.resource_category_description, lpd.SpeciesId AS species_id, " + "lpd.InbredSetId AS population_id, lpd.PublishXRefId AS xref_id, " + "lpd.dataset_name, lpd.dataset_fullname, lpd.dataset_shortname " + "FROM linked_phenotype_data AS lpd " + "INNER JOIN phenotype_resources AS pr " + "ON lpd.data_link_id=pr.data_link_id INNER JOIN resources AS rsc " + "ON pr.resource_id=rsc.resource_id " + "INNER JOIN resource_categories AS rc " + "ON rsc.resource_category_id=rc.resource_category_id " + "WHERE " + "(lpd.SpeciesId, lpd.InbredSetId, lpd.PublishFreezeId) = (?, ?, ?)", + (species_id, population_id, dataset_id)) + + _rscdatakeys = ( + "species_id", "population_id", "xref_id", "dataset_name", + "dataset_fullname", "dataset_shortname") + def __organise__(resources, row): + _rscid = uuid.UUID(row["resource_id"]) + _resource = resources.get(_rscid, resource_from_dbrow(row)) + return { + **resources, + _rscid: Resource( + _resource.resource_id, + _resource.resource_name, + _resource.resource_category, + _resource.public, + _resource.resource_data + ( + {key: row[key] for key in _rscdatakeys},)) + } + results: dict[uuid.UUID, Resource] = reduce( + __organise__, cursor.fetchall(), {}) + if len(results) == 0: + return Nothing + return Just(tuple(results.values())) diff --git a/gn_auth/auth/authorisation/resources/phenotypes/views.py b/gn_auth/auth/authorisation/resources/phenotypes/views.py new file mode 100644 index 0000000..c0a5e81 --- /dev/null +++ b/gn_auth/auth/authorisation/resources/phenotypes/views.py @@ -0,0 +1,77 @@ +"""Views for the phenotype resources.""" +from pymonad.either import Left, Right +from flask import jsonify, Blueprint, current_app as app + +from gn_auth.auth.db import sqlite3 as db +from gn_auth.auth.requests import request_json +from gn_auth.auth.authorisation.resources.request_utils import check_form +from gn_auth.auth.authorisation.roles.models import user_roles_on_resource + +from gn_auth.auth.authentication.oauth2.resource_server import require_oauth + +from .models import all_linked_resources, individual_linked_resource + +phenobp = Blueprint("phenotypes", __name__) + +@phenobp.route("/phenotypes/individual/linked-resource", methods=["POST"]) +@require_oauth("profile group resource") +def get_individual_linked_resource(): + """Get the linked resource for a particular phenotype within the dataset. + + Phenotypes are a tad tricky. Each phenotype could technically be a resource + on its own, and thus a user could have access to only a subset of phenotypes + within the entire dataset.""" + with (require_oauth.acquire("profile group resource") as _token, + db.connection(app.config["AUTH_DB"]) as conn): + return check_form( + request_json(), + "species_id", + "population_id", + "dataset_id", + "xref_id" + ).then( + lambda formdata: individual_linked_resource( + conn, + int(formdata["species_id"]), + int(formdata["population_id"]), + int(formdata["dataset_id"]), + formdata["xref_id"] + ).maybe(Left("No linked resource!"), + lambda lrsc: Right({ + "formdata": formdata, + "resource": lrsc + })) + ).then( + lambda fdlrsc: { + **fdlrsc, + "roles": user_roles_on_resource( + conn, _token.user.user_id, fdlrsc["resource"].resource_id) + } + ).either(lambda error: (jsonify(error), 400), + lambda res: jsonify({ + key: value for key, value in res.items() + if key != "formdata" + })) + + +@phenobp.route("/phenotypes/linked-resources", methods=["POST"]) +@require_oauth("profile group resource") +def get_all_linked_resources(): + """Get all the linked resources for all phenotypes within a dataset. + + See `get_individual_linked_resource(…)` documentation.""" + with (require_oauth.acquire("profile group resource") as _token, + db.connection(app.config["AUTH_DB"]) as conn): + return check_form( + request_json(), + "species_id", + "population_id", + "dataset_id" + ).then( + lambda formdata: all_linked_resources( + conn, + int(formdata["species_id"]), + int(formdata["population_id"]), + int(formdata["dataset_id"])).maybe( + Left("No linked resource!"), Right) + ).either(lambda error: (jsonify(error), 400), jsonify) diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py index 31421f4..1c4104a 100644 --- a/gn_auth/auth/authorisation/resources/views.py +++ b/gn_auth/auth/authorisation/resources/views.py @@ -42,6 +42,7 @@ from gn_auth.auth.authentication.users import User, user_by_id, user_by_email from .checks import authorised_for from .inbredset.views import popbp from .genotypes.views import genobp +from .phenotypes.views import phenobp from .errors import MissingGroupError from .groups.models import Group, user_group from .models import ( @@ -54,6 +55,7 @@ from .models import ( resources = Blueprint("resources", __name__) resources.register_blueprint(popbp, url_prefix="/") resources.register_blueprint(genobp, url_prefix="/") +resources.register_blueprint(phenobp, url_prefix="/") @resources.route("/categories", methods=["GET"]) @require_oauth("profile group resource") @@ -409,9 +411,18 @@ def resource_roles(resource_id: UUID) -> Response: "ON rp.privilege_id=p.privilege_id " "WHERE rr.resource_id=? AND rr.role_created_by=?", (str(resource_id), str(_token.user.user_id))) - results = cursor.fetchall() + user_created = db_rows_to_roles(cursor.fetchall()) - return db_rows_to_roles(results) + cursor.execute( + "SELECT ur.user_id, ur.resource_id, 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 resource_id=? AND user_id=?", + (str(resource_id), str(_token.user.user_id))) + assigned_to_user = db_rows_to_roles(cursor.fetchall()) + + return assigned_to_user + user_created return jsonify(with_db_connection(__roles__)) diff --git a/gn_auth/auth/authorisation/users/admin/ui.py b/gn_auth/auth/authorisation/users/admin/ui.py index 64e79a0..43ca0a2 100644 --- a/gn_auth/auth/authorisation/users/admin/ui.py +++ b/gn_auth/auth/authorisation/users/admin/ui.py @@ -1,6 +1,6 @@ """UI utilities for the auth system.""" from functools import wraps -from flask import flash, url_for, redirect +from flask import flash, request, url_for, redirect from gn_auth.session import logged_in, session_user, clear_session_info from gn_auth.auth.authorisation.resources.system.models import ( @@ -24,5 +24,5 @@ def is_admin(func): flash("Expected a system administrator.", "alert-danger") flash("You have been logged out of the system.", "alert-info") clear_session_info() - return redirect(url_for("oauth2.admin.login")) + return redirect(url_for("oauth2.admin.login", **dict(request.args))) return __admin__ diff --git a/gn_auth/auth/authorisation/users/admin/views.py b/gn_auth/auth/authorisation/users/admin/views.py index 85aeb50..9bc1c36 100644 --- a/gn_auth/auth/authorisation/users/admin/views.py +++ b/gn_auth/auth/authorisation/users/admin/views.py @@ -30,6 +30,7 @@ from ....authentication.oauth2.models.oauth2client import ( save_client, OAuth2Client, oauth2_clients, + update_client_attribute, client as oauth2_client, delete_client as _delete_client) from ....authentication.users import ( @@ -97,7 +98,7 @@ def login(): expires=( datetime.now(tz=timezone.utc) + timedelta(minutes=int( app.config.get("SESSION_EXPIRY_MINUTES", 10))))) - return redirect(url_for(next_uri)) + return redirect(url_for(next_uri, **dict(request.args))) raise NotFoundError(error_message) except NotFoundError as _nfe: flash(error_message, "alert-danger") @@ -196,7 +197,7 @@ def register_client(): if request.method == "GET": return render_template( "admin/register-client.html", - scope=app.config["OAUTH2_SCOPE"], + scope=app.config["OAUTH2_SCOPES_SUPPORTED"], users=with_db_connection(__list_users__), granttypes=_FORM_GRANT_TYPES_, current_user=session.session_user()) @@ -261,7 +262,7 @@ def view_client(client_id: uuid.UUID): return render_template( "admin/view-oauth2-client.html", client=with_db_connection(partial(oauth2_client, client_id=client_id)), - scope=app.config["OAUTH2_SCOPE"], + scope=app.config["OAUTH2_SCOPES_SUPPORTED"], granttypes=_FORM_GRANT_TYPES_) @@ -321,3 +322,37 @@ def delete_client(): "successfully."), "alert-success") return redirect(url_for("oauth2.admin.list_clients")) + + +@admin.route("/clients/<uuid:client_id>/change-secret", methods=["GET", "POST"]) +@is_admin +def change_client_secret(client_id: uuid.UUID): + """Enable changing of a client's secret.""" + def __no_client__(): + # Calling the function causes the flash to be evaluated + # flash("No such client was found!", "alert-danger") + return redirect(url_for("oauth2.admin.list_clients")) + + with db.connection(app.config["AUTH_DB"]) as conn: + if request.method == "GET": + return oauth2_client( + conn, client_id=client_id + ).maybe(__no_client__(), lambda _client: render_template( + "admin/confirm-change-client-secret.html", + client=_client + )) + + _raw = random_string() + return oauth2_client( + conn, client_id=client_id + ).then( + lambda _client: save_client( + conn, + update_client_attribute( + _client, "client_secret", hash_password(_raw))) + ).then( + lambda _client: render_template( + "admin/registered-client.html", + client=_client, + client_secret=_raw) + ).maybe(__no_client__(), lambda resp: resp) diff --git a/gn_auth/auth/authorisation/users/masquerade/models.py b/gn_auth/auth/authorisation/users/masquerade/models.py index 8ac1a68..a155899 100644 --- a/gn_auth/auth/authorisation/users/masquerade/models.py +++ b/gn_auth/auth/authorisation/users/masquerade/models.py @@ -1,5 +1,4 @@ """Functions for handling masquerade.""" -import uuid from functools import wraps from datetime import datetime from authlib.jose import jwt @@ -10,12 +9,16 @@ from flask import current_app as app from gn_auth.auth.errors import ForbiddenAccess from gn_auth.auth.jwks import newest_jwk_with_rotation, jwks_directory +from gn_auth.auth.authentication.oauth2.grants.refresh_token_grant import ( + RefreshTokenGrant) +from gn_auth.auth.authentication.oauth2.models.jwtrefreshtoken import ( + JWTRefreshToken, + save_refresh_token) from ...roles.models import user_roles from ....db import sqlite3 as db from ....authentication.users import User -from ....authentication.oauth2.models.oauth2token import ( - OAuth2Token, save_token) +from ....authentication.oauth2.models.oauth2token import OAuth2Token __FIVE_HOURS__ = (60 * 60 * 5) @@ -53,28 +56,30 @@ def masquerade_as( original_token: OAuth2Token, masqueradee: User) -> OAuth2Token: """Get a token that enables `masquerader` to act as `masqueradee`.""" - token_details = app.config["OAUTH2_SERVER"].generate_token( + scope = original_token.get_scope().replace( + # Do not allow more than one level of masquerading + "masquerade", "").strip() + new_token = app.config["OAUTH2_SERVER"].generate_token( client=original_token.client, - grant_type="authorization_code", + grant_type="urn:ietf:params:oauth:grant-type:jwt-bearer", user=masqueradee, - expires_in=__FIVE_HOURS__, - include_refresh_token=True) - + expires_in=original_token.get_expires_in(), + include_refresh_token=True, + scope=scope) _jwt = jwt.decode( - original_token.access_token, + new_token["access_token"], newest_jwk_with_rotation( jwks_directory(app), int(app.config["JWKS_ROTATION_AGE_DAYS"]))) - new_token = OAuth2Token( - token_id=uuid.UUID(_jwt["jti"]), + save_refresh_token(conn, JWTRefreshToken( + token=new_token["refresh_token"], client=original_token.client, - token_type=token_details["token_type"], - access_token=token_details["access_token"], - refresh_token=token_details.get("refresh_token"), - scope=original_token.scope, + user=masqueradee, + issued_with=_jwt["jti"], + issued_at=datetime.fromtimestamp(_jwt["iat"]), + expires=datetime.fromtimestamp( + int(_jwt["iat"]) + RefreshTokenGrant.DEFAULT_EXPIRES_IN), + scope=scope, revoked=False, - issued_at=datetime.now(), - expires_in=token_details["expires_in"], - user=masqueradee) - save_token(conn, new_token) + parent_of=None)) return new_token diff --git a/gn_auth/auth/authorisation/users/masquerade/views.py b/gn_auth/auth/authorisation/users/masquerade/views.py index 68f19ee..8b897f2 100644 --- a/gn_auth/auth/authorisation/users/masquerade/views.py +++ b/gn_auth/auth/authorisation/users/masquerade/views.py @@ -28,22 +28,17 @@ def masquerade() -> Response: masq_user = with_db_connection(partial( user_by_id, user_id=masqueradee_id)) + def __masq__(conn): new_token = masquerade_as(conn, original_token=token, masqueradee=masq_user) return new_token - def __dump_token__(tok): - return { - key: value for key, value in asdict(tok).items() - if key in ("access_token", "refresh_token", "expires_in", - "token_type") - } + return jsonify({ "original": { - "user": asdict(token.user), - "token": __dump_token__(token) + "user": asdict(token.user) }, "masquerade_as": { "user": asdict(masq_user), - "token": __dump_token__(with_db_connection(__masq__)) + "token": with_db_connection(__masq__) } }) |