diff options
Diffstat (limited to 'gn2/wqflask')
-rw-r--r-- | gn2/wqflask/__init__.py | 59 | ||||
-rw-r--r-- | gn2/wqflask/app_errors.py | 1 | ||||
-rw-r--r-- | gn2/wqflask/marker_regression/display_mapping_results.py | 4 | ||||
-rw-r--r-- | gn2/wqflask/oauth2/request_utils.py | 3 | ||||
-rw-r--r-- | gn2/wqflask/oauth2/toplevel.py | 65 |
5 files changed, 102 insertions, 30 deletions
diff --git a/gn2/wqflask/__init__.py b/gn2/wqflask/__init__.py index 1f69c085..4dee4fea 100644 --- a/gn2/wqflask/__init__.py +++ b/gn2/wqflask/__init__.py @@ -1,7 +1,9 @@ """Entry point for flask app""" # pylint: disable=C0413,E0611 import os +import sys import time +import logging import datetime from typing import Tuple from pathlib import Path @@ -10,6 +12,7 @@ from urllib.parse import urljoin, urlparse import redis import jinja2 from flask_session import Session +from authlib.jose import JsonWebKey from authlib.integrations.requests_client import OAuth2Session from flask import g, Flask, flash, session, url_for, redirect, current_app @@ -44,13 +47,6 @@ from gn2.wqflask.startup import ( startup_errors, check_mandatory_configs) -app = Flask(__name__) - - -# See http://flask.pocoo.org/docs/config/#configuring-from-files -# Note no longer use the badly named WQFLASK_OVERRIDES (nyi) -app.config.from_object('gn2.default_settings') -app.config.from_envvar('GN2_SETTINGS') def numcoll(): """Handle possible errors.""" @@ -59,6 +55,52 @@ def numcoll(): except Exception as _exc: return "ERROR" + +def parse_ssl_key(app: Flask, keyconfig: str): + """Parse key file paths into objects""" + keypath = app.config.get(keyconfig, "").strip() + if not bool(keypath): + app.logger.error("Expected configuration '%s'", keyconfig) + return + + with open(keypath) as _sslkey: + app.config[keyconfig] = JsonWebKey.import_key(_sslkey.read()) + + +def dev_loggers(appl: Flask) -> None: + """Default development logging.""" + formatter = logging.Formatter( + fmt="[%(asctime)s] %(levelname)s in %(module)s: %(message)s") + stderr_handler = logging.StreamHandler(stream=sys.stderr) + stderr_handler.setFormatter(formatter) + appl.logger.addHandler(stderr_handler) + + root_logger = logging.getLogger() + root_logger.addHandler(stderr_handler) + root_logger.setLevel(appl.config.get("LOGLEVEL", "WARNING")) + + +def gunicorn_loggers(appl: Flask) -> None: + """Logging with gunicorn WSGI server.""" + logger = logging.getLogger("gunicorn.error") + appl.logger.handlers = logger.handlers + appl.logger.setLevel(logger.level) + +def setup_logging(appl: Flask) -> None: + """Setup appropriate logging""" + software, *_version_and_comments = os.environ.get( + "SERVER_SOFTWARE", "").split('/') + gunicorn_loggers(app) if software == "gunicorn" else dev_loggers(app) + + +app = Flask(__name__) +setup_logging(app) + +# See http://flask.pocoo.org/docs/config/#configuring-from-files +# Note no longer use the badly named WQFLASK_OVERRIDES (nyi) +app.config.from_object('gn2.default_settings') +app.config.from_envvar('GN2_SETTINGS') + app.jinja_env.globals.update( undefined=jinja2.StrictUndefined, numify=formatting.numify, @@ -107,6 +149,9 @@ except StartupError as serr: server_session = Session(app) +parse_ssl_key(app, "SSL_PRIVATE_KEY") +parse_ssl_key(app, "AUTH_SERVER_SSL_PUBLIC_KEY") + @app.before_request def before_request(): g.request_start_time = time.time() diff --git a/gn2/wqflask/app_errors.py b/gn2/wqflask/app_errors.py index b0e3b665..503f4e1c 100644 --- a/gn2/wqflask/app_errors.py +++ b/gn2/wqflask/app_errors.py @@ -3,6 +3,7 @@ import os import random import datetime import traceback +from uuid import uuid4 from werkzeug.exceptions import InternalServerError from authlib.integrations.base_client.errors import InvalidTokenError diff --git a/gn2/wqflask/marker_regression/display_mapping_results.py b/gn2/wqflask/marker_regression/display_mapping_results.py index 0f1df3ec..dec414c0 100644 --- a/gn2/wqflask/marker_regression/display_mapping_results.py +++ b/gn2/wqflask/marker_regression/display_mapping_results.py @@ -214,8 +214,8 @@ class DisplayMappingResults: # BEGIN HaplotypeAnalyst HAPLOTYPE_POSITIVE = BLUE - HAPLOTYPE_NEGATIVE = YELLOW - HAPLOTYPE_HETEROZYGOUS = GREEN + HAPLOTYPE_NEGATIVE = RED + HAPLOTYPE_HETEROZYGOUS = ORANGE HAPLOTYPE_RECOMBINATION = DARKGRAY # END HaplotypeAnalyst diff --git a/gn2/wqflask/oauth2/request_utils.py b/gn2/wqflask/oauth2/request_utils.py index a453d18e..1cdc465f 100644 --- a/gn2/wqflask/oauth2/request_utils.py +++ b/gn2/wqflask/oauth2/request_utils.py @@ -35,7 +35,8 @@ def process_error(error: Response, if error.status_code in range(400, 500): try: err = error.json() - msg = err.get("error_description", f"{error.reason}") + msg = err.get( + "error", err.get("error_description", f"{error.reason}")) except simplejson.errors.JSONDecodeError as _jde: msg = message return { diff --git a/gn2/wqflask/oauth2/toplevel.py b/gn2/wqflask/oauth2/toplevel.py index dffc0a7c..23965cc1 100644 --- a/gn2/wqflask/oauth2/toplevel.py +++ b/gn2/wqflask/oauth2/toplevel.py @@ -1,14 +1,18 @@ """Authentication endpoints.""" -from uuid import UUID +import uuid +import datetime from urllib.parse import urljoin, urlparse, urlunparse + +from authlib.jose import jwt from flask import ( flash, request, Blueprint, url_for, redirect, render_template, current_app as app) from . import session -from .client import SCOPE, no_token_post, user_logged_in from .checks import require_oauth2 from .request_utils import user_details, process_error +from .client import ( + SCOPE, no_token_post, user_logged_in, authserver_uri, oauth2_clientid) toplevel = Blueprint("toplevel", __name__) @@ -18,38 +22,59 @@ def register_client(): """Register an OAuth2 client.""" return "USER IS LOGGED IN AND SUCCESSFULLY ACCESSED THIS ENDPOINT!" + @toplevel.route("/code", methods=["GET"]) def authorisation_code(): """Use authorisation code to get token.""" - def __error__(error): - flash(f"{error['error']}: {error['error_description']}", - "alert-danger") - return redirect("/") - - def __success__(token): - session.set_user_token(token) - udets = user_details() - session.set_user_details({ - "user_id": UUID(udets["user_id"]), - "name": udets["name"], - "email": udets["email"], - "token": session.user_token(), - "logged_in": True - }) - return redirect("/") - code = request.args.get("code", "") if bool(code): base_url = urlparse(request.base_url, scheme=request.scheme) + jwtkey = app.config["SSL_PRIVATE_KEY"] + issued = datetime.datetime.now() request_data = { - "grant_type": "authorization_code", + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "code": code, "scope": SCOPE, "redirect_uri": urljoin( urlunparse(base_url), url_for("oauth2.toplevel.authorisation_code")), + "assertion": jwt.encode( + header={ + "alg": "RS256", + "typ": "jwt", + "kid": jwtkey.as_dict()["kid"]}, + payload={ + "iss": str(oauth2_clientid()), + "sub": request.args["user_id"], + "aud": urljoin(authserver_uri(), "auth/token"), + "exp": (issued + datetime.timedelta(minutes=5)), + "nbf": int(issued.timestamp()), + "iat": int(issued.timestamp()), + "jti": str(uuid.uuid4())}, + key=jwtkey).decode("utf8"), "client_id": app.config["OAUTH2_CLIENT_ID"] } + + def __error__(error): + flash(f"{error['error']}: {error['error_description']}", + "alert-danger") + app.logger.debug("Request error (%s) %s: %s", + error["status_code"], error["error_description"], + request_data) + return redirect("/") + + def __success__(token): + session.set_user_token(token) + udets = user_details() + session.set_user_details({ + "user_id": uuid.UUID(udets["user_id"]), + "name": udets["name"], + "email": udets["email"], + "token": session.user_token(), + "logged_in": True + }) + return redirect("/") + return no_token_post( "auth/token", data=request_data).either( lambda err: __error__(process_error(err)), __success__) |