Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
views.py355 linesDownload Raw Back to mfa
1##############################################################################2#3# pgAdmin 4 - PostgreSQL Tools4#5# Copyright (C) 2013 - 2024, The pgAdmin Development Team6# This software is released under the PostgreSQL Licence7#8##############################################################################9"""Multi-factor Authentication (MFA) views"""10 11import base6412from typing import Union13 14from flask import Response, render_template, request, flash, \15    current_app, url_for, redirect, session16from flask_babel import gettext as _17from flask_login import current_user, login_required18from flask_login.utils import login_url19 20from pgadmin.utils.csrf import pgCSRFProtect21from pgadmin.utils.ajax import bad_request22from .utils import user_supported_mfa_methods, mfa_user_registered, \23    mfa_suppored_methods, ValidationException, mfa_delete, is_mfa_enabled, \24    is_mfa_session_authenticated25from pgadmin.utils.constants import MessageType26 27 28_INDEX_URL = "browser.index"29_NO_CACHE_HEADERS = dict({30    "Cache-Control": "no-cache, no-store, must-revalidate, public, max-age=0",31    "Pragma": "no-cache",32    "Expires": "0",33})34 35 36def __handle_mfa_validation_request(37    mfa_method: str, user_mfa_auths: dict, form_data: dict38) -> None:39    """40    An internal utlity function to execute mfa.validate(...) method in case, it41    matched the following conditions:42    1. Method specified is a valid and in the supported methods list.43    2. User has registered for this auth method.44 45    Otherwise, raise an exception with appropriate error message.46 47    Args:48        mfa_method (str)     : Name of the authentication method49        user_mfa_auths (dict): List of the user supported authentication method50        form_data (dict)     : Form data in the request51 52    Raises:53        ValidationException: Raise the exception when user is not registered54                             for the given method, or not a valid MFA method.55    """56 57    if mfa_method is None:58        raise ValidationException(_("No authentication method provided."))59 60    mfa_auth = user_mfa_auths.get(mfa_method, None)61 62    if mfa_auth is None:63        raise ValidationException(_(64            "No user supported authentication method provided"65        ))66 67    mfa_auth.validate(**form_data)68 69 70@pgCSRFProtect.exempt71@login_required72def validate_view() -> Response:73    """74    An end-point to render the authentication view.75 76    It supports two HTTP methods:77    1. GET : Generate the view listing all the supported auth methods.78    2. POST: Validate the code/OTP, or whatever data the selected auth method79             supports.80 81    Returns:82        Response: Redirect to 'next' url in case authentication validate,83                  otherwise - a view with listing down all the supported auth84                  methods, and it's supporting views.85    """86 87    # Load at runtime to avoid circular dependency88    from pgadmin.authenticate import get_logout_url89 90    next_url = request.args.get("next", None)91 92    if next_url is None or next_url == url_for('mfa.register') or \93            next_url == url_for('mfa.validate'):94        next_url = url_for(_INDEX_URL)95 96    if session.get('mfa_authenticated', False) is True:97        return redirect(next_url)98 99    return_code = 200100    mfa_method = None101    user_mfa_auths = user_supported_mfa_methods()102 103    if request.method == 'POST':104        try:105            form_data = {key: request.form[key] for key in request.form}106            next_url = form_data.pop('next', url_for(_INDEX_URL))107            mfa_method = form_data.pop('mfa_method', None)108 109            __handle_mfa_validation_request(110                mfa_method, user_mfa_auths, form_data111            )112 113            session['mfa_authenticated'] = True114 115            return redirect(next_url)116 117        except ValidationException as ve:118            current_app.logger.warning((119                "MFA validation failed for the user '{}' with an error: "120                "{}"121            ).format(current_user.username, str(ve)))122            flash(str(ve), MessageType.ERROR)123            return_code = 401124        except Exception as ex:125            current_app.logger.exception(ex)126            flash(str(ex), MessageType.ERROR)127            return_code = 500128 129    mfa_views = {130        key: user_mfa_auths[key].validation_view_dict(mfa_method)131        for key in user_mfa_auths132    }133 134    if mfa_method is None and len(mfa_views) > 0:135        list(mfa_views.items())[0][1]['selected'] = True136 137    send_email_url = None138    if 'email' in mfa_views:139        send_email_url = url_for("mfa.send_email_code")140 141    return Response(render_template(142        "mfa/validate.html", _=_, views=mfa_views, base64=base64,143        logout_url=get_logout_url(),144        send_email_url=send_email_url145    ), return_code, headers=_NO_CACHE_HEADERS, mimetype="text/html")146 147 148def _mfa_registration_view(149    supported_mfa: dict, form_data: dict150) -> Union[str, None]:151    """152    An internal utility function to generate the registration view, or153    unregister for the given MFA object (passed as a dict).154 155    It will call 'registration_view' function, specific for the MFA method,156    only if User has clicked on 'Setup' button on the registration page, and157    current user is not already registered for the Auth method.158 159    If the user has not clicked on the 'Setup' button, we assume that he has160    clicked on the 'Delete' button for a specific auth method.161 162    Args:163        supported_mfa (dict): [description]164        form_data (dict): [description]165 166    Returns:167        Union[str, None]: When registration for the Auth method is completed,168                          it could return None, otherwise view for the169                          registration view.170    """171    mfa = supported_mfa['mfa']172 173    if form_data[mfa.name] == 'SETUP':174        if supported_mfa['registered'] is True:175            flash(_("'{}' is already registerd'").format(mfa.label),176                  MessageType.SUCCESS)177            return None178 179        return mfa.registration_view(form_data)180 181    if mfa_delete(mfa.name) is True:182        flash(_(183            "'{}' unregistered from the authentication list."184        ).format(mfa.label), MessageType.SUCCESS)185 186        return None187 188    flash(_(189        "'{}' is not found in the authentication list."190    ).format(mfa.label), MessageType.WARNING)191 192    return None193 194 195def _registration_view_or_deregister(196    _auth_list: dict197) -> Union[str, bool, None]:198    """199    An internal utility function to parse the request, and act upon it:200    1. Find the auth method in the request, and call the201       '_mfa_registration_view' internal utility function for the same, and202       return the result of it.203 204    It could return a registration view as a string, or None (on205    deregistering).206 207    Args:208        _auth_list (dict): List of all supported methods with a flag for the209                           current user registration.210 211    Returns:212        Union[str, bool, None]: When no valid request found, it will return213                                False, otherwise the response of the214                                '_mfa_registration_view(...)' method call.215    """216 217    for key in _auth_list:218        if key in request.form:219            return _mfa_registration_view(220                _auth_list[key], request.form221            )222 223    return False224 225 226def __handle_registration_view_for_post_method(227    _next_url: str, _mfa_auths: dict228) -> (Union[str, None], Union[Response, None], Union[dict, None]):229    """230    An internal utility function to handle the POST method for the registration231    view. It will pass on the request data to the appropriate Auth method, and232    may generate further registration view. When registration is completed, it233    will redirect to the 'next_url' in case the registration page is not opened234    from the internal dialog through menu, which can be identified by the235    'next_url' value is equal to 'internal'.236 237    Args:238        _next_url (str)  : Redirect to which url, when clicked on the239                           'continue' button on the registration page.240        _mfa_auths (dict): A dict object returned by the method -241                           'mfa_suppored_methods'.242 243    Returns:244        (Union[str, None], Union[Response, None], Union[dict, None]):245        Possibilities:246        1. Returns (None, redirect response to 'next' url, None) in case there247           is not valid 'auth' method found in the request.248        2. Returns (None, Registration view as Response, None) in case when249           valid method found, and it has returned a view to render.250        3. Otherwise - Returns the set as251           (updated 'next' url, None, updated Auth method list)252    """253 254    next_url = request.form.get("next", None)255 256    if next_url is None or next_url == url_for('mfa.validate'):257        next_url = url_for(_INDEX_URL)258 259    if request.form.get('cancel', None) is None:260        view = _registration_view_or_deregister(_mfa_auths)261 262        if view is False:263            if next_url != 'internal':264                return None, redirect(next_url), None265            flash(_("Please close the dialog."), MessageType.INFO)266 267        if view is not None:268            return None, Response(269                render_template(270                    "mfa/register.html", _=_,271                    mfa_list=list(), mfa_view=view,272                    next_url=next_url,273                    error_message=None274                ), 200,275                headers=_NO_CACHE_HEADERS276            ), None277 278        # Regenerate the supported MFA list after279        # registration/deregistration.280        _mfa_auths = mfa_suppored_methods()281 282    return next_url, None, _mfa_auths283 284 285@pgCSRFProtect.exempt286@login_required287def registration_view() -> Response:288    """289    A url end-point to register/deregister an authentication method.290 291    It supports two HTTP methods:292    * GET : Generate a view listing all the suppoted list with 'Setup',293            or 'Delete' buttons. If user has registered for the auth method, it294            will render a 'Delete' button next to it, and 'Setup' button295            otherwise.296    * POST: This handles multiple scenarios on the registration page:297            1. Clicked on the 'Delete' button, it will deregister the user for298               the specific auth method, and render the view same as for the299               'GET' method.300            2. Clicked on the 'Setup' button, it will render the registration301               view for the authentication method.302            3. Clicked 'Continue' button, redirect it to the url specified by303               'next' url.304            4. Clicking on 'Cancel' button on the Auth method specific view305               will render the view by 'GET' HTTP method.306            5. A registration method can run like a wizard, and generate307               different views based on the request data.308 309    Returns:310        Response: A response object with list of auth methods, a registration311                  view, or redirect to 'next' url312    """313    mfa_auths = mfa_suppored_methods()314    mfa_list = list()315 316    next_url = request.args.get("next", None)317 318    if request.method == 'POST':319        next_url, response, mfa_auths = \320            __handle_registration_view_for_post_method(next_url, mfa_auths)321 322        if response is not None:323            return response324 325    if next_url is None:326        next_url = url_for(_INDEX_URL)327 328    error_message = None329    found_one_mfa = False330 331    for key in mfa_auths:332        mfa = mfa_auths[key]['mfa']333        mfa = mfa.to_dict()334        mfa["registered"] = mfa_auths[key]["registered"]335        mfa_list.append(mfa)336        found_one_mfa = found_one_mfa or mfa["registered"]337 338    if request.method == 'GET':339        if is_mfa_enabled() is False:340            error_message = _(341                "Can't access this page, when multi factor authentication is "342                "disabled."343            )344        elif is_mfa_session_authenticated() is False and \345                found_one_mfa is True:346            flash(_("Complete the authentication process first"),347                  MessageType.ERROR)348            return redirect(login_url("mfa.validate", next_url=next_url))349 350    return Response(render_template(351        "mfa/register.html", _=_,352        mfa_list=mfa_list, mfa_view=None, next_url=next_url,353        error_message=error_message354    ), 200 if error_message is None else 401, headers=_NO_CACHE_HEADERS)355 
codekingpro/portable-devtools · Team Ai