diff options
author | Frederick Muriuki Muriithi | 2023-05-12 11:57:12 +0300 |
---|---|---|
committer | Frederick Muriuki Muriithi | 2023-05-12 11:57:12 +0300 |
commit | a175ea7ea3b0c85ca4e9c0909833f6842474e225 (patch) | |
tree | db6c123723328e81a47f107cf44e82e9800d53ba | |
parent | 96fb589371dd1700f0d0f7abc367098d6820c37a (diff) | |
download | genenetwork2-a175ea7ea3b0c85ca4e9c0909833f6842474e225.tar.gz |
auth: Integrate sessions with auth
Rework the sessions to do what was handled by the soon-to-be-obsolete
`wqflask.user_sessions` module.
This is necessary in order to retain the expected functionality of the
user collections, especially:
* anonymous user collections
* authenticated user collections
* import of anonymous collections when user authenticates
-rw-r--r-- | wqflask/wqflask/oauth2/checks.py | 28 | ||||
-rw-r--r-- | wqflask/wqflask/oauth2/client.py | 76 | ||||
-rw-r--r-- | wqflask/wqflask/oauth2/request_utils.py | 2 | ||||
-rw-r--r-- | wqflask/wqflask/oauth2/session.py | 74 | ||||
-rw-r--r-- | wqflask/wqflask/oauth2/toplevel.py | 15 | ||||
-rw-r--r-- | wqflask/wqflask/oauth2/ui.py | 7 | ||||
-rw-r--r-- | wqflask/wqflask/oauth2/users.py | 23 |
7 files changed, 170 insertions, 55 deletions
diff --git a/wqflask/wqflask/oauth2/checks.py b/wqflask/wqflask/oauth2/checks.py index c60ab1de..3b6d2471 100644 --- a/wqflask/wqflask/oauth2/checks.py +++ b/wqflask/wqflask/oauth2/checks.py @@ -3,33 +3,41 @@ from functools import wraps from urllib.parse import urljoin from authlib.integrations.requests_client import OAuth2Session -from flask import flash, request, session, url_for, redirect, current_app +from flask import ( + flash, request, url_for, redirect, current_app, session as flask_session) + +from . import session def user_logged_in(): """Check whether the user has logged in.""" - return bool(session.get("oauth2_token", False)) + suser = session.session_info()["user"] + return suser["token"].is_right() and suser["logged_in"] def require_oauth2(func): """Decorator for ensuring user is logged in.""" @wraps(func) def __token_valid__(*args, **kwargs): """Check that the user is logged in and their token is valid.""" - if user_logged_in(): - config = current_app.config + config = current_app.config + def __clear_session__(_no_token): + session.clear_session_info() + flask_session.pop("oauth2_token", None) + flask_session.pop("user_details", None) + flash("You need to be logged in.", "alert-warning") + return redirect("/") + + def __with_token__(token): client = OAuth2Session( config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], - token=session["oauth2_token"]) + token=token) resp = client.get( urljoin(config["GN_SERVER_URL"], "oauth2/user")) user_details = resp.json() if not user_details.get("error", False): return func(*args, **kwargs) - session.pop("oauth2_token", None) - session.pop("user_details", None) + return clear_session_info(token) - flash("You need to be logged in.", "alert-warning") - # return redirect(url_for("oauth2.user.login", next=request.endpoint)) - return redirect("/") + return session.user_token().either(__clear_session__, __with_token__) return __token_valid__ diff --git a/wqflask/wqflask/oauth2/client.py b/wqflask/wqflask/oauth2/client.py index efa862f2..249d158d 100644 --- a/wqflask/wqflask/oauth2/client.py +++ b/wqflask/wqflask/oauth2/client.py @@ -3,50 +3,70 @@ import requests from typing import Any, Optional from urllib.parse import urljoin -from flask import session, current_app as app +from flask import jsonify, current_app as app from pymonad.maybe import Just, Maybe, Nothing from pymonad.either import Left, Right, Either from authlib.integrations.requests_client import OAuth2Session +from wqflask.oauth2 import session + SCOPE = "profile group role resource register-client user introspect migrate-data" def oauth2_client(): config = app.config - return OAuth2Session( - config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], - scope=SCOPE, token_endpoint_auth_method="client_secret_post", - token=session.get("oauth2_token")) + def __client__(token) -> OAuth2Session: + return OAuth2Session( + config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], + scope=SCOPE, token_endpoint_auth_method="client_secret_post", + token=token) + return session.user_token().either( + lambda _notok: __client__(None), + lambda token: __client__(token)) + +def __no_token__(_err) -> Left: + """Handle situation where request is attempted with no token.""" + resp = requests.models.Response() + resp._content = json.dumps({ + "error": "AuthenticationError", + "error-description": ("You need to authenticate to access requested " + "information.")}).encode("utf-8") + resp.status_code = 400 + return Left(resp) def oauth2_get(uri_path: str, data: dict = {}, **kwargs) -> Either: - token = session.get("oauth2_token") - config = app.config - client = OAuth2Session( - config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], - token=token, scope=SCOPE) - resp = client.get( - urljoin(config["GN_SERVER_URL"], uri_path), - data=data, - **kwargs) - if resp.status_code == 200: - return Right(resp.json()) + def __get__(token) -> Either: + config = app.config + client = OAuth2Session( + config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], + token=token, scope=SCOPE) + resp = client.get( + urljoin(config["GN_SERVER_URL"], uri_path), + data=data, + **kwargs) + if resp.status_code == 200: + return Right(resp.json()) - return Left(resp) + return Left(resp) + + return session.user_token().either(__no_token__, __get__) def oauth2_post( uri_path: str, data: Optional[dict] = None, json: Optional[dict] = None, **kwargs) -> Either: - token = session.get("oauth2_token") - config = app.config - client = OAuth2Session( - config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], - token=token, scope=SCOPE) - resp = client.post( - urljoin(config["GN_SERVER_URL"], uri_path), data=data, json=json, - **kwargs) - if resp.status_code == 200: - return Right(resp.json()) + def __post__(token) -> Either: + config = app.config + client = OAuth2Session( + config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"], + token=token, scope=SCOPE) + resp = client.post( + urljoin(config["GN_SERVER_URL"], uri_path), data=data, json=json, + **kwargs) + if resp.status_code == 200: + return Right(resp.json()) - return Left(resp) + return Left(resp) + + return session.user_token().either(__no_token__, __post__) def no_token_get(uri_path: str, **kwargs) -> Either: config = app.config diff --git a/wqflask/wqflask/oauth2/request_utils.py b/wqflask/wqflask/oauth2/request_utils.py index ac21e223..ef8ee9fd 100644 --- a/wqflask/wqflask/oauth2/request_utils.py +++ b/wqflask/wqflask/oauth2/request_utils.py @@ -23,7 +23,7 @@ def raise_unimplemented(): def user_details(): return oauth2_get("oauth2/user").either( - handle_error("oauth2.login"), + lambda err: {}, lambda usr_dets: usr_dets) def process_error(error: Response, diff --git a/wqflask/wqflask/oauth2/session.py b/wqflask/wqflask/oauth2/session.py new file mode 100644 index 00000000..011d95f3 --- /dev/null +++ b/wqflask/wqflask/oauth2/session.py @@ -0,0 +1,74 @@ +"""Deal with user sessions""" +from uuid import UUID, uuid4 +from typing import Any, TypedDict + +from flask import request, session +from pymonad.either import Left, Right, Either + +class UserDetails(TypedDict): + """Session information relating specifically to the user.""" + user_id: UUID + name: str + token: Either + logged_in: bool + +class SessionInfo(TypedDict): + """All Session information we save.""" + session_id: UUID + user: UserDetails + anon_id: UUID + user_agent: str + ip_addr: str + +__SESSION_KEY__ = "session_info" # Do not use this outside this module!! + +def clear_session_info(): + """Clears the session.""" + session.pop(__SESSION_KEY__) + +def save_session_info(sess_info: SessionInfo) -> SessionInfo: + """Save `session_info`.""" + # TODO: if it is an existing session, verify that certain important security + # bits have not changed before saving. + # old_session_info = session.get(__SESSION_KEY__) + # if bool(old_session_info): + # if old_session_info["user_agent"] == request.headers.get("User-Agent"): + # session[__SESSION_KEY__] = sess_info + # return sess_info + # # request session verification + # return verify_session(sess_info) + # New session + session[__SESSION_KEY__] = sess_info + return sess_info + +def session_info() -> SessionInfo: + """Retrieve the session information""" + anon_id = uuid4() + return save_session_info( + session.get(__SESSION_KEY__, { + "session_id": uuid4(), + "user": { + "user_id": anon_id, + "name": "Anonymous User", + "token": Left("INVALID-TOKEN"), + "logged_in": False + }, + "anon_id": anon_id, + "user_agent": request.headers.get("User-Agent"), + "ip_addr": request.environ.get("HTTP_X_FORWARDED_FOR", + request.remote_addr) + })) + +def set_user_token(token: str) -> SessionInfo: + """Set the user's token.""" + info = session_info() + return save_session_info({ + **info, "user": {**info["user"], "token": Right(token)}}) + +def set_user_details(userdets: UserDetails) -> SessionInfo: + """Set the user details information""" + return save_session_info({**session_info(), "user": userdets}) + +def user_token() -> Either: + """Retrieve the user token.""" + return session_info()["user"]["token"] diff --git a/wqflask/wqflask/oauth2/toplevel.py b/wqflask/wqflask/oauth2/toplevel.py index 109ed06c..ef9ce3db 100644 --- a/wqflask/wqflask/oauth2/toplevel.py +++ b/wqflask/wqflask/oauth2/toplevel.py @@ -1,12 +1,14 @@ """Authentication endpoints.""" +from uuid import UUID from urllib.parse import urljoin from flask import ( - flash, request, session, Blueprint, url_for, redirect, render_template, + flash, request, Blueprint, url_for, redirect, render_template, current_app as app) +from . import session from .client import SCOPE, no_token_post -from .request_utils import process_error from .checks import require_oauth2, user_logged_in +from .request_utils import user_details, process_error toplevel = Blueprint("toplevel", __name__) @@ -25,7 +27,14 @@ def authorisation_code(): return redirect("/") def __success__(token): - session["oauth2_token"] = token + session.set_user_token(token) + udets = user_details() + session.set_user_details({ + "user_id": UUID(udets["user_id"]), + "name": udets["name"], + "token": session.user_token(), + "logged_in": True + }) return redirect(url_for("oauth2.user.user_profile")) code = request.args.get("code", "") diff --git a/wqflask/wqflask/oauth2/ui.py b/wqflask/wqflask/oauth2/ui.py index abf30f4e..315aae2b 100644 --- a/wqflask/wqflask/oauth2/ui.py +++ b/wqflask/wqflask/oauth2/ui.py @@ -2,13 +2,13 @@ from flask import session, render_template from .client import oauth2_get +from .checks import user_logged_in from .request_utils import process_error def render_ui(templatepath: str, **kwargs): """Handle repetitive UI rendering stuff.""" - logged_in = lambda: ("oauth2_token" in session and bool(session["oauth2_token"])) roles = kwargs.get("roles", tuple()) # Get roles if already provided - if logged_in() and not bool(roles): # If not, try fetching them + if user_logged_in() and not bool(roles): # If not, try fetching them roles_results = oauth2_get("oauth2/user/roles").either( lambda err: {"roles_error": process_error(err)}, lambda roles: {"roles": roles}) @@ -18,7 +18,6 @@ def render_ui(templatepath: str, **kwargs): privilege["privilege_id"] for role in roles for privilege in role["privileges"]) kwargs = { - **kwargs, "roles": roles, "user_privileges": user_privileges, - "logged_in": logged_in + **kwargs, "roles": roles, "user_privileges": user_privileges } return render_template(templatepath, **kwargs) diff --git a/wqflask/wqflask/oauth2/users.py b/wqflask/wqflask/oauth2/users.py index 44ba252a..597dfb33 100644 --- a/wqflask/wqflask/oauth2/users.py +++ b/wqflask/wqflask/oauth2/users.py @@ -1,11 +1,13 @@ import requests +from uuid import UUID from urllib.parse import urljoin from authlib.integrations.base_client.errors import OAuthError from flask import ( - flash, request, session, url_for, redirect, Response, Blueprint, + flash, request, url_for, redirect, Response, Blueprint, current_app as app) +from . import session from .ui import render_ui from .checks import require_oauth2, user_logged_in from .client import oauth2_get, oauth2_post, oauth2_client @@ -18,7 +20,6 @@ users = Blueprint("user", __name__) def user_profile(): __id__ = lambda the_val: the_val usr_dets = user_details() - client = oauth2_client() def __render__(usr_dets, roles=[], **kwargs): return render_ui( "oauth2/view-user.html", user_details=usr_dets, roles=roles, @@ -74,7 +75,14 @@ def login(): username=form.get("email_address"), password=form.get("password"), grant_type="password") - session["oauth2_token"] = token + session.set_token(token) + udets = user_details() + session.set_user_details({ + "user_id": UUID(udets["user_id"]), + "name": udets["name"], + "token": session.user_token(), + "logged_in": True + }) except OAuthError as _oaerr: flash(_oaerr.args[0], "alert-danger") return render_ui( @@ -91,13 +99,10 @@ def login(): @users.route("/logout", methods=["GET", "POST"]) def logout(): if user_logged_in(): - token = session.get("oauth2_token", False) config = app.config - client = oauth2_client() - resp = client.revoke_token(urljoin(config["GN_SERVER_URL"], "oauth2/revoke")) - keys = tuple(key for key in session.keys() if not key.startswith("_")) - for key in keys: - session.pop(key, default=None) + resp = oauth2_client().revoke_token( + urljoin(config["GN_SERVER_URL"], "oauth2/revoke")) + session.clear_session_info() flash("Successfully logged out.", "alert-success") return redirect("/") |