diff options
-rw-r--r-- | wqflask/wqflask/__init__.py | 4 | ||||
-rw-r--r-- | wqflask/wqflask/app_errors.py | 11 | ||||
-rw-r--r-- | wqflask/wqflask/decorators.py | 56 | ||||
-rw-r--r-- | wqflask/wqflask/metadata_edits.py | 21 | ||||
-rw-r--r-- | wqflask/wqflask/templates/authorisation_error.html | 19 | ||||
-rw-r--r-- | wqflask/wqflask/templates/show_trait_details.html | 4 |
6 files changed, 97 insertions, 18 deletions
diff --git a/wqflask/wqflask/__init__.py b/wqflask/wqflask/__init__.py index 13685324..140e94ad 100644 --- a/wqflask/wqflask/__init__.py +++ b/wqflask/wqflask/__init__.py @@ -78,6 +78,10 @@ app.register_blueprint(group_management, url_prefix="/group-management") app.register_blueprint(jobs_bp, url_prefix="/jobs") app.register_blueprint(oauth2, url_prefix="/oauth2") +from wqflask.decorators import AuthorisationError +from wqflask.app_errors import handle_authorisation_error +app.register_error_handler(AuthorisationError, handle_authorisation_error) + server_session = Session(app) @app.before_request diff --git a/wqflask/wqflask/app_errors.py b/wqflask/wqflask/app_errors.py new file mode 100644 index 00000000..18b29841 --- /dev/null +++ b/wqflask/wqflask/app_errors.py @@ -0,0 +1,11 @@ +"""Handle errors at the application's top-level""" + +from flask import current_app, render_template + +from wqflask.decorators import AuthorisationError + +def handle_authorisation_error(exc: AuthorisationError): + """Handle AuthorisationError if not handled anywhere else.""" + current_app.logger.error(exc) + return render_template( + "authorisation_error.html", error_type=type(exc).__name__, error=exc) diff --git a/wqflask/wqflask/decorators.py b/wqflask/wqflask/decorators.py index 41d23084..b281e9bd 100644 --- a/wqflask/wqflask/decorators.py +++ b/wqflask/wqflask/decorators.py @@ -1,15 +1,19 @@ """This module contains gn2 decorators""" +import json +import requests +from functools import wraps +from urllib.parse import urljoin +from typing import Dict, Callable + import redis +from flask import g, flash, request, url_for, redirect, current_app -from flask import current_app, g, redirect, request, url_for -from typing import Dict -from urllib.parse import urljoin -from functools import wraps from gn3.authentication import AdminRole from gn3.authentication import DataRole -import json -import requests +from wqflask.oauth2 import client +from wqflask.oauth2.session import session_info +from wqflask.oauth2.request_utils import process_error def login_required(f): @@ -78,3 +82,43 @@ def edit_admins_access_required(f): return redirect(url_for("no_access_page")) return f(*args, **kwargs) return wrap + +class AuthorisationError(Exception): + """Raise when there is an authorisation issue.""" + def __init__(self, description, user): + self.description = description + self.user = user + super().__init__(self, description, user) + +def required_access(access_levels: tuple[str, ...], + dataset_key: str = "dataset_name", + trait_key: str = "name") -> Callable: + def __build_access_checker__(func: Callable): + @wraps(func) + def __checker__(*args, **kwargs): + def __error__(err): + error = process_error(err) + raise AuthorisationError( + f"{error['error']}: {error['error_description']}", + session_info()["user"]) + + def __success__(priv_info): + if all(priv in priv_info[0]["privileges"] for priv in access_levels): + return func(*args, **kwargs) + missing = tuple(f"'{priv}'" for priv in access_levels + if priv not in priv_info[0]["privileges"]) + raise AuthorisationError( + f"Missing privileges: {', '.join(missing)}", + session_info()["user"]) + dataset_name = kwargs.get( + dataset_key, + request.args.get(dataset_key, request.form.get(dataset_key, ""))) + trait_name = kwargs.get( + trait_key, + request.args.get(trait_key, request.form.get(trait_key, ""))) + return client.post( + "oauth2/data/authorisation", + json={"traits": [f"{dataset_name}::{trait_name}"]}).either( + __error__, __success__) + return __checker__ + return __build_access_checker__ diff --git a/wqflask/wqflask/metadata_edits.py b/wqflask/wqflask/metadata_edits.py index 64fc3f19..df52fdc6 100644 --- a/wqflask/wqflask/metadata_edits.py +++ b/wqflask/wqflask/metadata_edits.py @@ -20,9 +20,9 @@ from flask import request from flask import url_for from wqflask.database import database_connection -from wqflask.decorators import edit_access_required -from wqflask.decorators import edit_admins_access_required from wqflask.decorators import login_required +from wqflask.decorators import required_access +from wqflask.decorators import edit_admins_access_required from gn3.authentication import AdminRole from gn3.authentication import get_highest_user_access_role @@ -126,8 +126,8 @@ def edit_probeset(conn, name): @metadata_edit.route("/<dataset_id>/traits/<name>") -@edit_access_required -@login_required +@required_access( + ("group:resource:view-resource", "group:resource:edit-resource")) def display_phenotype_metadata(dataset_id: str, name: str): with database_connection() as conn: _d = edit_phenotype(conn=conn, name=name, dataset_id=dataset_id) @@ -144,8 +144,8 @@ def display_phenotype_metadata(dataset_id: str, name: str): @metadata_edit.route("/traits/<name>") -@edit_access_required -@login_required +@required_access( + ("group:resource:view-resource", "group:resource:edit-resource")) def display_probeset_metadata(name: str): with database_connection() as conn: _d = edit_probeset(conn=conn, name=name) @@ -160,8 +160,8 @@ def display_probeset_metadata(name: str): @metadata_edit.route("/<dataset_id>/traits/<name>", methods=("POST",)) -@edit_access_required -@login_required +@required_access( + ("group:resource:view-resource", "group:resource:edit-resource")) def update_phenotype(dataset_id: str, name: str): data_ = request.form.to_dict() TMPDIR = current_app.config.get("TMPDIR") @@ -369,8 +369,9 @@ View the diffs <a href='{url}' target='_blank'>here</a>", "success") @metadata_edit.route("/traits/<name>", methods=("POST",)) -@edit_access_required -@login_required +@required_access( + ("group:resource:view-resource", "group:resource:edit-resource"), + dataset_key="dataset_id", trait_key="name") def update_probeset(name: str): with database_connection() as conn: data_ = request.form.to_dict() diff --git a/wqflask/wqflask/templates/authorisation_error.html b/wqflask/wqflask/templates/authorisation_error.html new file mode 100644 index 00000000..3dce8b52 --- /dev/null +++ b/wqflask/wqflask/templates/authorisation_error.html @@ -0,0 +1,19 @@ +{%extends "base.html"%} +{%block title%}{{error_type}}: ...{%endblock%} +{%block content%} +<div class="container"> + <div class="page-header"> + <h3>Access Error: {{error_type}}</h3> + </div> + <p> + <span class="glyphicon glyphicon-exclamation-sign text-danger"></span> + <strong>{{error_type}}:</strong> + <small class="text-danger">{{error.description}}</small> + </p> + <p> + Please contact the data's owner or GN administrators if you believe you + should have access to these data. + </p> +</div> + +{%endblock%} diff --git a/wqflask/wqflask/templates/show_trait_details.html b/wqflask/wqflask/templates/show_trait_details.html index cce76082..cd8671bb 100644 --- a/wqflask/wqflask/templates/show_trait_details.html +++ b/wqflask/wqflask/templates/show_trait_details.html @@ -237,11 +237,11 @@ <button type="button" id="view_in_gn1" class="btn btn-primary" title="View Trait in GN1" onclick="window.open('http://gn1.genenetwork.org/webqtl/main.py?cmd=show&db={{ this_trait.dataset.name }}&probeset={{ this_trait.name }}', '_blank')">Go to GN1</button> {%if "group:resource:edit-resource" in trait_privileges%} {% if this_trait.dataset.type == 'Publish' %} - <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('/datasets/{{ this_trait.dataset.group.id }}/traits/{{ this_trait.name }}?resource-id={{ resource_id }}', '_blank')">Edit</button> + <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('/datasets/{{ this_trait.dataset.group.id }}/traits/{{ this_trait.name }}?resource-id={{ resource_id }}&dataset_name={{this_trait.dataset.name}}', '_blank')">Edit</button> {% endif %} {% if this_trait.dataset.type == 'ProbeSet' %} - <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('/datasets/traits/{{ this_trait.name }}?resource-id={{ resource_id }}', '_blank')">Edit</button> + <button type="button" id="edit_resource" class="btn btn-success" title="Edit Resource" onclick="window.open('/datasets/traits/{{ this_trait.name }}?resource-id={{ resource_id }}&dataset_name={{this_trait.dataset.name}}', '_blank')">Edit</button> {% endif %} {% endif %} </div> |