Esempio n. 1
0
def rest__user_signup():
    """
    REST API endpoint: user sign up
    """
    def add_user_signup_req(us_req_map, us_req):
        key = us_req['email_address']
        us_req_map[key] = us_req  # finally, add request

    def rm_user_signup_req(us_req_map, us_req):
        key = us_req['email_address']
        del us_req_map[key]

    def sanitize_and_validate_input(req):
        req_json = request.get_json()

        # TODO: sanitize
        pw_min_len = 8
        field_to_regex_map = {
            'first_name': r'\w{1,16}',
            'last_name': r'\w{1,16}',
            'rz_username': r'\w{1,16}',
            'email_address': r'[^@]+@[^@]+\.[^@]+',
            'pw_plaintxt': r'[A-Za-z0-9]{%d,%d}' %
            (pw_min_len, 3 * pw_min_len),  # avoid symbols
        }

        # match field_val <> regex
        for f_key, regex in field_to_regex_map.items():
            f_val = req_json.get(f_key)
            f_name = f_key.replace('_', ' ')

            if None == f_val or 0 == len(f_val):  # missing or empty value
                e = API_Exception__bad_request(
                    'malformed signup request: missing field: %s' % (f_key))
                e.caller_err_msg = 'Missing value: %s' % (f_name)
                raise e

            if None == re.match(regex, f_val):
                e = API_Exception__bad_request(
                    'malformed signup request: regex match failure: regex: %s, input: %s'
                    % (regex, f_val))
                if f_key == 'pw_plaintxt':  # provide more info on invalid pw case
                    e.caller_err_msg = 'Illegal password: use a minimum of %d alphanumeric chars' % (
                        pw_min_len)
                else:
                    e.caller_err_msg = 'Illegal \'%s\' value: %s' % (f_name,
                                                                     f_val)
                raise e

        first_name = req_json['first_name']
        last_name = req_json['last_name']
        rz_username = req_json['rz_username']
        email_address = req_json['email_address']
        pw_plaintxt = req_json['pw_plaintxt']

        ret = User_Signup_Request(email_address=email_address,
                                  first_name=first_name,
                                  last_name=last_name,
                                  pw_plaintxt=pw_plaintxt,
                                  rz_username=rz_username)

        return ret

    def get_or_init_usreq_map():
        """
        lazy user signup request map getter
        """

        if not hasattr(current_app, 'usreq_email_to_req_map'):
            setattr(current_app, 'usreq_email_to_req_map', {})
        us_req_map = current_app.usreq_email_to_req_map

        return us_req_map

    # TODO: externalize
    html_ok__submitted = '<p>Your request has been successfully submitted.<br>Please check your email to activate your account.</p>'
    html_ok__already_pending = '<p>Your request has already been submitted.<br>Please check your email to activate your account.</p>'
    html_err__tech_difficulty = '<p>We are experiencing technical difficulty processing your request,<br>Please try again later.</p>'
    html_err__acl__singup = '<p>Invalid email: \'%s\'. Please use an email account from the following domains or contact an administrator: %s.</p>'

    # use incoming request as house keeping trigger
    us_req_map = get_or_init_usreq_map()
    filter_expired__singup_requests(us_req_map)

    if request.method == 'POST':
        # FIXME: add captcha
        # if req_json['captcha_solution'] = ...

        try:
            us_req = sanitize_and_validate_input(request)
        except API_Exception__bad_request as e:
            log.exception(e)
            return make_response__json__html(
                status=HTTP_STATUS__400_BAD_REQUEST,
                html_str='<p>%s</p>' % (e.caller_err_msg))
        except Exception as e:
            log.exception(e)
            return make_response__json__html(
                status=HTTP_STATUS__400_BAD_REQUEST,
                html_str=html_err__tech_difficulty)

        # FIXME: implement form validation

        # probe for existing request
        req_email_address = us_req['email_address']
        existing_req = us_req_map.get(req_email_address)
        if None != existing_req:
            # already pending
            log.warning('user signup: request already pending: %s' %
                        (existing_req))
            return make_response__json__html(status=HTTP_STATUS__200_OK,
                                             html_str=html_ok__already_pending)

        # match email if access_control is enabled and at least one ACL was provided
        rz_config = current_app.rz_config
        if rz_config.access_control:
            if rz_config.acl_wl__email_domain_set is not None or \
               rz_config.acl_wl__email_address_set is not None:
                if False == acl_wl_match__email_address(req_email_address):
                    return make_response__json__html(
                        status=HTTP_STATUS__400_BAD_REQUEST,
                        html_str=html_err__acl__singup %
                        (req_email_address,
                         current_app.rz_config.acl_wl__email_domain_set))

        us_req['submission_date'] = datetime.now()
        us_req['validation_key'] = generate_security_token()

        # send activation email
        try:
            activation_link = send_user_activation_link__email(
                request.url_root, us_req)

            log.info(
                'user sign-up: req received, activation link sent via email: link: %s'
                % (activation_link))

            add_user_signup_req(us_req_map, us_req)
            return make_response__json__html(html_str=html_ok__submitted)
        except Exception as e:
            log.exception('user sign-up: failed to send validation email'
                          )  # exception derived from stack
            return make_response__json__html(
                status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR,
                html_str=html_err__tech_difficulty)

    if request.method == 'GET':

        v_tok = request.args.get('v_tok')
        if None != v_tok:  # validation token passed
            for _, us_req in us_req_map.items():
                other_req_v_tok = us_req['validation_key']
                if None == other_req_v_tok:
                    continue
                if other_req_v_tok == v_tok:

                    if us_req.has_expired(
                    ):  # check again whether request has expired
                        log.warning(
                            'user signup: attempt to activate expired signup request: email: %s'
                            % (us_req['email_address']))
                        return render_template('user_signup.html',
                                               state='activation_failure')

                    try:
                        activate_user_account(us_req)
                    except Exception as e:
                        log.exception(e)
                        return render_template('user_signup.html',
                                               state='activation_failure')

                    # activation success
                    rm_user_signup_req(us_req_map, us_req)
                    return render_template('user_signup.html',
                                           state='activation_success')

            # request expired & already removed OR bad token
            log.warning(
                'user sign-up: request not found or bad token: remote-address: %s'
                % (request.remote_addr))
            return render_template('user_signup.html',
                                   state='activation_failure')

        else:  # no validation token: new user
            return render_template('user_signup.html')
Esempio n. 2
0
def rest__user_signup():
    """
    REST API endpoint: user sign up
    """

    def add_user_signup_req(us_req_map, us_req):
        key = us_req['email_address']
        us_req_map[key] = us_req  # finally, add request

    def rm_user_signup_req(us_req_map, us_req):
        key = us_req['email_address']
        del us_req_map[key]

    def sanitize_and_validate_input(req):
        req_json = request.get_json()

        # TODO: sanitize
        pw_min_len = 8
        field_to_regex_map = {'first_name': r'\w{1,16}',
                              'last_name': r'\w{1,16}',
                              'rz_username': r'\w{1,16}',
                              'email_address': r'[^@]+@[^@]+\.[^@]+',
                              'pw_plaintxt': r'[A-Za-z0-9]{%d,%d}' % (pw_min_len, 3 * pw_min_len),  # avoid symbols
                              }

        # match field_val <> regex
        for f_key, regex in field_to_regex_map.items():
            f_val = req_json.get(f_key)
            f_name = f_key.replace('_', ' ')

            if None == f_val or 0 == len(f_val):  # missing or empty value
                e = API_Exception__bad_request('malformed signup request: missing field: %s' % (f_key))
                e.caller_err_msg = 'Missing value: %s' % (f_name)
                raise e

            if None == re.match(regex, f_val):
                e = API_Exception__bad_request('malformed signup request: regex match failure: regex: %s, input: %s' % (regex, f_val))
                if f_key == 'pw_plaintxt':  # provide more info on invalid pw case
                    e.caller_err_msg = 'Illegal password: use a minimum of %d alphanumeric chars' % (pw_min_len)
                else:
                    e.caller_err_msg = 'Illegal \'%s\' value: %s' % (f_name, f_val)
                raise e

        first_name = req_json['first_name']
        last_name = req_json['last_name']
        rz_username = req_json['rz_username']
        email_address = req_json['email_address']
        pw_plaintxt = req_json['pw_plaintxt']

        ret = User_Signup_Request(email_address=email_address,
                                  first_name=first_name,
                                  last_name=last_name,
                                  pw_plaintxt=pw_plaintxt,
                                  rz_username=rz_username)

        return ret

    def get_or_init_usreq_map():
        """
        lazy user signup request map getter
        """

        if not hasattr(current_app, 'usreq_email_to_req_map'):
            setattr(current_app, 'usreq_email_to_req_map', {})
        us_req_map = current_app.usreq_email_to_req_map

        return us_req_map

    # TODO: externalize
    html_ok__submitted = '<p>Your request has been successfully submitted.<br>Please check your email to activate your account.</p>'
    html_ok__already_pending = '<p>Your request has already been submitted.<br>Please check your email to activate your account.</p>'
    html_err__tech_difficulty = '<p>We are experiencing technical difficulty processing your request,<br>Please try again later.</p>'
    html_err__acl__singup = '<p>Invalid email: \'%s\'. Please use an email account from the following domains or contact an administrator: %s.</p>'

    # use incoming request as house keeping trigger
    us_req_map = get_or_init_usreq_map()
    filter_expired__singup_requests(us_req_map)

    if request.method == 'POST':
        # FIXME: add captcha
        # if req_json['captcha_solution'] = ...

        try:
            us_req = sanitize_and_validate_input(request)
        except API_Exception__bad_request as e:
            log.exception(e)
            return make_response__json__html(status=HTTP_STATUS__400_BAD_REQUEST, html_str='<p>%s</p>' % (e.caller_err_msg))
        except Exception as e:
            log.exception(e)
            return make_response__json__html(status=HTTP_STATUS__400_BAD_REQUEST, html_str=html_err__tech_difficulty)

        # FIXME: implement form validation

        # probe for existing request
        req_email_address = us_req['email_address']
        existing_req = us_req_map.get(req_email_address)
        if None != existing_req:
            # already pending
            log.warning('user signup: request already pending: %s' % (existing_req))
            return make_response__json__html(status=HTTP_STATUS__200_OK, html_str=html_ok__already_pending)

        # match email if access_control is enabled and at least one ACL was provided
        rz_config = current_app.rz_config
        if rz_config.access_control:
            if rz_config.acl_wl__email_domain_set is not None or \
               rz_config.acl_wl__email_address_set is not None:
                if False == acl_wl_match__email_address(req_email_address):
                    return make_response__json__html(status=HTTP_STATUS__400_BAD_REQUEST, html_str=html_err__acl__singup % (req_email_address, current_app.rz_config.acl_wl__email_domain_set))

        us_req['submission_date'] = datetime.now()
        us_req['validation_key'] = generate_security_token()

        # send activation email
        try:
            activation_link = send_user_activation_link__email(request.url_root, us_req)

            log.info('user sign-up: req received, activation link sent via email: link: %s' % (activation_link))

            add_user_signup_req(us_req_map, us_req)
            return make_response__json__html(html_str=html_ok__submitted)
        except Exception as e:
            log.exception('user sign-up: failed to send validation email')  # exception derived from stack
            return make_response__json__html(status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR, html_str=html_err__tech_difficulty)

    if request.method == 'GET':

        v_tok = request.args.get('v_tok')
        if None != v_tok:  # validation token passed
            for _, us_req in us_req_map.items():
                other_req_v_tok = us_req['validation_key']
                if None == other_req_v_tok:
                    continue
                if other_req_v_tok == v_tok:

                    if us_req.has_expired():  # check again whether request has expired
                        log.warning('user signup: attempt to activate expired signup request: email: %s' % (us_req['email_address']))
                        return render_template('user_signup.html', state='activation_failure')

                    try:
                        activate_user_account(us_req)
                    except Exception as e:
                        log.exception(e)
                        return render_template('user_signup.html', state='activation_failure')

                    # activation success
                    rm_user_signup_req(us_req_map, us_req)
                    return render_template('user_signup.html', state='activation_success')

            # request expired & already removed OR bad token
            log.warning('user sign-up: request not found or bad token: remote-address: %s' % (request.remote_addr))
            return render_template('user_signup.html', state='activation_failure')

        else:  # no validation token: new user
            return render_template('user_signup.html')
Esempio n. 3
0
def rest__pw_reset():
    def get_or_init_pw_rst_req_map():
        """
        lazy pw reset request map getter. We map requests twice:
           - once by email, to throttle reset requests per account
           - second by security token, to allow lookup by supplied token on POST
        """
        if not hasattr(current_app, 'pw_rst_req_map'):
            setattr(current_app, 'pw_rst_req_map', {})
        return current_app.pw_rst_req_map

    def sanitize_input(req):
        """
        here we expect a map of input-element id's to input-element values
        """
        req_json = request.get_json()
        email_address = req_json.get(
            'email_address')  # None when collecting the new password
        new_user_pw = req_json.get(
            'new_user_password')  # None when submitting initial reset request
        pw_rst_tok = req_json.get('pw_rst_tok')  # None unless accepting new pw

        if None != email_address:
            regex__email_address = r'[^@]+@[^@]+\.[^@]+'
            if None == re.match(regex__email_address, email_address):
                raise Exception(
                    'malformed signup request: regex match failure: regex: %s, input: %s'
                    % (regex__email_address, email_address))

        if None != pw_rst_tok:
            tmp_tok = generate_security_token()
            if len(tmp_tok) != len(pw_rst_tok):
                raise Exception(
                    'malformed signup request: malformed security token: %s' %
                    (pw_rst_tok))

        return {
            'email_address': email_address,
            'new_user_pw': new_user_pw,
            'pw_rst_tok': pw_rst_tok
        }

    html_ok__submitted = '<p>Your request has been received.<br>Please check your email to reset your password.</p>'
    html_ok__pw_updated = '<p>Your password has been successfully updated.<br>Please head over to the <a href="/login">login page</a> to access your account.'
    html_ok__already_pending = '<p>Your request has already been submitted.<br>please check your email to proceed.</p>'
    html_err__tech_difficulty = '<p>We are experiencing technical difficulty processing your request,<br>please try again later.</p>'

    pw_rst_req_map = get_or_init_pw_rst_req_map()
    filter_expired__pw_rst_requests(
        pw_rst_req_map)  # first do some housekeeping

    if request.method == 'GET':

        pw_rst_tok = request.args.get('pw_rst_tok')
        if None == pw_rst_tok:  # request submission
            return render_template(
                'pw_reset.html',
                pw_rst_step='step_0__reset_request_submission')

        else:  # token passed, perform request validation
            pw_rst_req = pw_rst_req_map.get(pw_rst_tok)

            if None == pw_rst_req:
                # request expired & already removed OR bad token
                log.warning(
                    'pw reset: request not found or bad token: remote-address: %s'
                    % (request.remote_addr))
                return render_template('pw_reset.html',
                                       pw_rst_step='general_error')

            if pw_rst_req.has_expired(
            ):  # check again whether request has expired
                log.warning(
                    'pw reset: attempt to activate expired reset request: email: %s'
                    % (pw_rst_req.email_address))
                return render_template('pw_reset.html',
                                       pw_rst_step='general_error')

            return render_template('pw_reset.html',
                                   pw_rst_step='step_1__collect_new_password')

    if request.method == 'POST':

        req_data = sanitize_input(request)
        user_db = current_app.user_db

        pw_rst_tok = req_data.get('pw_rst_tok')
        new_user_pw = req_data.get('new_user_pw')
        email_address = req_data.get('email_address')

        # handle according to passed req arguments
        if pw_rst_tok and new_user_pw and None == email_address:  # validate token & collect new pw

            uid = None

            pw_rst_req = pw_rst_req_map.get(pw_rst_tok)
            if None == pw_rst_req:
                # request expired & already removed OR bad token
                log.warning(
                    'pw reset: request not found or bad token: remote-address: %s'
                    % (request.remote_addr))
                return make_response__json__html(
                    status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR,
                    html_str=html_err__tech_difficulty)

            # perform pw update
            uid, u_account = user_db.lookup_user__by_email_address(
                pw_rst_req.u_account.email_address)
            user_db.update_user_password(uid, new_user_pw)
            del pw_rst_req_map[pw_rst_tok]  # rm request mapping: sec token
            del pw_rst_req_map[
                u_account.email_address]  # rm request mapping: email address

            log.info('pw reset: pw reset complete: u_account: %s' %
                     (u_account))
            return make_response__json__html(html_str=html_ok__pw_updated)

        elif email_address and None == pw_rst_tok and None == new_user_pw:  # request submission

            u_account = None

            try:
                uid, u_account = user_db.lookup_user__by_email_address(
                    email_address)
            except Exception as _:
                log.exception(
                    'pw reset request for non-existing account: email_address: %s'
                    % (email_address))
                return make_response__json__html(
                    status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR,
                    html_str=html_err__tech_difficulty)

            if None != pw_rst_req_map.get(
                    email_address):  # probe for pending requests
                return make_response__json__html(
                    html_str=html_ok__already_pending)

            pw_rst_tok = generate_security_token()
            pw_rst_req = User_Pw_Reset_Request(u_account, pw_rst_tok)

            try:
                pw_reset_link = send_user_pw_reset__email(
                    request.url_root, u_account, pw_rst_tok)
                pw_rst_req.submission_date = datetime.now(
                )  # mark submission date
                pw_rst_req_map[pw_rst_tok] = pw_rst_req
                pw_rst_req_map[email_address] = pw_rst_req

                log.info(
                    'pw reset: req received, reset link sent via email: link: %s'
                    % (pw_reset_link))
                return make_response__json__html(html_str=html_ok__submitted)

            except Exception as _:
                return make_response__json__html(
                    status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR,
                    html_str=html_err__tech_difficulty)

        else:  # weird state: missing / unnecessary post fields
            return make_response__json__html(
                status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR,
                html_str=html_err__tech_difficulty)
Esempio n. 4
0
def rest__pw_reset():

    def get_or_init_pw_rst_req_map():
        """
        lazy pw reset request map getter. We map requests twice:
           - once by email, to throttle reset requests per account
           - second by security token, to allow lookup by supplied token on POST
        """
        if not hasattr(current_app, 'pw_rst_req_map'):
            setattr(current_app, 'pw_rst_req_map', {})
        return current_app.pw_rst_req_map

    def sanitize_input(req):
        """
        here we expect a map of input-element id's to input-element values
        """
        req_json = request.get_json()
        email_address = req_json.get('email_address')  # None when collecting the new password
        new_user_pw = req_json.get('new_user_password')  # None when submitting initial reset request
        pw_rst_tok = req_json.get('pw_rst_tok')  # None unless accepting new pw

        if None != email_address:
            regex__email_address = r'[^@]+@[^@]+\.[^@]+'
            if None == re.match(regex__email_address, email_address):
                raise Exception('malformed signup request: regex match failure: regex: %s, input: %s' % (regex__email_address, email_address))

        if None != pw_rst_tok:
            tmp_tok = generate_security_token()
            if len(tmp_tok) != len(pw_rst_tok):
                raise Exception('malformed signup request: malformed security token: %s' % (pw_rst_tok))

        return {'email_address': email_address,
                'new_user_pw': new_user_pw,
                'pw_rst_tok': pw_rst_tok }

    html_ok__submitted = '<p>Your request has been received.<br>Please check your email to reset your password.</p>'
    html_ok__pw_updated = '<p>Your password has been successfully updated.<br>Please head over to the <a href="/login">login page</a> to access your account.'
    html_ok__already_pending = '<p>Your request has already been submitted.<br>please check your email to proceed.</p>'
    html_err__tech_difficulty = '<p>We are experiencing technical difficulty processing your request,<br>please try again later.</p>'

    pw_rst_req_map = get_or_init_pw_rst_req_map()
    filter_expired__pw_rst_requests(pw_rst_req_map)  # first do some housekeeping

    if request.method == 'GET':

        pw_rst_tok = request.args.get('pw_rst_tok')
        if None == pw_rst_tok:  # request submission
            return render_template('pw_reset.html', pw_rst_step='step_0__reset_request_submission')

        else:  # token passed, perform request validation
            pw_rst_req = pw_rst_req_map.get(pw_rst_tok)

            if None == pw_rst_req:
                # request expired & already removed OR bad token
                log.warning('pw reset: request not found or bad token: remote-address: %s' % (request.remote_addr))
                return render_template('pw_reset.html', pw_rst_step='general_error')

            if pw_rst_req.has_expired():  # check again whether request has expired
                log.warning('pw reset: attempt to activate expired reset request: email: %s' % (pw_rst_req.email_address))
                return render_template('pw_reset.html', pw_rst_step='general_error')

            return render_template('pw_reset.html', pw_rst_step='step_1__collect_new_password')

    if request.method == 'POST':

        req_data = sanitize_input(request)
        user_db = current_app.user_db

        pw_rst_tok = req_data.get('pw_rst_tok')
        new_user_pw = req_data.get('new_user_pw')
        email_address = req_data.get('email_address')

        # handle according to passed req arguments
        if pw_rst_tok and new_user_pw and None == email_address:  # validate token & collect new pw

            uid = None

            pw_rst_req = pw_rst_req_map.get(pw_rst_tok)
            if None == pw_rst_req:
                # request expired & already removed OR bad token
                log.warning('pw reset: request not found or bad token: remote-address: %s' % (request.remote_addr))
                return make_response__json__html(status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR, html_str=html_err__tech_difficulty)

            # perform pw update
            uid, u_account = user_db.lookup_user__by_email_address(pw_rst_req.u_account.email_address)
            user_db.update_user_password(uid, new_user_pw)
            del pw_rst_req_map[pw_rst_tok]  # rm request mapping: sec token
            del pw_rst_req_map[u_account.email_address]  # rm request mapping: email address

            log.info('pw reset: pw reset complete: u_account: %s' % (u_account))
            return make_response__json__html(html_str=html_ok__pw_updated)

        elif email_address and None == pw_rst_tok and None == new_user_pw:  # request submission

            u_account = None

            try:
                uid, u_account = user_db.lookup_user__by_email_address(email_address)
            except Exception as _:
                log.exception('pw reset request for non-existing account: email_address: %s' % (email_address))
                return make_response__json__html(status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR, html_str=html_err__tech_difficulty)

            if None != pw_rst_req_map.get(email_address):  # probe for pending requests
                return make_response__json__html(html_str=html_ok__already_pending)

            pw_rst_tok = generate_security_token()
            pw_rst_req = User_Pw_Reset_Request(u_account, pw_rst_tok)

            try:
                pw_reset_link = send_user_pw_reset__email(request.url_root, u_account, pw_rst_tok)
                pw_rst_req.submission_date = datetime.now()  # mark submission date
                pw_rst_req_map[pw_rst_tok] = pw_rst_req
                pw_rst_req_map[email_address] = pw_rst_req

                log.info('pw reset: req received, reset link sent via email: link: %s' % (pw_reset_link))
                return make_response__json__html(html_str=html_ok__submitted)

            except Exception as _:
                return make_response__json__html(status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR, html_str=html_err__tech_difficulty)

        else:  # weird state: missing / unnecessary post fields
            return make_response__json__html(status=HTTP_STATUS__500_INTERNAL_SERVER_ERROR, html_str=html_err__tech_difficulty)