about summary refs log tree commit diff
path: root/gn_auth/auth/authorisation/users/admin
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth/auth/authorisation/users/admin')
-rw-r--r--gn_auth/auth/authorisation/users/admin/models.py56
-rw-r--r--gn_auth/auth/authorisation/users/admin/ui.py4
-rw-r--r--gn_auth/auth/authorisation/users/admin/views.py160
3 files changed, 95 insertions, 125 deletions
diff --git a/gn_auth/auth/authorisation/users/admin/models.py b/gn_auth/auth/authorisation/users/admin/models.py
index 36f3c09..3d68932 100644
--- a/gn_auth/auth/authorisation/users/admin/models.py
+++ b/gn_auth/auth/authorisation/users/admin/models.py
@@ -1,23 +1,55 @@
 """Major function for handling admin users."""
+import warnings
+
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.authentication.users import User
+from gn_auth.auth.authorisation.roles.models import Role, db_rows_to_roles
 
-def make_sys_admin(cursor: db.DbCursor, user: User) -> User:
-    """Make a given user into an system admin."""
+
+def sysadmin_role(conn: db.DbConnection) -> Role:
+    """Fetch the `system-administrator` role details."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "SELECT roles.*, privileges.* "
+            "FROM roles INNER JOIN role_privileges "
+            "ON roles.role_id=role_privileges.role_id "
+            "INNER JOIN privileges "
+            "ON role_privileges.privilege_id=privileges.privilege_id "
+            "WHERE role_name='system-administrator'")
+        results = db_rows_to_roles(cursor.fetchall())
+
+    assert len(results) == 1, (
+        "There should only ever be one 'system-administrator' role.")
+    return results[0]
+
+
+def grant_sysadmin_role(cursor: db.DbCursor, user: User) -> User:
+    """Grant `system-administrator` role to `user`."""
     cursor.execute(
             "SELECT * FROM roles WHERE role_name='system-administrator'")
     admin_role = cursor.fetchone()
-    cursor.execute(
-            "SELECT * FROM resources AS r "
-            "INNER JOIN resource_categories AS rc "
-            "ON r.resource_category_id=rc.resource_category_id "
-            "WHERE resource_category_key='system'")
-    the_system = cursor.fetchone()
-    cursor.execute(
+    cursor.execute("SELECT resources.resource_id FROM resources")
+    cursor.executemany(
         "INSERT INTO user_roles VALUES (:user_id, :role_id, :resource_id)",
-        {
+        tuple({
             "user_id": str(user.user_id),
             "role_id": admin_role["role_id"],
-            "resource_id": the_system["resource_id"]
-        })
+            "resource_id": resource_id
+        } for resource_id in cursor.fetchall()))
     return user
+
+
+def make_sys_admin(cursor: db.DbCursor, user: User) -> User:
+    """Make a given user into an system admin."""
+    warnings.warn(
+        DeprecationWarning(
+            f"The function `{__name__}.make_sys_admin` will be removed soon"),
+        stacklevel=1)
+    return grant_sysadmin_role(cursor, user)
+
+
+def revoke_sysadmin_role(conn: db.DbConnection, user: User):
+    """Revoke `system-administrator` role from `user`."""
+    with db.cursor(conn) as cursor:
+        cursor.execute("DELETE FROM user_roles WHERE user_id=? AND role_id=?",
+                       (str(user.user_id), str(sysadmin_role(conn).role_id)))
diff --git a/gn_auth/auth/authorisation/users/admin/ui.py b/gn_auth/auth/authorisation/users/admin/ui.py
index 64e79a0..43ca0a2 100644
--- a/gn_auth/auth/authorisation/users/admin/ui.py
+++ b/gn_auth/auth/authorisation/users/admin/ui.py
@@ -1,6 +1,6 @@
 """UI utilities for the auth system."""
 from functools import wraps
-from flask import flash, url_for, redirect
+from flask import flash, request, url_for, redirect
 
 from gn_auth.session import logged_in, session_user, clear_session_info
 from gn_auth.auth.authorisation.resources.system.models import (
@@ -24,5 +24,5 @@ def is_admin(func):
         flash("Expected a system administrator.", "alert-danger")
         flash("You have been logged out of the system.", "alert-info")
         clear_session_info()
-        return redirect(url_for("oauth2.admin.login"))
+        return redirect(url_for("oauth2.admin.login", **dict(request.args)))
     return __admin__
diff --git a/gn_auth/auth/authorisation/users/admin/views.py b/gn_auth/auth/authorisation/users/admin/views.py
index 8ca1e51..9bc1c36 100644
--- a/gn_auth/auth/authorisation/users/admin/views.py
+++ b/gn_auth/auth/authorisation/users/admin/views.py
@@ -3,14 +3,12 @@ import uuid
 import json
 import random
 import string
-from pathlib import Path
 from typing import Optional
 from functools import partial
 from dataclasses import asdict
 from urllib.parse import urlparse
 from datetime import datetime, timezone, timedelta
 
-from authlib.jose import KeySet, JsonWebKey
 from email_validator import validate_email, EmailNotValidError
 from flask import (
     flash,
@@ -32,6 +30,7 @@ from ....authentication.oauth2.models.oauth2client import (
     save_client,
     OAuth2Client,
     oauth2_clients,
+    update_client_attribute,
     client as oauth2_client,
     delete_client as _delete_client)
 from ....authentication.users import (
@@ -62,7 +61,8 @@ _FORM_GRANT_TYPES_ = ({
 @admin.before_request
 def update_expires():
     """Update session expiration."""
-    if session.session_info() and not session.update_expiry():
+    if (session.session_info() and not session.update_expiry(
+            int(app.config.get("SESSION_EXPIRY_MINUTES", 10)))):
         flash("Session has expired. Logging out...", "alert-warning")
         session.clear_session_info()
         return redirect(url_for("oauth2.admin.login"))
@@ -96,8 +96,9 @@ def login():
                 session.update_session_info(
                     user=asdict(user),
                     expires=(
-                        datetime.now(tz=timezone.utc) + timedelta(minutes=10)))
-                return redirect(url_for(next_uri))
+                        datetime.now(tz=timezone.utc) + timedelta(minutes=int(
+                            app.config.get("SESSION_EXPIRY_MINUTES", 10)))))
+                return redirect(url_for(next_uri, **dict(request.args)))
             raise NotFoundError(error_message)
     except NotFoundError as _nfe:
         flash(error_message, "alert-danger")
@@ -176,6 +177,9 @@ def check_register_client_form(form):
         "scope[]",
         "You need to select at least one scope option."),)
 
+    if not uri_valid(form.get("client_jwk_uri", "")):
+        errors = errors + ("The provided client's public JWKs URI is invalid.",)
+
     errors = tuple(item for item in errors if item is not None)
     if bool(errors):
         raise RegisterClientError(errors)
@@ -193,7 +197,7 @@ def register_client():
     if request.method == "GET":
         return render_template(
             "admin/register-client.html",
-            scope=app.config["OAUTH2_SCOPE"],
+            scope=app.config["OAUTH2_SCOPES_SUPPORTED"],
             users=with_db_connection(__list_users__),
             granttypes=_FORM_GRANT_TYPES_,
             current_user=session.session_user())
@@ -223,7 +227,8 @@ def register_client():
             "default_redirect_uri": default_redirect_uri,
             "redirect_uris": [default_redirect_uri] + form.get("other_redirect_uri", "").split(),
             "response_type": __response_types__(tuple(grant_types)),
-            "scope": form.getlist("scope[]")
+            "scope": form.getlist("scope[]"),
+            "public-jwks-uri": form.get("client_jwk_uri", "")
         },
         user = with_db_connection(partial(
             user_by_id, user_id=uuid.UUID(form["user"])))
@@ -257,111 +262,9 @@ def view_client(client_id: uuid.UUID):
     return render_template(
         "admin/view-oauth2-client.html",
         client=with_db_connection(partial(oauth2_client, client_id=client_id)),
-        scope=app.config["OAUTH2_SCOPE"],
+        scope=app.config["OAUTH2_SCOPES_SUPPORTED"],
         granttypes=_FORM_GRANT_TYPES_)
 
-@admin.route("/register-client-public-key", methods=["POST"])
-@is_admin
-def register_client_public_key():
-    """Register a client's SSL key"""
-    form = request.form
-    admin_dashboard_uri = redirect(url_for("oauth2.admin.dashboard"))
-    view_client_uri = redirect(url_for("oauth2.admin.view_client",
-                                       client_id=form["client_id"]))
-    if not bool(form.get("client_id")):
-        flash("No client selected.", "alert-danger")
-        return admin_dashboard_uri
-
-    try:
-        _client = with_db_connection(partial(
-            oauth2_client, client_id=uuid.UUID(form["client_id"])))
-        if _client.is_nothing():
-            raise ValueError("No such client.")
-        _client = _client.value
-    except ValueError:
-        flash("Invalid client ID provided.", "alert-danger")
-        return admin_dashboard_uri
-    try:
-        _key = JsonWebKey.import_key(form["client_ssl_key"].strip())
-    except ValueError:
-        flash("Invalid key provided!", "alert-danger")
-        return view_client_uri
-
-    keypath = Path(app.config["CLIENTS_SSL_PUBLIC_KEYS_DIR"]).joinpath(
-        f"{_key.thumbprint()}.pem")
-    if not keypath.exists():
-        with open(keypath, mode="w", encoding="utf8") as _kpth:
-            _kpth.write(form["client_ssl_key"])
-
-    with_db_connection(partial(save_client, the_client=OAuth2Client(
-        client_id=_client.client_id,
-        client_secret=_client.client_secret,
-        client_id_issued_at=_client.client_id_issued_at,
-        client_secret_expires_at=_client.client_secret_expires_at,
-        client_metadata={
-            **_client.client_metadata,
-            "public_keys": list(set(
-                _client.client_metadata.get("public_keys", []) +
-                [str(keypath)]))},
-        user=_client.user)))
-    flash("Client key successfully registered.", "alert-success")
-    return view_client_uri
-
-
-@admin.route("/delete-client-public-key", methods=["POST"])
-@is_admin
-def delete_client_public_key():
-    """Delete a client's SSL key"""
-    form = request.form
-    admin_dashboard_uri = redirect(url_for("oauth2.admin.dashboard"))
-    view_client_uri = redirect(url_for("oauth2.admin.view_client",
-                                       client_id=form["client_id"]))
-    if not bool(form.get("client_id")):
-        flash("No client selected.", "alert-danger")
-        return admin_dashboard_uri
-
-    try:
-        _client = with_db_connection(partial(
-            oauth2_client, client_id=uuid.UUID(form["client_id"])))
-        if _client.is_nothing():
-            raise ValueError("No such client.")
-        _client = _client.value
-    except ValueError:
-        flash("Invalid client ID provided.", "alert-danger")
-        return admin_dashboard_uri
-
-    if form.get("ssl_key", None) is None:
-        flash("The key must be provided.", "alert-danger")
-        return view_client_uri
-
-    try:
-        def find_by_kid(keyset: KeySet, kid: str) -> JsonWebKey:
-            for key in keyset.keys:
-                if key.thumbprint() == kid:
-                    return key
-            raise ValueError('Invalid JSON Web Key Set')
-        _key = find_by_kid(_client.jwks, form.get("ssl_key"))
-    except ValueError:
-        flash("Could not delete: No such public key.", "alert-danger")
-        return view_client_uri
-
-    _keys = (_key for _key in _client.jwks.keys
-             if _key.thumbprint() != form["ssl_key"])
-    _keysdir = Path(app.config["CLIENTS_SSL_PUBLIC_KEYS_DIR"])
-    with_db_connection(partial(save_client, the_client=OAuth2Client(
-            client_id=_client.client_id,
-            client_secret=_client.client_secret,
-            client_id_issued_at=_client.client_id_issued_at,
-            client_secret_expires_at=_client.client_secret_expires_at,
-            client_metadata={
-                **_client.client_metadata,
-                "public_keys": list(set(
-                    _keysdir.joinpath(f"{_key.thumbprint()}.pem")
-                    for _key in _keys))},
-            user=_client.user)))
-    flash("Key deleted.", "alert-success")
-    return view_client_uri
-
 
 @admin.route("/edit-client", methods=["POST"])
 @is_admin
@@ -389,7 +292,8 @@ def edit_client():
             [form["redirect_uri"]] +
             form["other_redirect_uris"].split("\r\n"))),
         "grant_types": form.getlist("grants[]"),
-        "scope": form.getlist("scope[]")
+        "scope": form.getlist("scope[]"),
+        "public-jwks-uri": form.get("client_jwk_uri", "")
     }
     with_db_connection(partial(save_client, the_client=OAuth2Client(
         the_client.client_id,
@@ -418,3 +322,37 @@ def delete_client():
            "successfully."),
           "alert-success")
     return redirect(url_for("oauth2.admin.list_clients"))
+
+
+@admin.route("/clients/<uuid:client_id>/change-secret", methods=["GET", "POST"])
+@is_admin
+def change_client_secret(client_id: uuid.UUID):
+    """Enable changing of a client's secret."""
+    def __no_client__():
+        # Calling the function causes the flash to be evaluated
+        # flash("No such client was found!", "alert-danger")
+        return redirect(url_for("oauth2.admin.list_clients"))
+
+    with db.connection(app.config["AUTH_DB"]) as conn:
+        if request.method == "GET":
+            return oauth2_client(
+                conn, client_id=client_id
+            ).maybe(__no_client__(), lambda _client: render_template(
+                "admin/confirm-change-client-secret.html",
+                client=_client
+            ))
+
+        _raw = random_string()
+        return oauth2_client(
+            conn, client_id=client_id
+        ).then(
+            lambda _client: save_client(
+                conn,
+                update_client_attribute(
+                    _client, "client_secret", hash_password(_raw)))
+        ).then(
+            lambda _client: render_template(
+                "admin/registered-client.html",
+                client=_client,
+                client_secret=_raw)
+        ).maybe(__no_client__(), lambda resp: resp)