diff options
-rw-r--r-- | gn3/auth/authentication/oauth2/models/oauth2client.py | 33 | ||||
-rw-r--r-- | gn3/auth/authentication/users.py | 16 | ||||
-rw-r--r-- | gn3/auth/authorisation/users/admin/views.py | 20 | ||||
-rw-r--r-- | gn3/templates/admin/dashboard.html | 4 | ||||
-rw-r--r-- | gn3/templates/admin/list-oauth2-clients.html | 45 |
5 files changed, 110 insertions, 8 deletions
diff --git a/gn3/auth/authentication/oauth2/models/oauth2client.py b/gn3/auth/authentication/oauth2/models/oauth2client.py index 14c4c94..564ed32 100644 --- a/gn3/auth/authentication/oauth2/models/oauth2client.py +++ b/gn3/auth/authentication/oauth2/models/oauth2client.py @@ -1,13 +1,13 @@ """OAuth2 Client model.""" import json -import uuid import datetime +from uuid import UUID from typing import Sequence, Optional, NamedTuple from pymonad.maybe import Just, Maybe, Nothing from gn3.auth import db -from gn3.auth.authentication.users import User, user_by_id, same_password +from gn3.auth.authentication.users import User, users, user_by_id, same_password from gn3.auth.authorisation.errors import NotFoundError @@ -18,7 +18,7 @@ class OAuth2Client(NamedTuple): This is defined according to the mixin at https://docs.authlib.org/en/latest/specs/rfc6749.html#authlib.oauth2.rfc6749.ClientMixin """ - client_id: uuid.UUID + client_id: UUID client_secret: str client_id_issued_at: datetime.datetime client_secret_expires_at: datetime.datetime @@ -129,7 +129,7 @@ class OAuth2Client(NamedTuple): """Return the default redirect uri""" return self.client_metadata.get("default_redirect_uri", "") -def client(conn: db.DbConnection, client_id: uuid.UUID, +def client(conn: db.DbConnection, client_id: UUID, user: Optional[User] = None) -> Maybe: """Retrieve a client by its ID""" with db.cursor(conn) as cursor: @@ -145,7 +145,7 @@ def client(conn: db.DbConnection, client_id: uuid.UUID, the_user = None return Just( - OAuth2Client(uuid.UUID(result["client_id"]), + OAuth2Client(UUID(result["client_id"]), result["client_secret"], datetime.datetime.fromtimestamp( result["client_id_issued_at"]), @@ -156,7 +156,7 @@ def client(conn: db.DbConnection, client_id: uuid.UUID, return Nothing -def client_by_id_and_secret(conn: db.DbConnection, client_id: uuid.UUID, +def client_by_id_and_secret(conn: db.DbConnection, client_id: UUID, client_secret: str) -> OAuth2Client: """Retrieve a client by its ID and secret""" with db.cursor(conn) as cursor: @@ -171,7 +171,7 @@ def client_by_id_and_secret(conn: db.DbConnection, client_id: uuid.UUID, datetime.datetime.fromtimestamp( row["client_secret_expires_at"]), json.loads(row["client_metadata"]), - user_by_id(conn, uuid.UUID(row["user_id"]))) + user_by_id(conn, UUID(row["user_id"]))) raise NotFoundError("Could not find client with the given credentials.") @@ -203,3 +203,22 @@ def save_client(conn: db.DbConnection, the_client: OAuth2Client) -> OAuth2Client "user_id": str(the_client.user.user_id) }) return the_client + +def oauth2_clients(conn: db.DbConnection) -> tuple[OAuth2Client, ...]: + """Fetch a list of all OAuth2 clients.""" + with db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM oauth2_clients") + clients_rs = cursor.fetchall() + the_users = { + usr.user_id: usr for usr in users( + conn, tuple({UUID(result["user_id"]) for result in clients_rs})) + } + return tuple(OAuth2Client(UUID(result["client_id"]), + result["client_secret"], + datetime.datetime.fromtimestamp( + result["client_id_issued_at"]), + datetime.datetime.fromtimestamp( + result["client_secret_expires_at"]), + json.loads(result["client_metadata"]), + the_users[UUID(result["user_id"])]) + for result in clients_rs) diff --git a/gn3/auth/authentication/users.py b/gn3/auth/authentication/users.py index 8b4f115..0e72ed2 100644 --- a/gn3/auth/authentication/users.py +++ b/gn3/auth/authentication/users.py @@ -110,3 +110,19 @@ def set_user_password( "ON CONFLICT (user_id) DO UPDATE SET password=:hash"), {"user_id": str(user.user_id), "hash": hashed_password}) return user, hashed_password + +def users(conn: db.DbConnection, + ids: tuple[UUID, ...] = tuple()) -> tuple[User, ...]: + """ + Fetch all users with the given `ids`. If `ids` is empty, return ALL users. + """ + params = ", ".join(["?"] * len(ids)) + with db.cursor(conn) as cursor: + query = "SELECT * FROM users" + ( + f" WHERE user_id IN ({params})" + if len(ids) > 0 else "") + print(query) + cursor.execute(query, tuple(str(the_id) for the_id in ids)) + return tuple(User(UUID(row["user_id"]), row["email"], row["name"]) + for row in cursor.fetchall()) + return tuple() diff --git a/gn3/auth/authorisation/users/admin/views.py b/gn3/auth/authorisation/users/admin/views.py index ee76354..11152d2 100644 --- a/gn3/auth/authorisation/users/admin/views.py +++ b/gn3/auth/authorisation/users/admin/views.py @@ -1,5 +1,6 @@ """UI for admin stuff""" import uuid +import json import random import string from functools import partial @@ -22,7 +23,8 @@ from gn3.auth.db_utils import with_db_connection from gn3.auth.authentication.oauth2.models.oauth2client import ( save_client, - OAuth2Client) + OAuth2Client, + oauth2_clients) from gn3.auth.authentication.users import ( User, user_by_id, @@ -44,6 +46,7 @@ def update_expires(): return None @admin.route("/dashboard", methods=["GET"]) +@is_admin def dashboard(): """Admin dashboard.""" return render_template("admin/dashboard.html") @@ -151,3 +154,18 @@ def register_client(): "admin/registered-client.html", client=client, client_secret = raw_client_secret) + +def __parse_client__(sqlite3Row) -> dict: + """Parse the client details into python datatypes.""" + return { + **dict(sqlite3Row), + "client_metadata": json.loads(sqlite3Row["client_metadata"]) + } + +@admin.route("/list-client", methods=["GET"]) +@is_admin +def list_clients(): + """List all registered OAuth2 clients.""" + return render_template( + "admin/list-oauth2-clients.html", + clients=with_db_connection(oauth2_clients)) diff --git a/gn3/templates/admin/dashboard.html b/gn3/templates/admin/dashboard.html index 889e652..52fb7d3 100644 --- a/gn3/templates/admin/dashboard.html +++ b/gn3/templates/admin/dashboard.html @@ -13,6 +13,10 @@ title="Register a new OAuth2 client.">Register OAuth2 Client</a> </li> <li> + <a href="{{url_for('oauth2.admin.list_clients')}}" + title="List OAuth2 clients.">List OAuth2 Client</a> + </li> + <li> <a href="{{url_for('oauth2.admin.logout')}}" title="Log out of the system.">Logout</a> </li> diff --git a/gn3/templates/admin/list-oauth2-clients.html b/gn3/templates/admin/list-oauth2-clients.html new file mode 100644 index 0000000..f6bbcb2 --- /dev/null +++ b/gn3/templates/admin/list-oauth2-clients.html @@ -0,0 +1,45 @@ +{%extends "base.html"%} + +{%block title%}Genenetwork3: OAuth2 Clients{%endblock%} + +{%block content%} +{{flash_messages()}} + +<h1>Genenetwork3: OAuth2 Clients</h1> + +<table> + <legend>List of registered OAuth2 clients</legend> + <thead> + <tr> + <th>Client ID</th> + <th>Client Name</th> + <th>Default Redirect URI</th> + <th>Owner</th> + <th>Actions</th> + </tr> + </thead> + + <tbody> + {%for client in clients%} + <tr> + <td>{{client.client_id}}</td> + <td>{{client.client_metadata.client_name}}</td> + <td>{{client.client_metadata.default_redirect_uri}}</td> + <td>{{client.user.name}} ({{client.user.email}})</td> + <td> + <a href="#{{client.client_id}}" + title"View/Edit client {{client.client_metadata.client_name}}"> + View/Edit + </a> + </td> + </tr> + {%else%} + <tr> + <td colspan="4" style="text-align: center;"> + No registered OAuth2 clients! + </td> + </tr> + {%endfor%} + </tbody> +</table> +{%endblock%} |