aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2023-05-23 11:44:18 +0300
committerFrederick Muriuki Muriithi2023-05-23 11:44:18 +0300
commit3f5e71500e845130f992e24d288d827c76d795b3 (patch)
treea7fe4f47198df04e9a6cf84b5fe2c1b1b8e967e6
parent7506b570548f93fe223030679c577b663c55d36f (diff)
downloadgenenetwork2-3f5e71500e845130f992e24d288d827c76d795b3.tar.gz
oauth2: Enable admins to masquerade as other users
-rw-r--r--wqflask/wqflask/oauth2/client.py3
-rw-r--r--wqflask/wqflask/oauth2/session.py34
-rw-r--r--wqflask/wqflask/oauth2/toplevel.py1
-rw-r--r--wqflask/wqflask/oauth2/users.py59
-rw-r--r--wqflask/wqflask/templates/oauth2/masquerade.html39
-rw-r--r--wqflask/wqflask/templates/oauth2/profile_nav.html12
6 files changed, 139 insertions, 9 deletions
diff --git a/wqflask/wqflask/oauth2/client.py b/wqflask/wqflask/oauth2/client.py
index 44463456..b992f0ad 100644
--- a/wqflask/wqflask/oauth2/client.py
+++ b/wqflask/wqflask/oauth2/client.py
@@ -12,7 +12,8 @@ from authlib.integrations.requests_client import OAuth2Session
from wqflask.oauth2 import session
from wqflask.oauth2.checks import user_logged_in
-SCOPE = "profile group role resource register-client user introspect migrate-data"
+SCOPE = ("profile group role resource register-client user masquerade "
+ "introspect migrate-data")
def oauth2_client():
config = app.config
diff --git a/wqflask/wqflask/oauth2/session.py b/wqflask/wqflask/oauth2/session.py
index 011d95f3..ec878f50 100644
--- a/wqflask/wqflask/oauth2/session.py
+++ b/wqflask/wqflask/oauth2/session.py
@@ -1,6 +1,6 @@
"""Deal with user sessions"""
from uuid import UUID, uuid4
-from typing import Any, TypedDict
+from typing import Any, Optional, TypedDict
from flask import request, session
from pymonad.either import Left, Right, Either
@@ -9,6 +9,7 @@ class UserDetails(TypedDict):
"""Session information relating specifically to the user."""
user_id: UUID
name: str
+ email: str
token: Either
logged_in: bool
@@ -19,6 +20,7 @@ class SessionInfo(TypedDict):
anon_id: UUID
user_agent: str
ip_addr: str
+ masquerade: Optional[UserDetails]
__SESSION_KEY__ = "session_info" # Do not use this outside this module!!
@@ -49,14 +51,16 @@ def session_info() -> SessionInfo:
"session_id": uuid4(),
"user": {
"user_id": anon_id,
- "name": "Anonymous User",
+ "name": "Anonymous User",
+ "email": "anon@ymous.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)
+ request.remote_addr),
+ "masquerading": None
}))
def set_user_token(token: str) -> SessionInfo:
@@ -72,3 +76,27 @@ def set_user_details(userdets: UserDetails) -> SessionInfo:
def user_token() -> Either:
"""Retrieve the user token."""
return session_info()["user"]["token"]
+
+def set_masquerading(masq_info):
+ """Save the masquerading user information."""
+ orig_user = session_info()["user"]
+ return save_session_info({
+ **session_info(),
+ "user": {
+ "user_id": UUID(masq_info["masquerade_as"]["user"]["user_id"]),
+ "name": masq_info["masquerade_as"]["user"]["name"],
+ "email": masq_info["masquerade_as"]["user"]["email"],
+ "token": Right(masq_info["masquerade_as"]["token"]),
+ "logged_in": True
+ },
+ "masquerading": orig_user
+ })
+
+def unset_masquerading():
+ """Restore the original session."""
+ the_session = session_info()
+ return save_session_info({
+ **the_session,
+ "user": the_session["masquerading"],
+ "masquerading": None
+ })
diff --git a/wqflask/wqflask/oauth2/toplevel.py b/wqflask/wqflask/oauth2/toplevel.py
index 04a08870..61d53dfe 100644
--- a/wqflask/wqflask/oauth2/toplevel.py
+++ b/wqflask/wqflask/oauth2/toplevel.py
@@ -32,6 +32,7 @@ def authorisation_code():
session.set_user_details({
"user_id": UUID(udets["user_id"]),
"name": udets["name"],
+ "email": udets["email"],
"token": session.user_token(),
"logged_in": True
})
diff --git a/wqflask/wqflask/oauth2/users.py b/wqflask/wqflask/oauth2/users.py
index 597dfb33..51a4ca4c 100644
--- a/wqflask/wqflask/oauth2/users.py
+++ b/wqflask/wqflask/oauth2/users.py
@@ -7,11 +7,13 @@ from flask import (
flash, request, url_for, redirect, Response, Blueprint,
current_app as app)
+from . import client
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
-from .request_utils import user_details, request_error, process_error
+from .request_utils import (
+ user_details, request_error, process_error, with_flash_error)
users = Blueprint("user", __name__)
@@ -80,6 +82,7 @@ def login():
session.set_user_details({
"user_id": UUID(udets["user_id"]),
"name": udets["name"],
+ "email": udets["email"],
"token": session.user_token(),
"logged_in": True
})
@@ -102,10 +105,22 @@ def logout():
config = app.config
resp = oauth2_client().revoke_token(
urljoin(config["GN_SERVER_URL"], "oauth2/revoke"))
- session.clear_session_info()
- flash("Successfully logged out.", "alert-success")
-
- return redirect("/")
+ the_session = session.session_info()
+ if not bool(the_session["masquerading"]):
+ # Normal session - clear and go back.
+ session.clear_session_info()
+ flash("Successfully logged out.", "alert-success")
+ return redirect("/")
+ # Restore masquerading session
+ session.unset_masquerading()
+ flash(
+ "Successfully logged out as user "
+ f"{the_session['user']['name']} ({the_session['user']['email']}) "
+ "and restored session for user "
+ f"{the_session['masquerading']['name']} "
+ f"({the_session['masquerading']['email']})",
+ "alert-success")
+ return redirect("/")
@users.route("/register", methods=["GET", "POST"])
def register_user():
@@ -139,3 +154,37 @@ def register_user():
flash("Registration successful! Please login to continue.", "alert-success")
return redirect(url_for("oauth2.user.login"))
+
+@users.route("/masquerade", methods=["GET", "POST"])
+def masquerade():
+ """Masquerade as a particular user."""
+ if request.method == "GET":
+ this_user = session.session_info()["user"]
+ return client.get("oauth2/user/list").either(
+ lambda err: render_ui(
+ "oauth2/masquerade.html", users_error=process_error(err)),
+ lambda usrs: render_ui(
+ "oauth2/masquerade.html", users=tuple(
+ usr for usr in usrs
+ if UUID(usr["user_id"]) != this_user["user_id"])))
+
+ def __masq_success__(masq_details):
+ session.set_masquerading(masq_details)
+ flash(
+ f"User {masq_details['original']['user']['name']} "
+ f"({masq_details['original']['user']['email']}) is now "
+ "successfully masquerading as the user "
+ f"User {masq_details['masquerade_as']['user']['name']} "
+ f"({masq_details['masquerade_as']['user']['email']}) is now ",
+ "alert-success")
+ return redirect("/")
+ form = request.form
+ masquerade_as = form.get("masquerade_as").strip()
+ if not(bool(masquerade_as)):
+ flash("You must provide a user to masquerade as.", "alert-danger")
+ return redirect(url_for("oauth2.user.masquerade"))
+ return client.post(
+ "oauth2/user/masquerade/",
+ json={"masquerade_as": request.form.get("masquerade_as")}).either(
+ with_flash_error(redirect(url_for("oauth2.user.masquerade"))),
+ __masq_success__)
diff --git a/wqflask/wqflask/templates/oauth2/masquerade.html b/wqflask/wqflask/templates/oauth2/masquerade.html
new file mode 100644
index 00000000..48ec6cee
--- /dev/null
+++ b/wqflask/wqflask/templates/oauth2/masquerade.html
@@ -0,0 +1,39 @@
+{%extends "base.html"%}
+{%from "oauth2/profile_nav.html" import profile_nav%}
+{%from "oauth2/display_error.html" import display_error%}
+{%block title%}Masquerade As{%endblock%}
+{%block content%}
+<div class="container" style="min-width: 1250px;">
+ {{profile_nav("masquerade", user_privileges)}}
+
+ {{flash_me()}}
+
+ {%if users_error is defined%}
+ {{display_error("Users", users_error)}}
+ {%else%}
+ <div class="container-fluid">
+ <div class="row">
+ <form method="POST"
+ action="{{url_for('oauth2.user.masquerade')}}">
+ <legend>Masquerade As</legend>
+ <div class="form-group">
+ <label for="select-masquerade" class="form-label">
+ Masquerade as
+ </label>
+ <select id="select-masquerade" name="masquerade_as"
+ required="required" class="form-control">
+ <option value="">Select User</option>
+ {%for user in users%}
+ <option value="{{user.user_id}}">{{user.name}} ({{user.email}})</option>
+ {%endfor%}
+ </select>
+ </div>
+ <div class="form-group">
+ <input type="submit" class="btn btn-primary" value="Masquerade" />
+ </div>
+ </form>
+ </div>
+ </div>
+ {%endif%}
+</div>
+{%endblock%}
diff --git a/wqflask/wqflask/templates/oauth2/profile_nav.html b/wqflask/wqflask/templates/oauth2/profile_nav.html
index 0a61c40f..24658ee8 100644
--- a/wqflask/wqflask/templates/oauth2/profile_nav.html
+++ b/wqflask/wqflask/templates/oauth2/profile_nav.html
@@ -39,6 +39,18 @@
</li>
{%endif%}
+ {%if "system:user:masquerade" in user_privileges %}
+ <li role="presentation"
+ {%if calling_page == "masquerade"%}
+ class="active"
+ {%endif%}>
+ <a href="{{url_for('oauth2.user.masquerade')}}"
+ title="Masquerade as another user">
+ Masquerade As
+ </a>
+ </li>
+ {%endif%}
+
<li role="presentation">
{%if logged_in():%}
<a href="{{url_for('oauth2.user.logout')}}">Logout</a>