Пример #1
0
def notify_callback():
    notify_data = get_json_from_request()

    if notify_data['status'] == 'permanent-failure':
        user = User.query.filter(
            User.email_address == notify_data['to']
        ).first()

        if user and user.active:
            user.active = False
            db.session.add(user)

            audit_event = AuditEvent(
                audit_type=AuditTypes.update_user,
                user='******',
                data={"user": {"active": False}, "notify_callback_data": notify_data},
                db_object=user,
            )
            db.session.add(audit_event)

            db.session.commit()

            current_app.logger.info(
                "User account disabled for {hashed_email} after Notify reported permanent delivery "
                "failure.".format(hashed_email=hash_string(notify_data['to']))
            )

    elif notify_data['status'] == 'technical-failure':
        current_app.logger.warning("Notify failed to deliver {reference} to {hashed_email}".format(
            reference=notify_data['reference'],
            hashed_email=hash_string(notify_data['to']),
        ))

    return jsonify(status='ok'), 200
def process_login():
    form = LoginForm()
    next_url = request.args.get('next')
    if form.validate_on_submit():
        user_json = data_api_client.authenticate_user(form.email_address.data,
                                                      form.password.data)
        if not user_json:
            current_app.logger.info(
                "login.fail: failed to log in {email_hash}",
                extra={'email_hash': hash_string(form.email_address.data)})
            flash(NO_ACCOUNT_MESSAGE, "error")
            return render_template("auth/login.html",
                                   form=form,
                                   errors=get_errors_from_wtform(form),
                                   next=next_url), 403

        user = User.from_json(user_json)

        login_user(user)
        current_app.logger.info("login.success: role={role} user={email_hash}",
                                extra={
                                    'role':
                                    user.role,
                                    'email_hash':
                                    hash_string(form.email_address.data)
                                })
        return redirect_logged_in_user(next_url)

    else:
        errors = get_errors_from_wtform(form)
        return render_template("auth/login.html",
                               form=form,
                               errors=errors,
                               next=next_url), 400
    def send_email(self,
                   email_address,
                   template_id,
                   personalisation=None,
                   allow_resend=True,
                   reference=None):
        """
        Method to send an email using the Notify api.

        :param email_address: String email address for recipient
        :param template_id: Template accessible on the Notify account whose  api_key you instantiated the class with
        :param personalisation: The template variables, dict
        :param allow_resend: if False instantiate the delivered reference cache and ensure we are not sending duplicates
        :return: response from the api. For more information see https://github.com/alphagov/notifications-python-client
        """
        reference = reference or self.get_reference(email_address, template_id,
                                                    personalisation)
        if not allow_resend and self.has_been_sent(reference):
            self.logger.info(
                "Email {reference} (template {template_id}) has already been sent to {email_address} through Notify",
                extra=dict(
                    email_address=hash_string(email_address),
                    template_id=template_id,
                    reference=reference,
                ),
            )
            return
        try:
            response = self.client.send_email_notification(
                email_address,
                template_id,
                personalisation=personalisation,
                reference=reference,
            )
        except HTTPError as e:
            self.logger.error(
                self.get_error_message(hash_string(email_address), e))
            raise EmailError(str(e))
        self._update_cache(reference)
        self.logger.info(
            "Sent email {reference} to {email_address} (id: {notify_id}, template: {template_id}) through Notify",
            extra=dict(
                email_address=hash_string(email_address),
                notify_id=response['id'],
                template_id=template_id,
                reference=reference,
            ),
        )
        return response
def test_main_catches_email_errors_and_returns_ids(data_api_client,
                                                   notify_client):
    logger = mock.Mock()
    notify_client.send_email.side_effect = [EmailError, True]
    data_api_client.find_brief_responses_iter.side_effect = [
        [_get_dummy_brief_response(4321, AWARDED_BRIEFS[0], awarded=True)],
        [_get_dummy_brief_response(1234, AWARDED_BRIEFS[0], awarded=False)],
    ]

    assert not tested_script.main(data_api_client, notify_client,
                                  'notify_template_id', 'preview', logger)

    assert logger.error.call_args_list == [
        mock.call(
            'Email sending failed for BriefResponse {brief_response_id} (Brief ID {brief_id})',
            extra={
                "brief_id": 123,
                "brief_response_id": 4321
            }),
        mock.call(
            "Email sending failed for the following {count} BriefResponses: {brief_response_ids}",
            extra={
                'brief_response_ids': '4321',
                'count': 1
            })
    ]
    assert logger.info.call_args_list == [
        mock.call(
            "{dry_run}EMAIL: Award of Brief Response ID: {brief_response_id} to {email_address}",
            extra={
                'dry_run': '',
                'brief_response_id': 1234,
                'email_address': hash_string('*****@*****.**'),
            })
    ]
    def send_email(self,
                   to_email_address,
                   template_name_or_id,
                   personalisation=None,
                   allow_resend=True,
                   reference=None,
                   reply_to_address_id=None):
        """
        Method to send an email using the Notify api.

        :param to_email_address: String email address for recipient
        :param template_name_or_id: Template accessible on the Notify account,
                                    can either be a key to the `templates` dictionary or a Notify template ID.
        :param personalisation: The template variables, dict
        :param allow_resend: if False instantiate the delivered reference cache and ensure we are not sending duplicates
        :param reply_to_address_id: String id of reply-to email address. Must be set up in Notify config before use
        :return: response from the api. For more information see https://github.com/alphagov/notifications-python-client
        """
        template_id = self.templates.get(template_name_or_id,
                                         template_name_or_id)
        reference = reference or self.get_reference(
            to_email_address, template_id, personalisation)

        if not allow_resend and self.has_been_sent(reference):
            self.logger.info(
                "Email with reference '{reference}' has already been sent",
                extra=dict(client=self.client.__class__,
                           to_email_address=hash_string(to_email_address),
                           template_name_or_id=template_name_or_id,
                           reference=reference,
                           reply_to_address_id=reply_to_address_id),
            )
            return

        # NOTE how the potential replacement of the email address happens *after* the has_been_sent check and
        # reference generation
        final_email_address = (
            self._redirect_domains_to_address
            and self._redirect_domains_to_address.get(
                # splitting at rightmost @ should reliably give us the domain
                to_email_address.rsplit("@", 1)
                [-1].lower())) or to_email_address

        try:
            with log_external_request(service='Notify'):
                response = self.client.send_email_notification(
                    final_email_address,
                    template_id,
                    personalisation=personalisation,
                    reference=reference,
                    email_reply_to_id=reply_to_address_id)

        except HTTPError as e:
            self._log_email_error_message(to_email_address,
                                          template_name_or_id, reference, e)
            raise EmailError(str(e))

        self._update_cache(reference)

        return response
def generate_token(json_data, secret_key, namespace):
    """
    Encrypt data using a provided secret_key and namespace.

    The process is as follows:
    * The secret_key is combined with the namespace and hashed using sha256. The namespace ensures that a token for
      `invite-user` cannot be re-used by an attacker on the `reset-password` endpoint. Hashing is used to ensure the
      key conforms to the 32 byte length requirement for Fernet.
    * The combined secret key is used to initialise the Fernet encryption algorithm. Fernet is a wrapper around AES
      that provides HMAC, TTL (time to live), and some quality of life features (url-safe base64 encoding)
      The fernet spec can be viewed here: https://github.com/fernet/spec/blob/master/Spec.md
    * The data is dumped from json and encrypted.
    * The output data is returned as a urlsafe_base64 (https://tools.ietf.org/html/rfc4648#section-5) unicode string.

    Fernet acepts and returns bytes, so call `.encode` before and `.decode` after to convert to strings, to ensure we
    use regular python strings for as much of the code flow as possible

    :param json_data: data to encrypt. Must be json-like blob that `json.dumps` will accept
    :param secret_key: The secret key to encrypt with. No length/content restrictions. Must be a string type.
    :param namespace: The namespace to encrypt with. No length/content restrictions. Must be a string type.
    :return: returns a urlsale_base64 encoded encrypted unicode string.
    :rtype: `unicode`
    """
    secret_key = hash_string(secret_key + namespace)
    data = json.dumps(json_data).encode('utf-8')
    f = fernet.Fernet(secret_key.encode('utf-8'))
    return f.encrypt(data).decode('utf-8')
def process_login():
    form = LoginForm()
    next_url = request.args.get('next')
    if form.validate_on_submit():
        user_json = data_api_client.authenticate_user(form.email_address.data,
                                                      form.password.data)
        if not user_json:
            current_app.logger.info(
                "login.fail: failed to log in {email_hash}",
                extra={'email_hash': hash_string(form.email_address.data)})
            errors = govuk_errors({
                "email_address": {
                    "message": "Enter your email address",
                    "input_name": "email_address",
                },
                "password": {
                    "message": "Enter your password",
                    "input_name": "password",
                },
            })
            return render_template(
                "auth/login.html",
                form=form,
                errors=errors,
                error_summary_description_text=NO_ACCOUNT_MESSAGE,
                next=next_url), 403

        user = User.from_json(user_json)

        login_user(user)
        current_app.logger.info("login.success: role={role} user={email_hash}",
                                extra={
                                    'role':
                                    user.role,
                                    'email_hash':
                                    hash_string(form.email_address.data)
                                })
        return redirect_logged_in_user(next_url)

    else:
        errors = get_errors_from_wtform(form)
        return render_template("auth/login.html",
                               form=form,
                               errors=errors,
                               next=next_url), 400
Пример #8
0
    def __call__(self, form, field):
        user_json = data_api_client.authenticate_user(
            current_user.email_address, field.data)

        if user_json is None:
            current_app.logger.info(
                "change_password.fail: failed to authenticate user {email_hash}",
                extra={'email_hash': hash_string(current_user.email_address)})

            raise ValidationError(self.message)
Пример #9
0
    def get_reference(to_email_address, template_id, personalisation=None):
        """
        Method to return the standard reference given the variables the email is sent with.

        :param to_email_address: Emails recipient
        :param template_id: Emails template ID on Notify
        :param personalisation: Template parameters
        :return: Hashed string 'reference' to be passed to client.send_email_notification or self.send_email
        """
        personalisation_string = ",".join(map(
            str, personalisation.values())) if personalisation else ""
        return hash_string(
            f"{to_email_address}|{template_id}|{personalisation_string}")
Пример #10
0
def notify_callback():
    notify_data = get_json_from_request()

    if notify_data['status'] == 'permanent-failure':
        user = User.query.filter(
            User.email_address == notify_data['to']).first()

        if user and user.active:
            user.active = False
            db.session.add(user)

            audit_event = AuditEvent(
                audit_type=AuditTypes.update_user,
                user='******',
                data={
                    "user": {
                        "active": False
                    },
                    "notify_callback_data": notify_data
                },
                db_object=user,
            )
            db.session.add(audit_event)

            db.session.commit()

            current_app.logger.info(
                "User account disabled for {hashed_email} after Notify reported permanent delivery "
                "failure.".format(hashed_email=hash_string(notify_data['to'])))

    elif notify_data['status'] == 'technical-failure':
        current_app.logger.warning(
            "Notify failed to deliver {reference} to {hashed_email}".format(
                reference=notify_data['reference'],
                hashed_email=hash_string(notify_data['to']),
            ))

    return jsonify(status='ok'), 200
def notify_callback():
    notify_data = get_json_from_request()

    email_address = notify_data["to"]
    hashed_email = hash_string(email_address)
    reference = notify_data["reference"]
    status = notify_data["status"]

    # remove PII from response for logging
    # according to docs only "to" has PII
    # https://docs.notifications.service.gov.uk/rest-api.html#delivery-receipts
    clean_notify_data = notify_data.copy()
    del clean_notify_data["to"]

    current_app.logger.info(
        f"Notify callback: {status}: {reference} to {hashed_email}",
        extra={"notify_delivery_receipt": clean_notify_data},
    )

    if status == "permanent-failure":
        user = User.query.filter(User.email_address == email_address).first()

        if user and user.active:
            user.active = False
            db.session.add(user)

            audit_event = AuditEvent(
                audit_type=AuditTypes.update_user,
                user='******',
                data={
                    "user": {
                        "active": False
                    },
                    "notify_callback_data": notify_data
                },
                db_object=user,
            )
            db.session.add(audit_event)

            db.session.commit()

            current_app.logger.info(
                f"User account disabled for {hashed_email} after Notify reported permanent delivery "
                "failure.")

    elif status.endswith("failure"):
        current_app.logger.warning(
            f"Notify failed to deliver {reference} to {hashed_email}")

    return jsonify(status='ok'), 200
def log_email_error(exception, email_type, error_code, email_address):
    """
    Log errors in a separate module so we can patch `current_app.logger` in tests
    and assert the calls, without affecting the test client.
    """
    current_app.logger.error(
        "{code}: {email_type} email for email_hash {email_hash} failed to send. "
        "Error: {error}",
        extra={
            'email_hash': hash_string(email_address),
            'error': str(exception),
            'code': '{}'.format(error_code),
            'email_type': email_type
        })
    def get_reference(to_email_address, template_id, personalisation=None):
        """
        Method to return the standard reference given the variables the email is sent with.

        :param to_email_address: Emails recipient
        :param template_id: Emails template ID on Notify
        :param personalisation: Template parameters
        :return: Hashed string 'reference' to be passed to client.send_email_notification or self.send_email
        """
        personalisation_string = u','.join(
            list(map(lambda x: u'{}'.format(x),
                     personalisation.values()))) if personalisation else u''
        details_string = u'|'.join(
            [to_email_address, template_id, personalisation_string])
        return hash_string(details_string)
 def _log(self,
          lvl,
          msg,
          email_obj: DMNotifyEmail,
          *,
          extra: Optional[dict] = None,
          **kwargs):
     if extra is None:
         extra = {}
     extra.update({
         "client":
         self.__class__,
         "reference":
         email_obj.reference,
         "template_name_or_id":
         email_obj.template_name_or_id,
         "to_email_address":
         hash_string(email_obj.to_email_address),
     })
     self.logger.log(lvl, msg, extra)
    def _log_email_error_message(self, to_email_address, template_name_or_id,
                                 reference, error):
        """Format a logical error message from the error response and send it to the logger"""

        error_messages = []
        for error_message in error.message:
            error_messages.append("{status_code} {error}: {message}".format(
                status_code=error.status_code,
                error=error_message["error"],
                message=error_message["message"],
            ))

        self.logger.error(
            "Error sending email: {error_messages}",
            extra={
                "client": self.__class__,
                "reference": reference,
                "template_name_or_id": template_name_or_id,
                "to_email_address": hash_string(to_email_address),
                "error_messages": error_messages,
            },
        )
Пример #16
0
def main(data_api_client,
         mail_client,
         template_id,
         stage,
         logger,
         withdrawn_date,
         brief_id=None,
         dry_run=False):

    withdrawn_briefs = data_api_client.find_briefs_iter(
        status='withdrawn', withdrawn_on=withdrawn_date)

    if brief_id:
        withdrawn_briefs = filter(lambda i: i['id'] == brief_id,
                                  withdrawn_briefs)

    for brief in withdrawn_briefs:
        email_addresses = get_brief_response_emails(data_api_client,
                                                    brief['id'])
        if not email_addresses:
            continue

        brief_email_context = create_context_for_brief(stage, brief)
        for email_address in email_addresses:
            if not dry_run:
                mail_client.send_email(email_address,
                                       template_id,
                                       brief_email_context,
                                       allow_resend=False)
            logger.info(
                "%sEMAIL: Withdrawal of Brief ID: %d to %s",
                '[Dry-run]' if dry_run else '',
                brief['id'],
                hash_string(email_address),
            )
    return True
def decode_token(encrypted_data,
                 secret_key,
                 namespace,
                 max_age_in_seconds=ONE_DAY_IN_SECONDS):
    """
    Decrypt data using a provided secret_key, namespace, and TTL (max_age_in_seconds).

    The process is as follows:
    * secret key and fernet are initialised as in `encrypt_data`
    * the data is decrypted and json_dumped
    * the timestamp is pulled out and compared to the max_age_in_seconds to ensure that the token has not expired
    * the original json-able blob is returned, along with the timestamp that it was encrypted at. This timestamp can
      then be used for verifying the authenticity of the request, for example, comparing a password reset token
      against the last time the password was reset to ensure it is not used twice.

    Fernet acepts and returns bytes, so call `.encode` before and `.decode` after to convert to strings, to ensure we
    use regular python strings for as much of the code flow as possible

    :param encrypted_data: data to decrypt. Must be a string type.
    :param secret_key: The secret key you encrypted the data with. Must be a string type.
    :param namespace: The namespace you encrypted the data with. Must be a string type.
    :param max_age_in_seconds: The maximum age of the encrypted data.
    :return: the original encrypted data and the datetime it was encrypted at.
    :rtype: `tuple(json-able, datetime)`
    :raises fernet.InvalidToken: If the secret key and namespace are not able to decrypt the message,
        or max_age_in_seconds has been exceeded.
    """
    encrypted_bytes = encrypted_data.encode('utf-8')
    secret_key = hash_string(secret_key + namespace)
    f = fernet.Fernet(secret_key.encode('utf-8'))

    # this raises fernet.InvalidToken if the key does not match or if TTL is exceeded
    data = f.decrypt(encrypted_bytes, ttl=max_age_in_seconds)

    timestamp = _parse_fernet_timestamp(encrypted_bytes)
    return json.loads(data.decode('utf-8')), timestamp
def _build_and_send_emails(brief_responses, mail_client, stage, dry_run,
                           template_id, logger):
    failed_brief_responses = []

    # Now email everyone whose Brief got awarded
    for brief_response in brief_responses:
        # Although we don't officially support it, in some cases users
        # have multiple email addresses in their respondToEmailAddress field
        email_addresses = get_email_addresses(
            brief_response["respondToEmailAddress"])

        invalid_email_addresses = [
            a for a in email_addresses if not validate_email_address(a)
        ]
        if invalid_email_addresses:
            logger.error(
                "Invalid email address(es) for BriefResponse {brief_response_id} (Brief ID {brief_id})",
                extra={
                    "brief_id": brief_response['brief']['id'],
                    "brief_response_id": brief_response['id'],
                })
            failed_brief_responses.append(brief_response['id'])

        for invalid_email_address in invalid_email_addresses:
            email_addresses.remove(invalid_email_address)

        if not email_addresses:
            logger.error(
                "No valid email address(es) for BriefResponse {brief_response_id} (Brief ID {brief_id})",
                extra={
                    "brief_id": brief_response['brief']['id'],
                    "brief_response_id": brief_response['id'],
                })
            continue

        brief_email_context = _create_context_for_brief(
            stage, brief_response['brief'])
        for email_address in email_addresses:
            try:
                if not dry_run:
                    mail_client.send_email(email_address,
                                           template_id,
                                           brief_email_context,
                                           allow_resend=False)
                logger.info(
                    "{dry_run}EMAIL: Award of Brief Response ID: {brief_response_id} to {email_address}",
                    extra={
                        'dry_run': '[Dry-run] - ' if dry_run else '',
                        'brief_response_id': brief_response['id'],
                        'email_address': hash_string(email_address),
                    })
            except EmailError as e:
                # Log individual failures in more detail
                logger.error(
                    "Email sending failed for BriefResponse {brief_response_id} (Brief ID {brief_id})",
                    extra={
                        "brief_id": brief_response['brief']['id'],
                        "brief_response_id": brief_response['id']
                    })

                if isinstance(e, EmailTemplateError):
                    raise  # do not try to continue

                failed_brief_responses.append(brief_response['id'])

    return failed_brief_responses
Пример #19
0
def send_brief_clarification_question(data_api_client, brief,
                                      clarification_question):
    questions_url = (
        get_web_url_from_stage(current_app.config["DM_ENVIRONMENT"]) +
        url_for('external.supplier_questions',
                framework_slug=brief["framework"]['slug'],
                lot_slug=brief["lotSlug"],
                brief_id=brief["id"]))

    notify_client = DMNotifyClient(current_app.config['DM_NOTIFY_API_KEY'])

    # Email the question to brief owners
    for email_address in get_brief_user_emails(brief):
        try:
            notify_client.send_email(
                email_address,
                template_name_or_id=current_app.config['NOTIFY_TEMPLATES']
                ['clarification_question'],
                personalisation={
                    "brief_title":
                    brief['title'],
                    "brief_name":
                    brief['title'],
                    "message":
                    escape(clarification_question),
                    "publish_by_date":
                    dateformat(brief['clarificationQuestionsPublishedBy']),
                    "questions_url":
                    questions_url
                },
                reference="clarification-question-{}".format(
                    hash_string(email_address)))
        except EmailError as e:
            current_app.logger.error(
                "Brief question email failed to send. error={error} supplier_id={supplier_id} brief_id={brief_id}",
                extra={
                    'error': six.text_type(e),
                    'supplier_id': current_user.supplier_id,
                    'brief_id': brief['id']
                })

            abort(503, "Clarification question email failed to send")

    data_api_client.create_audit_event(
        audit_type=AuditTypes.send_clarification_question,
        user=current_user.email_address,
        object_type="briefs",
        object_id=brief['id'],
        data={
            "question": clarification_question,
            "briefId": brief['id'],
            "supplierId": current_user.supplier_id
        })

    brief_url = (get_web_url_from_stage(current_app.config["DM_ENVIRONMENT"]) +
                 url_for('external.get_brief_by_id',
                         framework_family=brief['framework']['family'],
                         brief_id=brief['id']))

    # Send the supplier a copy of the question
    try:
        notify_client.send_email(
            current_user.email_address,
            template_name_or_id=current_app.config["NOTIFY_TEMPLATES"]
            ["clarification_question_confirmation"],
            personalisation={
                "brief_name": brief['title'],
                "message": escape(clarification_question),
                "brief_url": brief_url,
            },
            reference="clarification-question-confirmation-{}".format(
                hash_string(current_user.email_address)))
    except EmailError as e:
        current_app.logger.error(
            "Brief question supplier email failed to send. error={error} supplier_id={supplier_id} brief_id={brief_id}",
            extra={
                'error': six.text_type(e),
                'supplier_id': current_user.supplier_id,
                'brief_id': brief['id']
            })
Пример #20
0
 def test_hash_string(self, test, expected):
     expected = expected.decode('utf-8')
     result = hash_string(test)
     assert result == expected
Пример #21
0
def send_email(to_email_addresses,
               email_body,
               api_key,
               subject,
               from_email,
               from_name,
               tags,
               reply_to=None,
               metadata=None,
               logger=None):
    logger = logger or current_app.logger

    if isinstance(to_email_addresses, string_types):
        to_email_addresses = [to_email_addresses]

    try:
        mandrill_client = Mandrill(api_key)

        message = {
            'html':
            email_body,
            'subject':
            subject,
            'from_email':
            from_email,
            'from_name':
            from_name,
            'to': [{
                'email': email_address,
                'type': 'to'
            } for email_address in to_email_addresses],
            'important':
            False,
            'track_opens':
            False,
            'track_clicks':
            False,
            'auto_text':
            True,
            'tags':
            tags,
            'metadata':
            metadata,
            'headers': {
                'Reply-To': reply_to or from_email
            },
            'preserve_recipients':
            False,
            'recipient_metadata': [{
                'rcpt': email_address
            } for email_address in to_email_addresses]
        }

        result = mandrill_client.messages.send(message=message, async=True)
    except Error as e:
        # Mandrill errors are thrown as exceptions
        logger.error("Failed to send an email: {error}", extra={'error': e})
        raise EmailError(e)
    logger.info("Sent {tags} response: id={id}, email={email_hash}",
                extra={
                    'tags': tags,
                    'id': result[0]['_id'],
                    'email_hash': hash_string(result[0]['email'])
                })
Пример #22
0
def change_password():
    form = PasswordChangeForm()
    dashboard_url = get_user_dashboard_url(current_user)

    # Checking that the old password is correct is done as a validator on the form.
    if form.validate_on_submit():
        response = data_api_client.update_user_password(
            current_user.id,
            form.password.data,
            updater=current_user.email_address)
        if response:
            current_app.logger.info(
                "User {user_id} successfully changed their password",
                extra={'user_id': current_user.id})

            notify_client = DMNotifyClient(
                current_app.config['DM_NOTIFY_API_KEY'])

            token = generate_token(
                {"user": current_user.id},
                current_app.config['SHARED_EMAIL_KEY'],
                current_app.config['RESET_PASSWORD_TOKEN_NS'])

            try:
                notify_client.send_email(
                    current_user.email_address,
                    template_name_or_id=current_app.config['NOTIFY_TEMPLATES']
                    ['change_password_alert'],
                    personalisation={
                        'url':
                        url_for('main.reset_password',
                                token=token,
                                _external=True),
                    },
                    reference='change-password-alert-{}'.format(
                        hash_string(current_user.email_address)))

                current_app.logger.info(
                    "{code}: Password change alert email sent for email_hash {email_hash}",
                    extra={
                        'email_hash': hash_string(current_user.email_address),
                        'code': 'login.password-change-alert-email.sent'
                    })

            except EmailError as exc:
                log_email_error(
                    exc, "Password change alert",
                    "login.password-change-alert-email.notify-error",
                    current_user.email_address)

            flash(PASSWORD_UPDATED_MESSAGE, "success")
        else:
            flash(PASSWORD_NOT_UPDATED_MESSAGE, "error")
        return redirect(dashboard_url)

    errors = get_errors_from_wtform(form)
    return render_template(
        "auth/change-password.html",
        form=form,
        errors=errors,
        dashboard_url=dashboard_url), 200 if not errors else 400
Пример #23
0
def update_user(user_id):
    """
        Update a user. Looks user up in DB, and updates where necessary.
    """
    update_details = validate_and_return_updater_request()

    user = User.query.filter(User.id == user_id).first_or_404()

    json_payload = get_json_from_request()
    json_has_required_keys(json_payload, ["users"])
    user_update = json_payload["users"]

    json_has_matching_id(user_update, user_id)

    if 'password' in user_update:
        user.password = encryption.hashpw(user_update['password'])
        user.password_changed_at = datetime.utcnow()
        user.failed_login_count = 0
        user_update['password'] = '******'
        if user.role in ('admin-manager', ):
            current_app.logger.warning(
                "{code}: Password reset requested for {user_role} user '{email_hash}'",
                extra={
                    "code": "update_user.password.role_warning",
                    "email_hash": hash_string(user.email_address),
                    "user_role": user.role,
                })
    if 'active' in user_update:
        user.active = user_update['active']
    if 'name' in user_update:
        user.name = user_update['name']
    if 'emailAddress' in user_update:
        user.email_address = user_update['emailAddress']
    if 'phoneNumber' in user_update:
        user.phone_number = user_update['phoneNumber']
    if 'role' in user_update:
        if user.role == 'supplier' and user_update['role'] != user.role:
            user.supplier_id = None
            user_update.pop('supplierId', None)
        if user.role == 'buyer' and user_update['role'] != user.role:
            abort(400, "Can not change a 'buyer' user to a different role.")
        user.role = user_update['role']
    if 'supplierId' in user_update:
        user.supplier_id = user_update['supplierId']
    if 'locked' in user_update and not user_update['locked']:
        user.failed_login_count = 0
    if 'userResearchOptedIn' in user_update:
        user.user_research_opted_in = user_update['userResearchOptedIn']

    check_supplier_role(user.role, user.supplier_id)

    audit = AuditEvent(audit_type=AuditTypes.update_user,
                       user=update_details.get('updated_by', 'no user data'),
                       data={
                           'user': user.email_address,
                           'update': user_update
                       },
                       db_object=user)

    db.session.add(user)
    db.session.add(audit)

    try:
        db.session.commit()
        return single_result_response(RESOURCE_NAME, user), 200
    except (IntegrityError, DataError):
        db.session.rollback()
        abort(400, "Could not update user with: {0}".format(user_update))
def _build_and_send_emails(brief_responses, mail_client, stage, dry_run, template_id, logger):
    failed_brief_responses = []

    # Now email everyone whose Brief got awarded
    for brief_response in brief_responses:
        email_addresses = get_email_addresses(brief_response["respondToEmailAddress"])

        # Users can enter multiple email addresses, but our input validation is very basic. So it's OK if some of the
        # addresses are invalid, as long as there is at least one valid address.
        invalid_email_addresses = [a for a in email_addresses if not validate_email_address(a)]
        if invalid_email_addresses:
            logger.warning(
                "Invalid email address(es) for BriefResponse {brief_response_id} (Brief ID {brief_id})",
                extra={
                    "brief_id": brief_response['brief']['id'],
                    "brief_response_id": brief_response['id'],
                }
            )

        for invalid_email_address in invalid_email_addresses:
            email_addresses.remove(invalid_email_address)

        if not email_addresses:
            logger.error(
                "No valid email address(es) for BriefResponse {brief_response_id} (Brief ID {brief_id})",
                extra={
                    "brief_id": brief_response['brief']['id'],
                    "brief_response_id": brief_response['id'],
                }
            )
            continue

        brief_email_context = _create_context_for_brief(stage, brief_response['brief'])
        for email_address in email_addresses:
            try:
                if not dry_run:
                    mail_client.send_email(
                        email_address, template_id, brief_email_context, allow_resend=False
                    )
                logger.info(
                    "{dry_run}EMAIL: Award of Brief Response ID: {brief_response_id} to {email_address}",
                    extra={
                        'dry_run': '[Dry-run] - ' if dry_run else '',
                        'brief_response_id': brief_response['id'],
                        'email_address': hash_string(email_address),
                    }
                )
            except EmailError as e:
                # Log individual failures in more detail
                logger.error(
                    "Email sending failed for BriefResponse {brief_response_id} (Brief ID {brief_id})",
                    extra={
                        "brief_id": brief_response['brief']['id'],
                        "brief_response_id": brief_response['id']
                    }
                )

                if isinstance(e, EmailTemplateError):
                    raise  # do not try to continue

                failed_brief_responses.append(brief_response['id'])

    return failed_brief_responses
Пример #25
0
def send_reset_password_email():
    form = EmailAddressForm()
    if form.validate_on_submit():
        email_address = form.email_address.data
        user_json = data_api_client.get_user(email_address=email_address)
        notify_client = DMNotifyClient(current_app.config['DM_NOTIFY_API_KEY'])

        if user_json is not None:
            user = User.from_json(user_json)
            if user.role in ("admin-manager", ):
                # if this user wants their password reset they'll have to come to us
                current_app.logger.warning(
                    "{code}: Password reset requested for {user_role} user '{email_hash}'",
                    extra={
                        "code": "login.reset-email.bad-role",
                        "email_hash": hash_string(user.email_address),
                        "user_role": user.role,
                    })

            elif user.active:  # specifically checking just .active, ignoring whether account is "locked"
                token = generate_token(
                    {"user": user.id}, current_app.config['SHARED_EMAIL_KEY'],
                    current_app.config['RESET_PASSWORD_TOKEN_NS'])

                try:
                    notify_client.send_email(
                        user.email_address,
                        template_name_or_id=current_app.
                        config['NOTIFY_TEMPLATES']['reset_password'],
                        personalisation={
                            'url':
                            url_for('main.reset_password',
                                    token=token,
                                    _external=True),
                        },
                        reference='reset-password-{}'.format(
                            hash_string(user.email_address)),
                    )
                except EmailError as exc:
                    log_email_error(
                        exc,
                        "Password reset",
                        "login.reset-email.notify-error",
                        user.email_address,
                    )
                    abort(503, "Failed to send password reset email.")

                current_app.logger.info(
                    "{code}: Sent password reset email for email_hash {email_hash}",
                    extra={
                        'email_hash': hash_string(user.email_address),
                        'code': 'login.reset-email.sent'
                    })
            else:
                try:
                    notify_client.send_email(
                        user.email_address,
                        template_name_or_id=current_app.
                        config['NOTIFY_TEMPLATES']['reset_password_inactive'],
                        reference='reset-password-inactive-{}'.format(
                            hash_string(user.email_address)),
                    )
                except EmailError as exc:
                    log_email_error(
                        exc,
                        "Password reset (inactive user)",
                        "login.reset-email-inactive.notify-error",
                        user.email_address,
                    )
                    abort(503, "Failed to send password reset email.")

                current_app.logger.warning(
                    "{code}: Sent password (non-)reset email for inactive user email_hash {email_hash}",
                    extra={
                        'email_hash': hash_string(user.email_address),
                        'code': 'login.reset-email-inactive.sent',
                    })
        else:
            # Send a email to the Notify sandbox using the 'inactive' template, to mitigate any timing attacks (where
            # a user's existence could be determined by the response time of this view). Any errors are also handled
            # in the same way as for inactive users.
            try:
                notify_client.send_email(
                    NOTIFY_SANDBOX_ADDRESS,
                    template_name_or_id=current_app.config['NOTIFY_TEMPLATES']
                    ['reset_password_inactive'],
                    reference='reset-password-nonexistent-user-{}'.format(
                        hash_string(email_address)),
                )
            except EmailError as exc:
                log_email_error(
                    exc,
                    "Password reset (non-existent user)",
                    "login.reset-email-nonexistent.notify-error",
                    email_address,  # Hashed by the helper function
                )
                abort(503, "Failed to send password reset email.")

            current_app.logger.info(
                "{code}: Sent password (non-)reset email for invalid user email_hash {email_hash}",
                extra={
                    'email_hash': hash_string(email_address),
                    'code': 'login.reset-email.invalid-email'
                })

        flash(
            EMAIL_SENT_MESSAGE.format(
                support_email=current_app.config['SUPPORT_EMAIL_ADDRESS']),
            "success")
        return redirect(url_for('.request_password_reset'))
    else:
        return render_template("auth/request-password-reset.html",
                               errors=get_errors_from_wtform(form),
                               form=form), 400
Пример #26
0
def notify_suppliers_of_framework_application_event(
    data_api_client: DataAPIClient,
    notify_client: DMNotifyClient,
    notify_template_id: str,
    framework_slug: str,
    stage: str,
    dry_run: bool,
    logger: Logger,
    run_id: Optional[UUID] = None,
) -> int:
    run_is_new = not run_id
    run_id = run_id or uuid4()
    logger.info(
        f"{'Starting' if run_is_new else 'Resuming'} run id {{run_id}}",
        extra={"run_id": str(run_id)})

    framework = data_api_client.get_framework(framework_slug)["frameworks"]
    framework_context = {
        "framework_name":
        framework["name"],
        "updates_url":
        f"{get_web_url_from_stage(stage)}/suppliers/frameworks/{framework['slug']}/updates",
        "framework_dashboard_url":
        f"{get_web_url_from_stage(stage)}/suppliers/frameworks/{framework['slug']}/",
        "clarification_questions_closed":
        "no" if framework["clarificationQuestionsOpen"] else "yes",
        **_formatted_dates_from_framework(framework),
    }

    failure_count = 0

    for supplier_framework in data_api_client.find_framework_suppliers_iter(
            framework_slug):
        for user in data_api_client.find_users_iter(
                supplier_id=supplier_framework["supplierId"]):
            if user["active"]:
                # generating ref separately so we can exclude certain parameters from the context dict
                notify_ref = notify_client.get_reference(
                    user["emailAddress"],
                    notify_template_id,
                    {
                        "framework_slug": framework["slug"],
                        "run_id": str(run_id),
                    },
                )
                if dry_run:
                    # Use the sent references cache unless we're re-running the script following a failure
                    if notify_client.has_been_sent(
                            notify_ref, use_recent_cache=run_is_new):
                        logger.debug(
                            "[DRY RUN] Would NOT send notification to {email_hash} (already sent)",
                            extra={
                                "email_hash": hash_string(user["emailAddress"])
                            },
                        )
                    else:
                        logger.info(
                            "[DRY RUN] Would send notification to {email_hash}",
                            extra={
                                "email_hash": hash_string(user["emailAddress"])
                            },
                        )
                else:
                    try:
                        # Use the sent references cache unless we're re-running the script following a failure
                        notify_client.send_email(
                            user["emailAddress"],
                            notify_template_id,
                            framework_context,
                            allow_resend=False,
                            reference=notify_ref,
                            use_recent_cache=run_is_new,
                        )
                    except EmailError as e:
                        failure_count += 1
                        logger.error(
                            "Failed sending to {email_hash}: {e}",
                            extra={
                                "email_hash":
                                hash_string(user["emailAddress"]),
                                "e": str(e),
                            },
                        )

                        if isinstance(e, EmailTemplateError):
                            raise  # do not try to continue

    return failure_count