about summary refs log tree commit diff
diff options
context:
space:
mode:
-rwxr-xr-xbin/genenetwork24
-rw-r--r--gn2/wqflask/__init__.py59
-rw-r--r--gn2/wqflask/app_errors.py1
-rw-r--r--gn2/wqflask/marker_regression/display_mapping_results.py4
-rw-r--r--gn2/wqflask/oauth2/request_utils.py3
-rw-r--r--gn2/wqflask/oauth2/toplevel.py65
6 files changed, 104 insertions, 32 deletions
diff --git a/bin/genenetwork2 b/bin/genenetwork2
index da35d049..fc14eedc 100755
--- a/bin/genenetwork2
+++ b/bin/genenetwork2
@@ -186,7 +186,7 @@ fi
 if [ "$1" = '-gunicorn-dev' ] ; then
     echo PYTHONPATH="${PYTHONPATH}"
     if [ -z "${SERVER_PORT}" ]; then echo "ERROR: Provide a SERVER_PORT" ; exit 1 ; fi
-    cmd="--bind 0.0.0.0:$SERVER_PORT --workers=1 --timeout 180 --reload gn2.run_gunicorn:app"
+    cmd="--bind 0.0.0.0:$SERVER_PORT --workers=1 --timeout 180 --reload ${GUNICORN_EXTRA_ARGS} gn2.run_gunicorn:app"
     echo "RUNNING gunicorn ${cmd}"
     gunicorn $cmd
     exit $?
@@ -195,7 +195,7 @@ if [ "$1" = '-gunicorn-prod' ] ; then
     echo PYTHONPATH="${PYTHONPATH}"
     if [ -z "${SERVER_PORT}" ]; then echo "ERROR: Provide a SERVER_PORT" ; exit 1 ; fi
     PID=$TMPDIR/gunicorn.$USER.pid
-    cmd="--bind 0.0.0.0:$SERVER_PORT --pid $PID --workers 20 --keep-alive 6000 --max-requests 100 --max-requests-jitter 30 --timeout 1200 gn2.wsgi"
+    cmd="--bind 0.0.0.0:$SERVER_PORT --pid $PID --workers 20 --keep-alive 6000 --max-requests 100 --max-requests-jitter 30 --timeout 1200 ${GUNICORN_EXTRA_ARGS} gn2.wsgi"
     echo "RUNNING gunicorn ${cmd}"
     gunicorn $cmd
     exit $?
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__)