From 25c6da03e1639895f0051e8be6502762beefde0b Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 29 May 2023 11:21:48 +0300 Subject: 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 --- gn3/auth/authentication/oauth2/views.py | 9 +-- gn3/auth/authorisation/users/admin/__init__.py | 2 + gn3/auth/authorisation/users/admin/ui.py | 42 ++++++++++++++ gn3/auth/authorisation/users/admin/views.py | 79 ++++++++++++++++++++++++++ gn3/auth/views.py | 2 + gn3/errors.py | 1 + gn3/templates/login.html | 32 +++++++++++ main.py | 5 +- 8 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 gn3/auth/authorisation/users/admin/__init__.py create mode 100644 gn3/auth/authorisation/users/admin/ui.py create mode 100644 gn3/auth/authorisation/users/admin/views.py create mode 100644 gn3/templates/login.html 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/", 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()}} + +

Genenetwork3: Admin Log In

+ +
+ +
+ User Credentials + + + +
+ + +
+ +
+ + +
+
+ + +
+{%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 ##### -- cgit v1.2.3