about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2023-05-29 11:21:48 +0300
committerFrederick Muriuki Muriithi2023-05-29 11:21:48 +0300
commit25c6da03e1639895f0051e8be6502762beefde0b (patch)
tree3355d9538c550753b6b5f45f37446c0f1e584011
parenta448329fac4ca3c3c42dd180e3bc823024bbd25c (diff)
downloadgenenetwork3-25c6da03e1639895f0051e8be6502762beefde0b.tar.gz
Enable Administrator login on GN3
* gn3/auth/authentication/oauth2/views.py: Remove endpoint
* gn3/auth/authorisation/users/admin/__init__.py: New admin module
* gn3/auth/authorisation/users/admin/ui.py: New admin module
* gn3/auth/authorisation/users/admin/views.py: New admin module
* gn3/auth/views.py: Use new admin module
* gn3/errors.py: Fix linting errors
* gn3/templates/login.html: New html template
* main.py: Fix linting errors
-rw-r--r--gn3/auth/authentication/oauth2/views.py9
-rw-r--r--gn3/auth/authorisation/users/admin/__init__.py2
-rw-r--r--gn3/auth/authorisation/users/admin/ui.py42
-rw-r--r--gn3/auth/authorisation/users/admin/views.py79
-rw-r--r--gn3/auth/views.py2
-rw-r--r--gn3/errors.py1
-rw-r--r--gn3/templates/login.html32
-rw-r--r--main.py5
8 files changed, 163 insertions, 9 deletions
diff --git a/gn3/auth/authentication/oauth2/views.py b/gn3/auth/authentication/oauth2/views.py
index f281295..7ce45fd 100644
--- a/gn3/auth/authentication/oauth2/views.py
+++ b/gn3/auth/authentication/oauth2/views.py
@@ -26,11 +26,6 @@ from ..users import valid_login, NotFoundError, user_by_email
 
 auth = Blueprint("auth", __name__)
 
-@auth.route("/register-client", methods=["GET", "POST"])
-def register_client():
-    """Register an OAuth2 client."""
-    return "WOULD REGISTER ..."
-
 @auth.route("/delete-client/<uuid:client_id>", methods=["GET", "POST"])
 def delete_client(client_id: uuid.UUID):
     """Delete an OAuth2 client."""
@@ -77,8 +72,8 @@ def authorise():
 
         return with_db_connection(__authorise__)
     except InvalidClientError as ice:
-            return render_template(
-                "oauth2/oauth2_error.html", error=ice), ice.status_code
+        return render_template(
+            "oauth2/oauth2_error.html", error=ice), ice.status_code
 
 @auth.route("/token", methods=["POST"])
 def token():
diff --git a/gn3/auth/authorisation/users/admin/__init__.py b/gn3/auth/authorisation/users/admin/__init__.py
new file mode 100644
index 0000000..8aa0743
--- /dev/null
+++ b/gn3/auth/authorisation/users/admin/__init__.py
@@ -0,0 +1,2 @@
+"""The admin module"""
+from .views import admin
diff --git a/gn3/auth/authorisation/users/admin/ui.py b/gn3/auth/authorisation/users/admin/ui.py
new file mode 100644
index 0000000..3918eb1
--- /dev/null
+++ b/gn3/auth/authorisation/users/admin/ui.py
@@ -0,0 +1,42 @@
+"""UI utilities for the auth system."""
+from functools import wraps
+from datetime import datetime, timezone
+from flask import flash, session, request, url_for, redirect
+
+from gn3.auth.authentication.users import User
+from gn3.auth.db_utils import with_db_connection
+from gn3.auth.authorisation.roles.models import user_roles
+
+SESSION_KEY = "session_details"
+
+def __session_expired__():
+    """Check whether the session has expired."""
+    return datetime.now(tz=timezone.utc) >= session[SESSION_KEY]["expires"]
+
+def logged_in(func):
+    """Verify the user is logged in."""
+    @wraps(func)
+    def __logged_in__(*args, **kwargs):
+        if bool(session.get(SESSION_KEY)) and not __session_expired__():
+            return func(*args, **kwargs)
+        flash("You need to be logged in to access that page.", "alert-danger")
+        return redirect(url_for("oauth2.admin.login", next=request.url))
+    return __logged_in__
+
+def is_admin(func):
+    """Verify user is a system admin."""
+    @wraps(func)
+    @logged_in
+    def __admin__(*args, **kwargs):
+        admin_roles = [
+            role for role in with_db_connection(
+                lambda conn: user_roles(
+                    conn, User(**session[SESSION_KEY]["user"])))
+            if role.role_name == "system-administrator"]
+        if len(admin_roles) > 0:
+            return func(*args, **kwargs)
+        flash("Expected a system administrator.", "alert-danger")
+        flash("You have been logged out of the system.", "alert-info")
+        session.pop(SESSION_KEY)
+        return redirect(url_for("oauth2.admin.login"))
+    return __admin__
diff --git a/gn3/auth/authorisation/users/admin/views.py b/gn3/auth/authorisation/users/admin/views.py
new file mode 100644
index 0000000..6a86da3
--- /dev/null
+++ b/gn3/auth/authorisation/users/admin/views.py
@@ -0,0 +1,79 @@
+"""UI for admin stuff"""
+from datetime import datetime, timezone, timedelta
+
+from email_validator import validate_email, EmailNotValidError
+from flask import (
+    flash,
+    session,
+    request,
+    url_for,
+    redirect,
+    Blueprint,
+    current_app,
+    render_template)
+
+from gn3.auth import db
+from gn3.auth.authentication.users import valid_login, user_by_email
+
+from .ui import SESSION_KEY, is_admin
+
+admin = Blueprint("admin", __name__)
+
+@admin.before_request
+def update_expires():
+    """Update session expiration."""
+    if bool(session.get(SESSION_KEY)):
+        now = datetime.now(tz=timezone.utc)
+        if now >= session[SESSION_KEY]["expires"]:
+            flash("Session has expired. Logging out...", "alert-warning")
+            session.pop(SESSION_KEY)
+            return redirect("/version")
+        # If not expired, extend expiry.
+        session[SESSION_KEY]["expires"] = now + timedelta(minutes=10)
+
+    return None
+
+@admin.route("/login", methods=["GET", "POST"])
+def login():
+    """Log in to GN3 directly without OAuth2 client."""
+    if request.method == "GET":
+        return render_template(
+            "login.html", next_uri=request.args.get("next", "/api/version"))
+
+    form = request.form
+    next_uri = form.get("next_uri", "/api/version")
+    error_message = "Invalid email or password provided."
+    login_page = redirect(url_for("oauth2.admin.login", next=next_uri))
+    try:
+        email = validate_email(form.get("email", "").strip(),
+                               check_deliverability=False)
+        password = form.get("password")
+        with db.connection(current_app.config["AUTH_DB"]) as conn:
+            user = user_by_email(conn, email["email"])
+            if valid_login(conn, user, password):
+                session[SESSION_KEY] = {
+                    "user": user._asdict(),
+                    "expires": datetime.now(tz=timezone.utc) + timedelta(minutes=10)
+                }
+                return redirect(next_uri)
+            flash(error_message, "alert-danger")
+            return login_page
+    except EmailNotValidError as _enve:
+        flash(error_message, "alert-danger")
+        return login_page
+
+@admin.route("/logout", methods=["GET"])
+def logout():
+    """Log out the admin."""
+    if not bool(session.get(SESSION_KEY)):
+        flash("Not logged in.", "alert-info")
+        return redirect(url_for("general.version"))
+    session.pop(SESSION_KEY)
+    flash("Logged out", "alert-success")
+    return redirect(url_for("general.version"))
+
+@admin.route("/register-client", methods=["GET", "POST"])
+@is_admin
+def register_client():
+    """Register an OAuth2 client."""
+    return "WOULD REGISTER ..."
diff --git a/gn3/auth/views.py b/gn3/auth/views.py
index 4e01cc9..56eace7 100644
--- a/gn3/auth/views.py
+++ b/gn3/auth/views.py
@@ -5,6 +5,7 @@ from .authentication.oauth2.views import auth
 
 from .authorisation.data.views import data
 from .authorisation.users.views import users
+from .authorisation.users.admin import admin
 from .authorisation.roles.views import roles
 from .authorisation.groups.views import groups
 from .authorisation.resources.views import resources
@@ -15,5 +16,6 @@ oauth2.register_blueprint(auth, url_prefix="/")
 oauth2.register_blueprint(data, url_prefix="/data")
 oauth2.register_blueprint(users, url_prefix="/user")
 oauth2.register_blueprint(roles, url_prefix="/role")
+oauth2.register_blueprint(admin, url_prefix="/admin")
 oauth2.register_blueprint(groups, url_prefix="/group")
 oauth2.register_blueprint(resources, url_prefix="/resource")
diff --git a/gn3/errors.py b/gn3/errors.py
index 7ae07f4..606b3d3 100644
--- a/gn3/errors.py
+++ b/gn3/errors.py
@@ -13,6 +13,7 @@ def handle_authorisation_error(exc: AuthorisationError):
     }), exc.error_code
 
 def handle_oauth2_errors(exc: OAuth2Error):
+    """Handle OAuth2Error if not handled anywhere else."""
     current_app.logger.error(exc)
     return jsonify({
         "error": exc.error,
diff --git a/gn3/templates/login.html b/gn3/templates/login.html
new file mode 100644
index 0000000..cf46009
--- /dev/null
+++ b/gn3/templates/login.html
@@ -0,0 +1,32 @@
+{%extends "base.html"%}
+
+{%block title%}Log in to Genenetwork3{%endblock%}
+
+{%block content%}
+{{flash_messages()}}
+
+<h1>Genenetwork3: Admin Log In</h1>
+
+<form method="POST" action="{{url_for('oauth2.admin.login')}}">
+
+  <fieldset>
+    <legend>User Credentials</legend>
+
+    <input name="next_uri" type="hidden" value={{next_uri}}>
+
+    <fieldset>
+      <label for="txt:email">Email Address</label>
+      <input name="email" type="email" id="txt:email" required="required"
+	     placeholder="your@email.address" />
+    </fieldset>
+
+    <fieldset>
+      <label for="txt:password">Password</label>
+      <input name="password" type="password" id="txt:password"
+	     required="required" />
+    </fieldset>
+  </fieldset>
+  
+  <input type="submit" value="log in" />
+</form>
+{%endblock%}
diff --git a/main.py b/main.py
index b6b323c..9864240 100644
--- a/main.py
+++ b/main.py
@@ -15,6 +15,8 @@ from gn3.auth.authentication.users import hash_password
 
 from gn3.auth import db
 
+from scripts import migrate_existing_data as med# type: ignore[import]
+
 app = create_app()
 
 ##### BEGIN: CLI Commands #####
@@ -111,8 +113,7 @@ def assign_system_admin(user_id: uuid.UUID):
 @app.cli.command()
 def make_data_public():
     """Make existing data that is not assigned to any group publicly visible."""
-    from scripts.migrate_existing_data import entry
-    entry(app.config["AUTH_DB"], app.config["SQL_URI"])
+    med.entry(app.config["AUTH_DB"], app.config["SQL_URI"])
 
 ##### END: CLI Commands #####