Exemplo n.º 1
0
def post_verification_token(email, token):
    """
    Gives call to party service to add a verification token for the respondent and increase the password reset counter
    :param email: the respondent's email
    :param token: the verification token
    """
    logger.info(
        "Attempting to add respondent verification token and increase password reset counter",
        email=obfuscate_email(email),
    )

    party_id = get_respondent_by_email(email)["id"]
    url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/{party_id}/password-verification-token"
    payload = {
        "token": token,
    }
    response = requests.post(url, auth=app.config["BASIC_AUTH"], json=payload)

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        logger.error(
            "Failed to add respondent verification token or increase password reset counter",
            email=obfuscate_email(email),
        )
        raise ApiError(logger, response)

    logger.info(
        "Successfully added respondent verification token and password reset counter",
        email=obfuscate_email(email))

    return response.json()
Exemplo n.º 2
0
def delete_verification_token(token):
    """
    Gives call to party service to delete a verification token for the respondent
    :param token: the verification token
    """
    email = decode_email_token(token)
    logger.info("Attempting to delete respondent verification token",
                email=obfuscate_email(email))

    party_id = get_respondent_by_email(email)["id"]
    url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/{party_id}/password-verification-token/{token}"
    response = requests.delete(url, auth=app.config["BASIC_AUTH"])

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        if response.status_code == 404:
            logger.error("Verification token not found")
            raise NotFound("Token not found")
        logger.error("Failed to delete respondent verification token",
                     email=obfuscate_email(email))
        raise ApiError(logger, response)

    logger.info("Successfully deleted respondent verification token",
                email=obfuscate_email(email))

    return response.json()
Exemplo n.º 3
0
def sign_in(username, password):
    """
    Checks if the users credentials are valid. On success it returns an empty dict (a hangover from when
    this function used to call a different authentication application).

    :param username: The username.  Should be an email address
    :param password: The password
    :raises AuthError: Raised if the credentials provided are incorrect
    :raises ApiError: Raised on any other non-401 error status code
    :return: An empty dict if credentials are valid.  An exception is raised otherwise
    """
    if app.config["CANARY_GENERATE_ERRORS"]:
        logger.error("Canary experiment running this error can be ignored",
                     status=500)
    logger.info("Attempting to sign in", email=obfuscate_email(username))

    url = f"{app.config['AUTH_URL']}/api/v1/tokens/"
    data = {
        "username": username,
        "password": password,
    }
    headers = {
        "Accept": "application/json",
        "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
    }
    response = requests.post(url,
                             headers=headers,
                             auth=app.config["BASIC_AUTH"],
                             data=data)

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        if response.status_code == 401:
            auth_error = response.json().get("detail", "")
            message = response.json().get("title", "")

            raise AuthError(logger,
                            response,
                            log_level="warning",
                            message=message,
                            auth_error=auth_error)
        else:
            logger.error("Failed to authenticate",
                         email=obfuscate_email(username))
            raise ApiError(logger, response)

    logger.info("Successfully signed in", email=obfuscate_email(username))
    return {}
Exemplo n.º 4
0
def change_password(session):
    form = ChangePasswordFrom(request.values)
    party_id = session.get_party_id()
    respondent_details = party_controller.get_respondent_party_by_id(party_id)
    if request.method == 'POST' and form.validate():
        username = respondent_details['emailAddress']
        password = request.form.get('password')
        new_password = request.form.get('new_password')
        if new_password == password:
            return render_template('account/account-change-password.html', form=form,
                                   errors={"new_password": ["Your new password is the same as your old password"]})
        bound_logger = logger.bind(email=obfuscate_email(username))
        bound_logger.info("Attempting to find user in auth service")
        try:
            # We call the sign in function to verify that the password provided is correct
            auth_controller.sign_in(username, password)
            bound_logger.info("Attempting to change password via party service")
            party_controller.change_password(username, new_password)
            bound_logger.info("password changed via party service")
            flash('Your password has been changed.')
            return redirect(url_for('surveys_bp.get_survey_list', tag='todo'))
        except AuthError as exc:
            error_message = exc.auth_error
            if BAD_CREDENTIALS_ERROR in error_message:
                bound_logger.info('Bad credentials provided')
                return render_template('account/account-change-password.html', form=form,
                                       errors={"password": ["Incorrect current password"]})
    else:
        errors = form.errors

    return render_template('account/account-change-password.html', form=form, errors=errors)
Exemplo n.º 5
0
def notify_party_and_respondent_account_locked(respondent_id,
                                               email_address,
                                               status=None):
    bound_logger = logger.bind(respondent_id=respondent_id,
                               email=obfuscate_email(email_address),
                               status=status)
    bound_logger.info(
        "Notifying respondent and party service that account is locked")
    url = f'{app.config["PARTY_URL"]}/party-api/v1/respondents/edit-account-status/{respondent_id}'

    data = {
        "respondent_id": respondent_id,
        "email_address": email_address,
        "status_change": status
    }

    response = requests.put(url, json=data, auth=app.config["BASIC_AUTH"])

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        bound_logger.error("Failed to notify party")
        bound_logger.unbind("respondent_id", "email", "status")
        raise ApiError(logger, response)

    bound_logger.info(
        "Successfully notified respondent and party service that account is locked"
    )
    bound_logger.unbind("respondent_id", "email", "status")
Exemplo n.º 6
0
def create_account(registration_data: dict) -> None:
    obfuscated_email = obfuscate_email(registration_data["emailAddress"])
    enrolment_code = registration_data["enrolmentCode"]
    logger.info("Attempting to create account",
                email=obfuscated_email,
                enrolment_code=enrolment_code)

    url = f"{app.config['PARTY_URL']}/party-api/v1/respondents"
    registration_data["status"] = "CREATED"
    response = requests.post(url,
                             auth=app.config["BASIC_AUTH"],
                             json=registration_data)

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        if response.status_code == 409:
            logger.info("Email has already been used",
                        email=obfuscated_email,
                        enrolment_code=enrolment_code)
        else:
            logger.error("Failed to create account",
                         email=obfuscated_email,
                         enrolment_code=enrolment_code)
        raise ApiError(logger, response, message=response.json())

    logger.info("Successfully created account",
                email=obfuscated_email,
                enrolment_code=enrolment_code)
Exemplo n.º 7
0
def change_password(email, password):
    bound_logger = logger.bind(email=obfuscate_email(email))
    bound_logger.info(
        "Attempting to change password through the party service")

    data = {"email_address": email, "new_password": password}
    url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/change_password"
    response = requests.put(url, auth=app.config["BASIC_AUTH"], json=data)

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        bound_logger.error(
            "Failed to send change password request to party service")
        raise ApiError(logger, response)

    bound_logger.info(
        "Successfully changed password through the party service")
    def test_obfuscate_email(self):
        """Tests the output of obfuscate email with both valid and invalid strings"""
        testAddresses = {
            "*****@*****.**": "e*****e@e*********m",
            "*****@*****.**": "p****x@d**********k",
            "*****@*****.**": "f********e@p**********k",
            "*****@*****.**": "m*********n@g*******m",
            "*****@*****.**": "a***********e@e*********m",
            "*****@*****.**":
            "j**************6@l****************k",
            "[email protected]": "m**?@e*********m",
            "*****@*****.**": "m@m***m",
            "joe.bloggs": "j********s",
            "joe.bloggs@": "j********s",
            "@gmail.com": "@g*******m"
        }

        for test in testAddresses:
            self.assertEqual(obfuscate_email(test), testAddresses[test])
Exemplo n.º 9
0
def delete_account(username: str):
    bound_logger = logger.bind(email=obfuscate_email(username))
    bound_logger.info("Attempting to delete account")
    url = f'{app.config["AUTH_URL"]}/api/account/user'
    # force_delete will always be true if deletion is initiated by user
    form_data = {"username": username, "force_delete": True}
    response = requests.delete(url,
                               data=form_data,
                               auth=app.config["BASIC_AUTH"])

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        bound_logger.error("Failed to delete account")
        bound_logger.unbind("email")
        raise ApiError(logger, response)

    bound_logger.info("Successfully deleted account")
    bound_logger.unbind("email")
    return response
Exemplo n.º 10
0
def reset_password_request(username):
    bound_logger = logger.bind(email=obfuscate_email(username))
    bound_logger.info(
        "Attempting to send reset password request to party service")

    url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/request_password_change"
    data = {"email_address": username}
    response = requests.post(url, auth=app.config["BASIC_AUTH"], json=data)

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        if response.status_code == 404:
            raise UserDoesNotExist("User does not exist in party service")
        bound_logger.error(
            "Failed to send reset password request to party service")
        raise ApiError(logger, response)

    bound_logger.info(
        "Successfully sent reset password request to party service")
Exemplo n.º 11
0
def get_respondent_by_email(email):
    bound_logger = logger.bind(email=obfuscate_email(email))
    bound_logger.info("Attempting to find respondent party by email")

    url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/email"
    response = requests.get(url,
                            json={"email": email},
                            auth=app.config["BASIC_AUTH"])

    if response.status_code == 404:
        bound_logger.info("Failed to retrieve party by email")
        return

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        bound_logger.error("Error retrieving respondent by email")
        raise ApiError(logger, response)

    bound_logger.info("Successfully retrieved respondent by email")
    return response.json()
def sign_in(username, password):
    if app.config["CANARY_GENERATE_ERRORS"]:
        logger.error("Canary experiment running this error can be ignored",
                     status=500)
    bound_logger = logger.bind(email=obfuscate_email(username))
    bound_logger.info('Attempting to sign in')

    url = f"{app.config['AUTH_URL']}/api/v1/tokens/"
    data = {
        'username': username,
        'password': password,
    }
    headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
    }
    response = requests.post(url,
                             headers=headers,
                             auth=app.config['BASIC_AUTH'],
                             data=data)

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        if response.status_code == 401:
            auth_error = response.json().get('detail', '')
            message = response.json().get('title', '')

            raise AuthError(logger,
                            response,
                            log_level='warning',
                            message=message,
                            auth_error=auth_error)
        else:
            bound_logger.error('Failed to authenticate')
            raise ApiError(logger, response)

    bound_logger.info('Successfully signed in')
    return {}
Exemplo n.º 13
0
def change_password(session):
    form = ChangePasswordFrom(request.values)
    party_id = session.get_party_id()
    respondent_details = party_controller.get_respondent_party_by_id(party_id)
    if request.method == "POST" and form.validate():
        username = respondent_details["emailAddress"]
        password = request.form.get("password")
        new_password = request.form.get("new_password")
        if new_password == password:
            return render_template(
                "account/account-change-password.html",
                form=form,
                errors={"new_password": ["Your new password is the same as your old password"]},
            )
        bound_logger = logger.bind(email=obfuscate_email(username))
        bound_logger.info("Attempting to find user in auth service")
        try:
            # We call the sign in function to verify that the password provided is correct
            auth_controller.sign_in(username, password)
            bound_logger.info("Attempting to change password via party service")
            party_controller.change_password(username, new_password)
            bound_logger.info("password changed via party service")
            flash("Your password has been changed. Please login with your new password.", "success")
            return redirect(url_for("sign_in_bp.logout"))
        except AuthError as exc:
            error_message = exc.auth_error
            if BAD_CREDENTIALS_ERROR in error_message:
                bound_logger.info("Bad credentials provided")
                return render_template(
                    "account/account-change-password.html",
                    form=form,
                    errors={"password": ["Incorrect current password"]},
                )
    else:
        errors = form.errors

    return render_template("account/account-change-password.html", form=form, errors=errors)
Exemplo n.º 14
0
def login():  # noqa: C901
    form = LoginForm(request.form)
    form.username.data = form.username.data.strip()
    account_activated = request.args.get('account_activated', None)

    secure = app.config['WTF_CSRF_ENABLED']

    if request.method == 'POST' and form.validate():
        username = form.username.data
        password = request.form.get('password')
        bound_logger = logger.bind(email=obfuscate_email(username))
        bound_logger.info("Attempting to find user in auth service")
        try:
            auth_controller.sign_in(username, password)
        except AuthError as exc:
            error_message = exc.auth_error
            party_json = party_controller.get_respondent_by_email(username)
            party_id = party_json.get('id') if party_json else None
            bound_logger = bound_logger.bind(party_id=party_id)

            if USER_ACCOUNT_LOCKED in error_message:  # pylint: disable=no-else-return
                if not party_id:
                    bound_logger.error("Respondent account locked in auth but doesn't exist in party")
                    return render_template('sign-in/sign-in.html', form=form, data={"error": {"type": "failed"}})
                bound_logger.info('User account is locked on the Auth server', status=party_json['status'])
                if party_json['status'] == 'ACTIVE' or party_json['status'] == 'CREATED':
                    notify_party_and_respondent_account_locked(respondent_id=party_id,
                                                               email_address=username,
                                                               status='SUSPENDED')
                return render_template('sign-in/sign-in.account-locked.html', form=form)
            elif NOT_VERIFIED_ERROR in error_message:
                bound_logger.info('User account is not verified on the Auth server')
                return render_template('sign-in/sign-in.account-not-verified.html', party_id=party_id)
            elif BAD_AUTH_ERROR in error_message:
                bound_logger.info('Bad credentials provided')
            elif UNKNOWN_ACCOUNT_ERROR in error_message:
                bound_logger.info('User account does not exist in auth service')
            else:
                bound_logger.error('Unexpected error was returned from Auth service', auth_error=error_message)

            return render_template('sign-in/sign-in.html', form=form, data={"error": {"type": "failed"}}, next=request.args.get('next'))

        bound_logger.info("Successfully found user in auth service.  Attempting to find user in party service")
        party_json = party_controller.get_respondent_by_email(username)
        if not party_json or 'id' not in party_json:
            bound_logger.error("Respondent has an account in auth but not in party")
            return render_template('sign-in/sign-in.html', form=form, data={"error": {"type": "failed"}})
        party_id = party_json['id']
        bound_logger = bound_logger.bind(party_id=party_id)

        if request.args.get('next'):
            response = make_response(redirect(request.args.get('next')))
        else:
            response = make_response(redirect(url_for('surveys_bp.get_survey_list', tag='todo', _external=True,
                                                      _scheme=getenv('SCHEME', 'http'))))

        bound_logger.info("Successfully found user in party service")
        bound_logger.info('Creating session')
        session = Session.from_party_id(party_id)
        response.set_cookie('authorization',
                            value=session.session_key,
                            expires=session.get_expires_in(),
                            secure=secure,
                            httponly=secure)
        count = conversation_controller.get_message_count_from_api(session)
        session.set_unread_message_total(count)
        bound_logger.info('Successfully created session', session_key=session.session_key)
        return response

    template_data = {
        "error": {
            "type": form.errors,
            "logged_in": "False"
        },
        'account_activated': account_activated
    }
    if request.args.get('next'):
        return render_template('sign-in/sign-in.html', form=form, data=template_data,
                               next=request.args.get('next'))
    return render_template('sign-in/sign-in.html', form=form, data=template_data)
Exemplo n.º 15
0
def register_enter_your_details():
    # Get and decrypt enrolment code
    encrypted_enrolment_code = request.args.get("encrypted_enrolment_code")
    try:
        enrolment_code = cryptographer.decrypt(
            encrypted_enrolment_code.encode()).decode()
    except AttributeError:
        logger.error("No enrolment code supplied",
                     exc_info=True,
                     url=request.url)
        raise
    form = RegistrationForm(request.values,
                            enrolment_code=encrypted_enrolment_code)
    if form.email_address.data is not None:
        form.email_address.data = form.email_address.data.strip()

    # Validate enrolment code before rendering or checking the form
    iac_controller.validate_enrolment_code(enrolment_code)

    if request.method == "POST" and form.validate():
        email_address = form.email_address.data

        registration_data = {
            "emailAddress": email_address,
            "firstName": request.form.get("first_name"),
            "lastName": request.form.get("last_name"),
            "password": request.form.get("password"),
            "telephone": request.form.get("phone_number"),
            "enrolmentCode": enrolment_code,
        }

        try:
            party_controller.create_account(registration_data)
        except ApiError as exc:
            if exc.status_code == 400:
                # If party returns an error, we should just log out the error with as much detail as possible, and
                # put a generic message up for the user as we don't want to show them any potentially ugly messages
                # from party
                logger.info(
                    "Party returned an error",
                    email=obfuscate_email(email_address),
                    enrolment_code=enrolment_code,
                    error=exc.message,
                )
                flash("Something went wrong, please try again or contact us",
                      "error")
                return render_template(
                    "register/register.enter-your-details.html",
                    form=form,
                    errors=form.errors)
            elif exc.status_code == 409:
                error = {
                    "email_address": [
                        "This email has already been used to register an account"
                    ]
                }
                return render_template(
                    "register/register.enter-your-details.html",
                    form=form,
                    errors=error)
            else:
                logger.error("Failed to create account",
                             status=exc.status_code,
                             error=exc.message)
                raise exc

        return render_template("register/register.almost-done.html",
                               email=email_address)

    else:
        return render_template("register/register.enter-your-details.html",
                               form=form,
                               errors=form.errors)
Exemplo n.º 16
0
def login():  # noqa: C901
    form = LoginForm(request.form)
    if form.username.data is not None:
        form.username.data = form.username.data.strip()

    if request.method == "POST" and form.validate():
        username = form.username.data
        password = request.form.get("password")
        bound_logger = logger.bind(email=obfuscate_email(username))
        bound_logger.info("Attempting to find user in auth service")
        try:
            auth_controller.sign_in(username, password)
        except AuthError as exc:
            error_message = exc.auth_error
            party_json = party_controller.get_respondent_by_email(username)
            party_id = party_json.get("id") if party_json else None
            bound_logger = bound_logger.bind(party_id=party_id)

            if USER_ACCOUNT_LOCKED in error_message:
                if not party_id:
                    bound_logger.error(
                        "Respondent account locked in auth but doesn't exist in party"
                    )
                    return render_template("sign-in/sign-in.html",
                                           form=form,
                                           data={"error": {
                                               "type": "failed"
                                           }})
                bound_logger.info("User account is locked on the Auth server",
                                  status=party_json["status"])
                if party_json["status"] == "ACTIVE" or party_json[
                        "status"] == "CREATED":
                    notify_party_and_respondent_account_locked(
                        respondent_id=party_id,
                        email_address=username,
                        status="SUSPENDED")
                return render_template("sign-in/sign-in.account-locked.html",
                                       form=form)
            elif NOT_VERIFIED_ERROR in error_message:
                bound_logger.info(
                    "User account is not verified on the Auth server")
                return render_template(
                    "sign-in/sign-in.account-not-verified.html",
                    party_id=party_id)
            elif BAD_AUTH_ERROR in error_message:
                bound_logger.info("Bad credentials provided")
            elif UNKNOWN_ACCOUNT_ERROR in error_message:
                bound_logger.info(
                    "User account does not exist in auth service")
            elif USER_ACCOUNT_DELETED in error_message:
                bound_logger.info("User account is marked for deletion")
            else:
                bound_logger.error(
                    "Unexpected error was returned from Auth service",
                    auth_error=error_message)

            logger.unbind("email")
            return render_template("sign-in/sign-in.html",
                                   form=form,
                                   data={"error": {
                                       "type": "failed"
                                   }})

        bound_logger.info(
            "Successfully found user in auth service.  Attempting to find user in party service"
        )
        party_json = party_controller.get_respondent_by_email(username)
        if not party_json or "id" not in party_json:
            bound_logger.error(
                "Respondent has an account in auth but not in party")
            return render_template("sign-in/sign-in.html",
                                   form=form,
                                   data={"error": {
                                       "type": "failed"
                                   }})
        party_id = party_json["id"]
        bound_logger = bound_logger.bind(party_id=party_id)

        if session.get("next"):
            response = make_response(redirect(session.get("next")))
            session.pop("next")
        else:
            response = make_response(
                redirect(
                    url_for("surveys_bp.get_survey_list",
                            tag="todo",
                            _external=True,
                            _scheme=getenv("SCHEME", "http"))))

        bound_logger.info("Successfully found user in party service")
        bound_logger.info("Creating session")
        redis_session = Session.from_party_id(party_id)
        secure = app.config["WTF_CSRF_ENABLED"]
        response.set_cookie(
            "authorization",
            value=redis_session.session_key,
            expires=redis_session.get_expires_in(),
            secure=secure,
            httponly=secure,
            samesite="strict",
        )
        count = conversation_controller.get_message_count_from_api(
            redis_session)
        redis_session.set_unread_message_total(count)
        bound_logger.info("Successfully created session",
                          session_key=redis_session.session_key)
        bound_logger.unbind("email")
        return response

    account_activated = request.args.get("account_activated", None)
    template_data = {
        "error": {
            "type": form.errors,
            "logged_in": "False"
        },
        "account_activated": account_activated
    }
    return render_template("sign-in/sign-in.html",
                           form=form,
                           data=template_data)