From 5f9f4ff97c27a0f34a86eec516ab3f58faf5937e Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Tue, 23 May 2023 09:34:19 +0300 Subject: auth: Enable user masquerade Enable users with the appropriate privileges to masquerade as other users by providing an endpoint that provides a new token for the "masqueradee" --- .../authorisation/users/masquerade/__init__.py | 1 + gn3/auth/authorisation/users/masquerade/models.py | 67 ++++++++++++++++++++++ gn3/auth/authorisation/users/masquerade/views.py | 48 ++++++++++++++++ gn3/auth/authorisation/users/views.py | 2 + 4 files changed, 118 insertions(+) create mode 100644 gn3/auth/authorisation/users/masquerade/__init__.py create mode 100644 gn3/auth/authorisation/users/masquerade/models.py create mode 100644 gn3/auth/authorisation/users/masquerade/views.py (limited to 'gn3/auth/authorisation') diff --git a/gn3/auth/authorisation/users/masquerade/__init__.py b/gn3/auth/authorisation/users/masquerade/__init__.py new file mode 100644 index 0000000..69d64f0 --- /dev/null +++ b/gn3/auth/authorisation/users/masquerade/__init__.py @@ -0,0 +1 @@ +"""Package to deal with masquerading.""" diff --git a/gn3/auth/authorisation/users/masquerade/models.py b/gn3/auth/authorisation/users/masquerade/models.py new file mode 100644 index 0000000..9f24b6b --- /dev/null +++ b/gn3/auth/authorisation/users/masquerade/models.py @@ -0,0 +1,67 @@ +"""Functions for handling masquerade.""" +from uuid import uuid4 +from functools import wraps +from datetime import datetime + +from flask import current_app as app + +from gn3.auth import db + +from gn3.auth.authorisation.errors import ForbiddenAccess +from gn3.auth.authorisation.roles.models import user_roles + +from gn3.auth.authentication.users import User +from gn3.auth.authentication.oauth2.models.oauth2token import ( + OAuth2Token, save_token) + +__FIVE_HOURS__ = (60 * 60 * 5) + +def can_masquerade(func): + """Security decorator.""" + @wraps(func) + def __checker__(*args, **kwargs): + if len(args) == 3: + conn, token, _masq_user = args + elif len(args) == 2: + conn, token = args + elif len(args) == 1: + conn = args[0] + token = kwargs["original_token"] + else: + conn = kwargs["conn"] + token = kwargs["original_token"] + + masq_privs = [priv for role in user_roles(conn, token.user) + for priv in role.privileges + if priv.privilege_id == "system:user:masquerade"] + if len(masq_privs) == 0: + raise ForbiddenAccess( + "You do not have the ability to masquerade as another user.") + return func(*args, **kwargs) + return __checker__ + +@can_masquerade +def masquerade_as( + conn: db.DbConnection, + original_token: OAuth2Token, + masqueradee: User) -> OAuth2Token: + """Get a token that enables `masquerader` to act as `masqueradee`.""" + token_details = app.config["OAUTH2_SERVER"].generate_token( + client=original_token.client, + grant_type="authorization_code", + user=masqueradee, + expires_in=__FIVE_HOURS__, + include_refresh_token=True) + new_token = OAuth2Token( + token_id=uuid4(), + client=original_token.client, + token_type=token_details["token_type"], + access_token=token_details["access_token"], + refresh_token=token_details.get("refresh_token"), + scope=original_token.scope, + revoked=False, + issued_at=datetime.now(), + expires_in=token_details["expires_in"], + user=masqueradee) + save_token(conn, new_token) + return new_token diff --git a/gn3/auth/authorisation/users/masquerade/views.py b/gn3/auth/authorisation/users/masquerade/views.py new file mode 100644 index 0000000..43286a1 --- /dev/null +++ b/gn3/auth/authorisation/users/masquerade/views.py @@ -0,0 +1,48 @@ +"""Endpoints for user masquerade""" +from uuid import UUID +from functools import partial + +from flask import request, jsonify, Response, Blueprint + +from gn3.auth.db_utils import with_db_connection +from gn3.auth.authorisation.errors import InvalidData +from gn3.auth.authorisation.checks import require_json + +from gn3.auth.authentication.users import user_by_id +from gn3.auth.authentication.oauth2.resource_server import require_oauth + +from .models import masquerade_as + +masq = Blueprint("masquerade", __name__) + +@masq.route("/", methods=["POST"]) +@require_oauth("profile user masquerade") +@require_json +def masquerade() -> Response: + """Masquerade as a particular user.""" + with require_oauth.acquire("profile user masquerade") as token: + masqueradee_id = UUID(request.json["masquerade_as"])#type: ignore[index] + if masqueradee_id == token.user.user_id: + raise InvalidData("You are not allowed to masquerade as yourself.") + + masq_user = with_db_connection(partial( + user_by_id, user_id=masqueradee_id)) + def __masq__(conn): + new_token = masquerade_as(conn, original_token=token, masqueradee=masq_user) + return new_token + def __dump_token__(tok): + return { + key: value for key, value in (tok._asdict().items()) + if key in ("access_token", "refresh_token", "expires_in", + "token_type") + } + return jsonify({ + "original": { + "user": token.user._asdict(), + "token": __dump_token__(token) + }, + "masquerade_as": { + "user": masq_user._asdict(), + "token": __dump_token__(with_db_connection(__masq__)) + } + }) diff --git a/gn3/auth/authorisation/users/views.py b/gn3/auth/authorisation/users/views.py index ae39110..826e222 100644 --- a/gn3/auth/authorisation/users/views.py +++ b/gn3/auth/authorisation/users/views.py @@ -12,6 +12,7 @@ from gn3.auth.dictify import dictify from gn3.auth.db_utils import with_db_connection from .models import list_users +from .masquerade.views import masq from .collections.views import collections from ..groups.models import user_group as _user_group @@ -25,6 +26,7 @@ from ...authentication.users import User, save_user, set_user_password from ...authentication.oauth2.models.oauth2token import token_by_access_token users = Blueprint("users", __name__) +users.register_blueprint(masq, url_prefix="/masquerade") users.register_blueprint(collections, url_prefix="/collections") @users.route("/", methods=["GET"]) -- cgit v1.2.3