diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index c308cd71a..e1d76775a 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -55,6 +55,13 @@ jobs: NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }} LOGIN_PEM: ${{ secrets.LOGIN_PEM }} + LOGIN_DOT_GOV_CLIENT_ID: "urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov" + LOGIN_DOT_GOV_USER_INFO_URL: "https://idp.int.identitysandbox.gov/api/openid_connect/userinfo" + LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://idp.int.identitysandbox.gov/api/openid_connect/token" + LOGIN_DOT_GOV_LOGOUT_URL: "https://idp.int.identitysandbox.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://notify-demo.app.cloud.gov/sign-out" + LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://idp.int.identitysandbox.gov/openid_connect/logout?" + LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-demo.app.cloud.gov/sign-out" + LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://idp.int.identitysandbox.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=01234567890123456789012345&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=abcdefghijklmnopabcdefghijklmnop" with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -69,6 +76,13 @@ jobs: --var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY" --var NR_BROWSER_KEY="$NR_BROWSER_KEY" --var LOGIN_PEM="$LOGIN_PEM" + --var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID" + --var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL" + --var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL" + --var LOGIN_DOT_GOV_LOGOUT_URL="$LOGIN_DOT_GOV_LOGOUT_URL" + --var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL" + --var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT" + --var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL" - name: Check for changes to egress config id: changed-egress-config diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 3c1ab8b8a..a0aaa81c5 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -55,7 +55,13 @@ jobs: NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }} LOGIN_PEM: ${{ secrets.LOGIN_PEM }} - + LOGIN_DOT_GOV_CLIENT_ID: "urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov" + LOGIN_DOT_GOV_USER_INFO_URL: "https://secure.login.gov/api/openid_connect/userinfo" + LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://secure.login.gov/api/openid_connect/token" + LOGIN_DOT_GOV_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://beta.notify.gov/sign-out" + LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?" + LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://beta.notify.gov/sign-out" + LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=01234567890123456789012345&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=abcdefghijklmnopabcdefghijklmnop" with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -70,6 +76,13 @@ jobs: --var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY" --var NR_BROWSER_KEY="$NR_BROWSER_KEY" --var LOGIN_PEM="$LOGIN_PEM" + --var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID" + --var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL" + --var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL" + --var LOGIN_DOT_GOV_LOGOUT_URL="$LOGIN_DOT_GOV_LOGOUT_URL" + --var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL" + --var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT" + --var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL" - name: Check for changes to egress config id: changed-egress-config diff --git a/app/__init__.py b/app/__init__.py index af9b3f9a9..3f008f7a1 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -21,7 +21,6 @@ from flask_login import LoginManager, current_user from flask_talisman import Talisman from flask_wtf import CSRFProtect from flask_wtf.csrf import CSRFError -from govuk_frontend_jinja.flask_ext import init_govuk_frontend from itsdangerous import BadSignature from notifications_python_client.errors import HTTPError from notifications_utils import logging, request_helper @@ -114,6 +113,7 @@ from app.notify_client.template_statistics_api_client import template_statistics from app.notify_client.upload_api_client import upload_api_client from app.notify_client.user_api_client import user_api_client from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter +from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend login_manager = LoginManager() csrf = CSRFProtect() diff --git a/app/assets/images/product/security-review-message.png b/app/assets/images/product/security-review-message.png new file mode 100644 index 000000000..ffb59076c Binary files /dev/null and b/app/assets/images/product/security-review-message.png differ diff --git a/app/content/get-started.md b/app/content/get-started.md index 7846c0fe6..47757b2c1 100644 --- a/app/content/get-started.md +++ b/app/content/get-started.md @@ -1,43 +1,21 @@ # Get started -1. ## Check if Notify.gov is right for you -

Read about our features, pricing and roadmap.

+1. ## Everyone starts in [Trial Mode](/using-notify/trial-mode) +Explore Notify, add team members, and practice [sending messages to teammates](/using-notify/trial-mode). +2. ## Personalize content +Learn how to [personalize messages](/using-notify/guidance) to increase response. -2. ## Create an account - {% if not current_user.is_authenticated %} -

Create an account for free and add your first Notify service. When you add a new service it will start in trial mode.

- {% else %} -

Create an account for free and add your first Notify service. When you add a new service, it will start in trial mode.

- {% endif %} - -3. ## Write some messages - {% if True %} -

Add message templates with examples of the content you plan to send. You can use our guidance to help you.

- {% endif %} - -4. ## Set up your service - {% if not current_user.is_authenticated or not current_service %} -

Review your settings to add message branding and sender information.

-

Add team members and check their permissions.

- {% else %} -

Review your settings to add message branding and sender information.

-

Add team members and check their permissions.

- {% endif %} +3. ## Check delivery status +[Analyze the delivery](/using-notify/delivery-status) of your messages and download reports +4. ## View messaging limits +Learn about message _parts_ and [how limits are calculated](/using-notify/pricing). 5. ## Start sending messages - {% if not current_user.is_authenticated or not current_service %} -

When you’re ready to send messages to people outside your team, go to the Settings page and select Request to go live. We’ll approve your request within one working day.

- {% else %} -

You should request to go live when you’re ready to send messages to people outside your team. We’ll approve your request within one working day.

- {% endif %} +To remove the restrictions of Trial Mode and begin sending messages to clients complete the [Live Campaign Form](https://airtable.com/appe4n7jYOALPLcyU/shrIPWnLTw9U1fclL). +We’ll respond within one business day. - - - +### Questions? +[Contact the Notify team](/support) diff --git a/app/main/views/notifications.py b/app/main/views/notifications.py index 019bdfb4d..ac05e05ff 100644 --- a/app/main/views/notifications.py +++ b/app/main/views/notifications.py @@ -90,7 +90,7 @@ def view_notification(service_id, notification_id, error_message=None): partials=get_single_notification_partials(notification), created_by=notification.get("created_by"), created_at=notification["created_at"], - updated_at=notification["updated_at"], + updated_at=notification["sent_at"], help=get_help_argument(), notification_id=notification["id"], can_receive_inbound=(current_service.has_permission("inbound_sms")), diff --git a/app/main/views/send.py b/app/main/views/send.py index 96389898b..a07a9009e 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -953,14 +953,20 @@ def send_notification(service_id, template_id): service_id, job_id=upload_id, include_one_off=True ) attempts = 0 - while notifications["total"] == 0 and attempts < 5: + + # The response can come back in different forms of incompleteness + while ( + notifications["total"] == 0 + and notifications["notifications"] == [] + and attempts < 50 + ): notifications = notification_api_client.get_notifications_for_service( service_id, job_id=upload_id, include_one_off=True ) time.sleep(0.1) attempts = attempts + 1 - if notifications["total"] == 0 and attempts == 5: + if notifications["total"] == 0 and attempts == 50: # This shows the job we auto-generated for the user return redirect( url_for( diff --git a/app/templates/components/table.html b/app/templates/components/table.html index 803030e54..50eb73d9a 100644 --- a/app/templates/components/table.html +++ b/app/templates/components/table.html @@ -172,7 +172,7 @@

{{ notification.status|format_notification_status_as_time( notification.created_at|format_datetime_short, - (notification.updated_at or notification.created_at)|format_datetime_short + (notification.sent_at or notification.created_at)|format_datetime_short ) }}

{% if displayed_on_single_line %}{% endif %} diff --git a/app/templates/views/guidance/index.html b/app/templates/views/guidance/index.html index 7d43f81c3..e754ef245 100644 --- a/app/templates/views/guidance/index.html +++ b/app/templates/views/guidance/index.html @@ -11,115 +11,229 @@

Guidance

-

This guidance is for teams using Notify.gov to send text messages.

- -

It explains how to:

+

Notify allows you easily to create templates for messages for your recipients. You can customize messages to encourage + your recipient to manage their benefits and increase follow through.

+

Below we explain how to:

-

Edit and format messages

+ {# Format content #} +

Format your content

+

Effective texts will help your message recipients take the steps needed to secure and keep the benefits and services + they depend on. To craft an effective text:

-

This section explains how to:

-

Format your content

- -

You can see a list of formatting instructions on the edit template page:

- -
    -
  1. Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.
  2. -
  3. Add a new template or choose an existing template and select Edit.
  4. +

    To create and format your message

    +
      +
    1. All messages start from a template
    2. +
    3. Click “Send Messages”. You’ll see existing templates.
    4. +
    5. Add a new template or choose an existing template and select Edit.
    - - -

    When composing a text message, write URLs in full and Notify will convert them into links for you. -

    - -

    You cannot convert text into a link. - -

    - -

    We do not recommend using a third-party link shortening service because:

    + {# Add links #} + +

    When composing a text message, links to websites or online applications can help your recipient respond quickly.

    -

    Personalize your content

    +

    About link-shortening services

    +

    We do not recommend using a third-party link-shortening service because:

    + -

    To personalize the content of your messages, add a placeholder to the template.

    + {# Personalize content #} +

    Personalize your content

    +

    Personalizing your content can increase response rates and help the recipient know the text is legitimate.

    + -

    Placeholders are filled in with details, like a name or reference number, each time you send a - message.

    - -

    To add a placeholder to the template:

    - -
      -
    1. Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.
    2. -
    3. Add a new template or choose an existing template and select Edit.
    4. -
    5. Add a placeholder using double brackets. For example: Hello ((first name)), your reference is - ((ref number)).
    6. -
    7. Select Save.
    8. +

      To personalize your content

      +
        +
      1. Add a placeholder to your content by placing two brackets around the personalized elements.
      2. +
      3. You can manually enter the personalized content or you can upload a spreadsheet with the details and let Notify do the + work for you. See data preparation.
      -

      When you send a message you can either:

      +

      Example

      +

      To personalize with the recipient's first name and include a reference number:

      +

      State WIC: Hello ((first name)), your reference is ((ref number)). Please provide this number when you call 123-123-1234 to make an appointment.

      - + {# Add conditional content #} +

      Add conditional content

      +

      Conditional (or optional) content appears only when a recipient meets certain criteria. This feature allows you to make + all or part of the message contingent upon specific criteria associated with the recipient.

      -

      If you upload a list, the column names need to match the placeholders in your template.

      - -

      Add optional content

      - -

      To add optional content to your messages:

      - -
        -
      1. Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.
      2. -
      3. Add a new template or choose an existing template and select Edit.
      4. -
      5. Use double brackets and ?? to define optional content. For example, if you only want to show something to people - who are under 18: ((under18??Please get your application signed by a parent or guardian.))
      6. -
      7. Select Save.
      8. +

        To add conditional content

        +
          +
        1. Use two brackets and ?? to define the conditional content.
        2. +
        3. You can manually enter the conditional content or you can upload a spreadsheet with the personal details and let Notify + do the work for you. See data preparation.
        -

        For each person you send this message to, specify ‘yes’ or ‘no’ to show or hide this content. You - can either:

        +

        Examples

        +
          +
        1. If you only want to show something to people who are under 18: +
          +

          State SNAP: Renewal applications are due by March 15. ((under18??Please get your application signed by a parent or + guardian.))

          +
        2. +
        3. + If you want to make people who are homebound aware of the option of virtual visits (but not other message recipients): +
          +

          State Medicaid: Please call 123-123-1234 to schedule an appointment. ((homebound??Virtual visits are available.))

          +
        4. +
        5. + If you want to send a messages in different languages to different recipients: +
          +

          ((English??We’ve identified unauthorized use on your EBT account. Call the phone number on the back of your card to + cancel or go to your local CSO for immediate replacement.))((Spanish??Hemos identificado un uso no autorizado en su + cuenta EBT. Llame al número de teléfono que aparece en el reverso de su tarjeta para cancelarla o diríjase a su CSO + local para que se la sustituyan inmediatamente.))

          +
        6. +
        -
          -
        • do this yourself
        • -
        • upload a list of personal details and let Notify do it for you
        • + + {# Identify your program #} +

          Identify your program

          +

          You can help your recipients identify your texts as legitimate by customizing your messages to clearly state who they + are from. Consider using the program or benefit name that is most familiar to your recipients.

          + +

          To customize your program name

          +

          To change the text message sender from the default service name:

          +
            +
          1. Go to the Settings page
          2. +
          3. Select “Start text messages with service name.”
          4. +
          5. Change the service name to a familiar program or benefit.
          6. +
          + + {# Prepare your data #} +

          Prepare your data

          +

          The easiest and most efficient way to personalize your content is by uploading a spreadsheet. Notify can accommodate + many file formats and structures.

          + +

          File format

          +

          Notify can accept files in the following formats: CSV, TSV, ODS, and Microsoft Excel.

          + +

          File structure

          +
            +
          • The phone number must be in the first column (Column A) and must be labeled Phone number.
          • +
          • Each column must have a unique name.
          • +
          • If you are using Excel, you must either disable the”auto-date/time format” for time and date columns or convert the file + to a CSV prior to loading. (If you do not, Excel will display date/time data in a confusing format.)
          -

          If you upload a list, the column names need to match the optional content in your template.

          +

          Formatting personalized content

          +

          If you are sending a message with personalized content, such as the first name of the recipient or the appointment time + and location, the names of the column headings have to match the indicator included in the message template.

          +

          For example, if the personalized content is the first name of the recipient, and we are using the spreadsheet below, the + indicator in the message needs to be ((First name)), not ((firstname)) or ((name)).

          -

          Message customization

          +

          Formatting conditional content

          +

          If you are sending messages with conditional content, such as content based on the recipient’s preferred language or + location, the flag to receive the content needs to be captured in its own column with a Yes or No (Y/N) flag.

          -

          Change the text message sender

          +

          Example

          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          + Example +
          Phone numberFirst nameLast nameSpanishEnglishDateTimeLocation
          123-456-7890LuluPraetherNYNovember 2, 20233:25123 Ford Rd.
          234-567-8901SelaAppelYNNovember 2, 20234:00123 Ford Rd.
          123-456-7890DexterMoseleyNYNovember 2, 20232:00123 Ford Rd.
          -

          The text message sender tells your users who the message is from.

          + {# Preventing fraud #} +

          Preventing fraud

          +

          Texting fraud is ever prevalent and while we can’t eliminate the risk, we can reduce the chances that recipients will + fall victim to fraud.

          +
            +
          1. Never send unnecessary or protected private information.
          2. +
          3. Remind your recipients that text messaging is not a secure means of communication and they should not send you private + information via text.
          4. +
          5. Only include links to websites and online applications that are secure.
          6. +
          7. Send an introductory text letting recipients know you will be texting them and to save the number in their phone.
          8. +
          9. Include an auto-response with the name of your organization and a contact phone number.
          10. +
          -

          To change the text message sender from the default of ‘Notify.gov’:

          - -
            -
          1. Go to the Text message settings section of the {{ - service_link(current_service, 'main.service_settings', 'settings') }} page.
          2. -
          3. Select Manage on the Text message - senders row.
          4. -
          5. Select Change or Add text message - sender.
          6. +

            Examples

            +
              +
            1. Introductory text: +
              +

              State Transit Dept: We're piloting a new way to get important reminders to our staff. Save this number to your phone, + you'll receive updates from us here.

              +
            2. +
            3. Auto-response text: +
              +

              State Agency: This number is unmonitored. To contact us, call us at 123-123-1234. We will never ask for personal details + in a text. If you have questions about how to protect your privacy, see statename.gov/privacy.

              +
            {% endblock %} diff --git a/app/templates/views/roadmap.html b/app/templates/views/roadmap.html index 0784e693d..6ea2e7cfb 100644 --- a/app/templates/views/roadmap.html +++ b/app/templates/views/roadmap.html @@ -22,18 +22,22 @@

            This roadmap is only a guide. It does not cover everything we do, and some things may change.

            You can contact us if you have any questions about the roadmap or suggestions for new features.

            -

            What we’re working on

            +

            What we’re working on

            -

            Now

            +

            Now

            We are investigating the Notify concept, building on the notifications tool pioneered by the UK.

            To do this, we are convening a pilot with a small set of partners.

            Goals during this stage:

              -
            • Achieve compliance to begin piloting, such as ATO and privacy standards
            • -
            • Demonstrate that a government-run notifications tool provides a unique value
            • -
            • Gather data from the pilot to improve the product
            • +
            • Achieve compliance to begin piloting, such as ATO and privacy standards. + +
            • +
            • Demonstrate that a government-run notifications tool provides a unique value.
            • +
            • Gather data from the pilot to improve the product.

            Features prioritized during this stage:

            @@ -46,53 +50,38 @@
          7. Message send/failure analytics
        -

        Next

        + Next

        If the pilot is successful, we hope to recruit additional high-impact partners to improve outcomes for low-income individuals and families.

        Goals during this stage:

          -
        • Complement Notify with practical guidance and support services
        • -
        • Iterate on existing features and implement new features based on what we've learned so far
        • -
        • Hone our measurement approaches to better quantify impact
        • +
        • Complement Notify with practical guidance and support services.
        • +
        • Iterate on existing features and implement new features based on what we've learned so far.
        • +
        • Hone our measurement approaches to better quantify impact.

        Features prioritized during this stage:

        -
          +
          • SMS sending via API integration
          • -
          • Single-level decision two-way messaging (e.g. reply “YES” if, or “NO” if…)
          • Self-service account creation
          • Application status page
          • -
          • Scheduled send option
          • +
          • Improved scheduled send option
          -

          Later

          +

          Later

          In the future, we may decide to expand beyond SMS, or to offer the service government-wide.

          Features to be considered during this stage:

          -
            +
              -
            • Email sending via UI and API
            • -
            • Multiple-level decision two-way messaging (greater than one layer decision-tree)
            • -
            • Open-text reply two-way messaging (rather than reply yes or no, 1 or 2, etc.)
            • +
            • Two-way messaging
            • Multilingual interface and content library options
            • Recurring scheduled send (such as “Send each Monday for 3 weeks”)
            • -
            - - {% endblock %} diff --git a/app/templates/views/security.html b/app/templates/views/security.html index f55ad3de5..d676b37bf 100644 --- a/app/templates/views/security.html +++ b/app/templates/views/security.html @@ -15,22 +15,52 @@
          • manage risks around information
          -

          Data

          -

          On Notify, data is encrypted:

          -
            -
          • when it passes through the service
          • -
          • when it’s stored on the service
          • -
          -

          Any recipient data you upload is only held for seven days.

          - +

          Infrastructure

          -

          Technical security

          - +

          Notify.gov is comprised of two applications both running on cloud.gov:

          +
            +
          • Admin, a Flask website running on the python_buildpack which hosts agency user-facing UI
          • +
          • API, a Flask application running on the python_buildpack hosting the Notify API
          • +
          + +

          Notify.gov utilizes several cloud.gov-provided services through Amazon Web Services (AWS):

          +
            +
          • AWS S3 buckets for temporary file storage
          • +
          • AWS Elasticache (Redis) for caching data and enqueueing background tasks
          • +
          • AWS RDS (PostgreSQL) for system data storage
          • +
          + +

          Notify.gov also provisions and uses two AWS services via a supplemental service broker:

          +
            +
          • AWS SNS for sending SMS messages
          • +
          • AWS SES for sending email messages (note: email function is currently unavailable to users for pilot)
          • +
          + +

          Current security review

          +

          Currently, Notify.gov operates under a GSA Lightweight Authority to Operate (LATO). This federal security authorization + process leverages security controls provided by National Institute of Standards and Technology (NIST). The process is + focused on operational security from both a functional and assurance perspective.

          +

          We are pursuing a full Authority to Operate (ATO)

          + +

          Data

          +

          To send a message, agencies upload a spreadsheet of phone numbers and other necessary data from their existing data + management system. On Notify.gov, data is encrypted when it passes through the service and when it’s stored on the + service.

          +

          Notify.gov is not a system of record and as a result does not have a SORN. Agencies are responsible for managing their + data outside of Notify.gov.

          +
            +
          • Notify stores data within the cloud.gov-managed PostgreSQL database and S3 buckets. The entire database is encrypted at + the disk level.
          • +
          • Additionally, data is encrypted at the field level when it is stored in the database, using OpenSSL 3.0.8 via the Python + cryptography library. The implementation is available on GitHub.
          • +
          + +

          Data retention

          +

          Any recipient data uploaded is only held for seven days; all personally identifiable information (PII) is deleted for + successful messages, so data is retained only for unsuccessful messages. +

          + +

          Technical security

          Protect sensitive information

          Some messages include sensitive information like security codes or password reset links.

          @@ -46,6 +76,23 @@

        If signing in with a text message is a problem for your team, contact us to find out about using an email link instead.

        + Screenshot of a teat message in review with the link to 'hide personalization after sending' circled. + +

        How to hide PII after sending a message

        + +

        User permissions and signing in

        +

        You can set different user permissions in Notify. This lets you control who in your team has access to certain parts of + the service.

        + +

        Multi-factor authentication (MFA)

        +

        Notify.gov uses Login.gov to authenticate users.

        +
          +
        • Your email address and password
        • +
        • A code from an authenticator app or sent by Login.gov in the form of a text message
        • +
        +

        If signing in with a text message is a problem for your team, contact us to find out about using an email link instead.

        + - + {% endblock %} diff --git a/app/utils/govuk_frontend_jinja/__init__.py b/app/utils/govuk_frontend_jinja/__init__.py new file mode 100644 index 000000000..34ac8d172 --- /dev/null +++ b/app/utils/govuk_frontend_jinja/__init__.py @@ -0,0 +1 @@ +from .templates import Environment # noqa diff --git a/app/utils/govuk_frontend_jinja/flask_ext.py b/app/utils/govuk_frontend_jinja/flask_ext.py new file mode 100644 index 000000000..41f1e59ce --- /dev/null +++ b/app/utils/govuk_frontend_jinja/flask_ext.py @@ -0,0 +1,30 @@ +from flask.templating import Environment as FlaskEnvironment +from jinja2 import select_autoescape + +from app.utils.govuk_frontend_jinja.templates import Environment as NunjucksEnvironment +from app.utils.govuk_frontend_jinja.templates import ( + NunjucksExtension, + NunjucksUndefined, +) + + +class Environment(NunjucksEnvironment, FlaskEnvironment): + pass + + +def init_govuk_frontend(app): + """Use the govuk_frontend_jinja Jinja environment in a Flask app + + >>> from flask import Flask + >>> app = Flask("cheeseshop_service") + >>> init_govuk_frontend(app) + """ + app.jinja_environment = Environment + app.select_jinja_autoescape = select_autoescape( + ("html", "htm", "xml", "xhtml", "njk") + ) + jinja_options = app.jinja_options.copy() + jinja_options["extensions"].append(NunjucksExtension) + jinja_options["undefined"] = NunjucksUndefined + app.jinja_options = jinja_options + return app diff --git a/app/utils/govuk_frontend_jinja/templates.py b/app/utils/govuk_frontend_jinja/templates.py new file mode 100644 index 000000000..1295fd616 --- /dev/null +++ b/app/utils/govuk_frontend_jinja/templates.py @@ -0,0 +1,292 @@ +import builtins +import os.path as path +import re +from collections.abc import Sized + +import jinja2 +import jinja2.ext +from jinja2.lexer import Token +from markupsafe import Markup + + +def njk_to_j2(template): + # Some component templates (such as radios) use `items` as the key of + # an object element. However `items` is also the name of a dictionary + # method in Python, and Jinja2 will prefer to return this attribute + # over the dict item. Handle specially. + template = re.sub(r"\.items\b", ".items__njk", template) + + # Some component templates (such as radios) append the loop index to a + # string. As the loop index is an integer this causes a TypeError in + # Python. Jinja2 has an operator `~` for string concatenation that + # converts integers to strings. + template = template.replace("+ loop.index", "~ loop.index") + + # The Character Count component in version 3 concatenates the word count + # with the hint text. As the word count is an integer this causes a + # TypeError in Python. Jinja2 has an operator `~` for string + # concatenation that converts integers to strings. + template = template.replace( + "+ (params.maxlength or params.maxwords) +", + "~ (params.maxlength or params.maxwords) ~", + ) + + # Nunjucks uses elseif, Jinja uses elif + template = template.replace("elseif", "elif") + + # Some component templates (such as input) call macros with params as + # an object which has unqoted keys. This causes Jinja to silently + # ignore the values. + template = re.sub( + r"""^([ ]*)([^ '"#\r\n:]+?)\s*:""", r"\1'\2':", template, flags=re.M + ) + + # govukFieldset can accept a call block argument, however the Jinja + # compiler does not detect this as the macro body is included from + # the template file. A workaround is to patch the declaration of the + # macro to include an explicit caller argument. + template = template.replace( + "macro govukFieldset(params)", "macro govukFieldset(params, caller=none)" + ) + + # Many components feature an attributes field, which is supposed to be + # a dictionary. In the template for these components, the keys and values + # are iterated. In Python, the default iterator for a dict is .keys(), but + # we want .items(). + # This only works because our undefined implements .items() + # We've tested this explicitly with: govukInput, govukCheckbox, govukTable, + # govukSummaryList + template = re.sub( + r"for attribute, value in (params|item|cell|action).attributes", + r"for attribute, value in \1.attributes.items()", + template, + flags=re.M, + ) + + # Some templates try to set a variable in an outer block, which is not + # supported in Jinja. We create a namespace in those templates to get + # around this. + template = re.sub( + r"""^([ ]*)({% set describedBy =( params.*describedBy if params.*describedBy else)? "" %})""", + r"\1{%- set nonlocal = namespace() -%}\n\1\2", + template, + flags=re.M, + ) + # Change any references to describedBy to be nonlocal.describedBy, + # unless describedBy is a dictionary key (i.e. quoted or dotted). + template = re.sub(r"""(?>> foo = ChainableUndefined(name='foo') + >>> str(foo.bar['baz']) + '' + >>> foo.bar['baz'] + 42 + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined + """ + return self + + __getitem__ = __getattr__ + + # Allow treating undefined as an (empty) dictionary. + # This works because Undefined is an iterable. + def items(self): + return self + + # Allow escaping with Markup. This is required when + # autoescape is enabled. Debugging this issue was + # annoying; the error messages were not clear as to + # the cause of the issue (see upstream pull request + # for info https://github.com/pallets/jinja/pull/1047) + def __html__(self): + return str(self) + + # attempt to behave a bit like js's `undefined` when concatenation is attempted + def __add__(self, other): + if isinstance(other, str): + return "undefined" + other + return super().__add__(other) + + def __radd__(self, other): + if isinstance(other, str): + return other + "undefined" + return super().__radd__(other) + + +class NunjucksCodeGenerator(jinja2.compiler.CodeGenerator): + def visit_CondExpr(self, node, frame): + if not (self.filename or "").endswith(".njk"): + return super().visit_CondExpr(node, frame) + + # else our replacement, which is based on that in + # https://github.com/pallets/jinja/blob/c4c4088945a2c12535f539be7f5453b9ca94666c/jinja2/compiler.py#L1613 + def write_expr2(): + if node.expr2 is not None: + return self.visit(node.expr2, frame) + # rather than complaining about a missing else + # clause we just assume it to be the empty + # string for nunjucks compatibility + return self.write('""') + + self.write("(") + self.visit(node.expr1, frame) + self.write(" if ") + self.visit(node.test, frame) + self.write(" else ") + write_expr2() + self.write(")") + + +_njk_signature = "__njk" +_builtin_function_or_method_type = type({}.keys) + + +class Environment(jinja2.Environment): + code_generator_class = NunjucksCodeGenerator + + def __init__(self, *args, **kwargs): + kwargs.setdefault("extensions", [NunjucksExtension]) + kwargs.setdefault("undefined", NunjucksUndefined) + super().__init__(*args, **kwargs) + self.filters["indent_njk"] = indent_njk + + def join_path(self, template, parent): + """Enable the use of relative paths in template import statements""" + if template.startswith(("./", "../")): + return path.normpath(path.join(path.dirname(parent), template)) + else: + return template + + def _handle_njk(method_name): + def inner(self, obj, argument): + if isinstance(argument, str) and argument.endswith(_njk_signature): + # a njk-originated access will always be assuming a dict lookup before an attr + final_method_name = "getitem" + final_argument = argument[: -len(_njk_signature)] + else: + final_argument = argument + final_method_name = method_name + + # pleasantly surprised that super() works in this context + retval = builtins.getattr(super(), final_method_name)(obj, final_argument) + + if ( + argument == f"length{_njk_signature}" + and isinstance(retval, jinja2.runtime.Undefined) + and isinstance(obj, Sized) + ): + return len(obj) + if ( + isinstance(argument, str) + and argument.endswith(_njk_signature) + and isinstance(retval, _builtin_function_or_method_type) + ): + # the lookup has probably gone looking for attributes and found a builtin method. because + # any njk-originated lookup will have been made to prefer dict lookups over attributes, we + # can be fairly sure there isn't a dict key matching this - so we should just call this a + # failure. + return self.undefined(obj=obj, name=final_argument) + return retval + + return inner + + getitem = _handle_njk("getitem") + + getattr = _handle_njk("getattr") diff --git a/poetry.lock b/poetry.lock index da354843e..86b3d047b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -529,47 +529,56 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.7" +version = "42.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, - {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, - {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, - {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, + {file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, + {file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, + {file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, + {file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, + {file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, + {file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, ] [package.dependencies] -cffi = ">=1.12" +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] nox = ["nox"] -pep8test = ["black", "check-sdist", "mypy", "ruff"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -929,28 +938,6 @@ files = [ [package.dependencies] requests = "*" -[[package]] -name = "govuk-frontend-jinja" -version = "0.5.8-alpha" -description = "Tools to use the GOV.UK Design System with Jinja-powered Python apps" -optional = false -python-versions = "*" -files = [] -develop = false - -[package.dependencies] -jinja2 = "*" - -[package.extras] -dev = ["pytest", "pytest-flakes", "pytest-helpers-namespace"] -flask = ["Flask"] - -[package.source] -type = "git" -url = "https://github.com/alphagov/govuk-frontend-jinja.git" -reference = "v0.5.8-alpha" -resolved_reference = "15845e4cca3a05df72c6e13ec6a7e35acc682f52" - [[package]] name = "greenlet" version = "3.0.1" @@ -1428,16 +1415,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1700,7 +1677,7 @@ requests = ">=2.0.0" [[package]] name = "notifications-utils" -version = "0.2.7" +version = "0.2.8" description = "" optional = false python-versions = ">=3.9,<3.12" @@ -1718,7 +1695,7 @@ certifi = "^2023.7.22" cffi = "^1.16.0" charset-normalizer = "^3.1.0" click = "^8.1.3" -cryptography = "^41.0.6" +cryptography = "^42.0.0" flask = "^2.3.2" flask-redis = "^0.4.0" geojson = "^3.0.1" @@ -1752,7 +1729,7 @@ werkzeug = "^3.0.1" type = "git" url = "https://github.com/GSA/notifications-utils.git" reference = "HEAD" -resolved_reference = "b6cee72f45dbcd48b59447fa08bbac59e15a7b98" +resolved_reference = "a48171e865eb83cf29c75751be7369f396cbe3e2" [[package]] name = "numpy" @@ -2476,7 +2453,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -3088,4 +3064,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "94e6fe2e143f12eaa31f01b5a9206012a6a48908d98daa9fa2f531e986e4a6d3" +content-hash = "be46f8e90d6213db5c54d62c3a364c0a2a7e50f5c8e5007ad662067a6191c51f" diff --git a/pyproject.toml b/pyproject.toml index 2aed42337..49ea4dbe3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ flask-login = "^0.6" flask-talisman = "*" flask-wtf = "^1.2" govuk-bank-holidays = "==0.13" -govuk-frontend-jinja = {git = "https://github.com/alphagov/govuk-frontend-jinja.git", tag = "v0.5.8-alpha"} gunicorn = {version = "==21.2.0", extras = ["eventlet"]} humanize = "~=4.9" itsdangerous = "~=2.1" diff --git a/terraform/README.md b/terraform/README.md index efab933fa..7390e3947 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -88,6 +88,8 @@ The below steps rely on you first configuring access to the Terraform state in s terraform plan ``` + If the `terraform init` command fails, you may need to run `terraform init -upgrade` to make sure new module versions are picked up. + 1. Apply changes with `terraform apply`. 1. Remove the space deployer service instance if it doesn't need to be used again, such as when manually running terraform once. diff --git a/terraform/bootstrap/providers.tf b/terraform/bootstrap/providers.tf index 44ccda085..b6f27acf8 100644 --- a/terraform/bootstrap/providers.tf +++ b/terraform/bootstrap/providers.tf @@ -3,7 +3,7 @@ terraform { required_providers { cloudfoundry = { source = "cloudfoundry-community/cloudfoundry" - version = "0.15.5" + version = "0.53.0" } } } diff --git a/terraform/demo/main.tf b/terraform/demo/main.tf index f26e495a5..7f34fb0f1 100644 --- a/terraform/demo/main.tf +++ b/terraform/demo/main.tf @@ -7,7 +7,7 @@ locals { } module "redis" { - source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name @@ -17,7 +17,7 @@ module "redis" { } module "logo_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name diff --git a/terraform/demo/providers.tf b/terraform/demo/providers.tf index eba9e9490..2ced7915f 100644 --- a/terraform/demo/providers.tf +++ b/terraform/demo/providers.tf @@ -3,7 +3,7 @@ terraform { required_providers { cloudfoundry = { source = "cloudfoundry-community/cloudfoundry" - version = "0.15.5" + version = "0.53.0" } } @@ -12,7 +12,6 @@ terraform { key = "admin.tfstate.demo" encrypt = "true" region = "us-gov-west-1" - profile = "notify-terraform-backend" } } diff --git a/terraform/development/main.tf b/terraform/development/main.tf index ff6001152..767256c3f 100644 --- a/terraform/development/main.tf +++ b/terraform/development/main.tf @@ -11,7 +11,7 @@ data "cloudfoundry_space" "dev" { } module "logo_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name diff --git a/terraform/development/providers.tf b/terraform/development/providers.tf index d8ae4488e..5dcaece3e 100644 --- a/terraform/development/providers.tf +++ b/terraform/development/providers.tf @@ -3,7 +3,7 @@ terraform { required_providers { cloudfoundry = { source = "cloudfoundry-community/cloudfoundry" - version = "0.50.7" + version = "0.53.0" } } } diff --git a/terraform/development/reset.sh b/terraform/development/reset.sh index d51d43eec..57c2f4d87 100755 --- a/terraform/development/reset.sh +++ b/terraform/development/reset.sh @@ -47,7 +47,7 @@ if [[ ! -s "secrets.auto.tfvars" ]]; then fi echo "Importing terraform state for $username" -terraform init +terraform init -upgrade key_name=$username-admin-dev-key diff --git a/terraform/production/main.tf b/terraform/production/main.tf index c635ed2d4..450212cdf 100644 --- a/terraform/production/main.tf +++ b/terraform/production/main.tf @@ -7,7 +7,7 @@ locals { } module "redis" { - source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name @@ -17,7 +17,7 @@ module "redis" { } module "logo_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name @@ -50,7 +50,7 @@ module "api_network_route" { # https://cloud.gov/docs/services/external-domain-service/#how-to-create-an-instance-of-this-service ########################################################################### module "domain" { - source = "github.com/18f/terraform-cloudgov//domain?ref=v0.5.2" + source = "github.com/18f/terraform-cloudgov//domain?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name diff --git a/terraform/production/providers.tf b/terraform/production/providers.tf index e0daee8b0..ed822db44 100644 --- a/terraform/production/providers.tf +++ b/terraform/production/providers.tf @@ -3,7 +3,7 @@ terraform { required_providers { cloudfoundry = { source = "cloudfoundry-community/cloudfoundry" - version = "0.15.5" + version = "0.53.0" } } @@ -12,7 +12,6 @@ terraform { key = "admin.tfstate.prod" encrypt = "true" region = "us-gov-west-1" - profile = "notify-terraform-backend" } } diff --git a/terraform/sandbox/main.tf b/terraform/sandbox/main.tf index 69457b5d5..74c16d808 100644 --- a/terraform/sandbox/main.tf +++ b/terraform/sandbox/main.tf @@ -7,7 +7,7 @@ locals { } module "redis" { - source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name @@ -17,7 +17,7 @@ module "redis" { } module "logo_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name diff --git a/terraform/sandbox/providers.tf b/terraform/sandbox/providers.tf index 5fe15f874..09911edc4 100644 --- a/terraform/sandbox/providers.tf +++ b/terraform/sandbox/providers.tf @@ -3,7 +3,7 @@ terraform { required_providers { cloudfoundry = { source = "cloudfoundry-community/cloudfoundry" - version = "0.15.5" + version = "0.53.0" } } @@ -12,7 +12,6 @@ terraform { key = "admin.tfstate.sandbox" encrypt = "true" region = "us-gov-west-1" - profile = "notify-terraform-backend" } } diff --git a/terraform/shared/container_networking/providers.tf b/terraform/shared/container_networking/providers.tf index 8db86ca90..21ac567a2 100644 --- a/terraform/shared/container_networking/providers.tf +++ b/terraform/shared/container_networking/providers.tf @@ -3,7 +3,7 @@ terraform { required_providers { cloudfoundry = { source = "cloudfoundry-community/cloudfoundry" - version = "~> 0.15" + version = "0.53.0" } } } diff --git a/terraform/staging/main.tf b/terraform/staging/main.tf index 4a07e9bd7..d0df6e81d 100644 --- a/terraform/staging/main.tf +++ b/terraform/staging/main.tf @@ -7,7 +7,7 @@ locals { } module "redis" { - source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name @@ -17,7 +17,7 @@ module "redis" { } module "logo_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1" cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name diff --git a/terraform/staging/providers.tf b/terraform/staging/providers.tf index 7648e192f..d6928e61e 100644 --- a/terraform/staging/providers.tf +++ b/terraform/staging/providers.tf @@ -3,7 +3,7 @@ terraform { required_providers { cloudfoundry = { source = "cloudfoundry-community/cloudfoundry" - version = "0.15.5" + version = "0.53.0" } } @@ -12,7 +12,6 @@ terraform { key = "admin.tfstate.stage" encrypt = "true" region = "us-gov-west-1" - profile = "notify-terraform-backend" } } diff --git a/tests/app/main/views/test_activity.py b/tests/app/main/views/test_activity.py index 69e704827..96c50a308 100644 --- a/tests/app/main/views/test_activity.py +++ b/tests/app/main/views/test_activity.py @@ -160,8 +160,8 @@ def test_can_show_notifications( assert normalize_spaces( first_row.select_one(".table-field-right-aligned .align-with-message-body").text ) in [ - "Delivered 1 January at 02:01 US/Eastern", - "Delivered 1 January at 01:01 US/Eastern", + "Delivered 1 January at 02:00 US/Eastern", + "Delivered 1 January at 01:00 US/Eastern", ] assert page_title in page.h1.text.strip() @@ -661,31 +661,31 @@ def test_redacts_templates_that_should_be_redacted( ( "email", "temporary-failure", - "Inbox not accepting messages right now 27 September at 08:31 US/Eastern", + "Inbox not accepting messages right now 27 September at 08:30 US/Eastern", False, ), ( "email", "permanent-failure", - "Email address does not exist 27 September at 08:31 US/Eastern", + "Email address does not exist 27 September at 08:30 US/Eastern", False, ), - ("email", "delivered", "Delivered 27 September at 08:31 US/Eastern", True), + ("email", "delivered", "Delivered 27 September at 08:30 US/Eastern", True), ("sms", "created", "Sending since 27 September at 08:30 US/Eastern", True), ("sms", "sending", "Sending since 27 September at 08:30 US/Eastern", True), ( "sms", "temporary-failure", - "Phone not accepting messages right now 27 September at 08:31 US/Eastern", + "Phone not accepting messages right now 27 September at 08:30 US/Eastern", False, ), ( "sms", "permanent-failure", - "Not delivered 27 September at 08:31 US/Eastern", + "Not delivered 27 September at 08:30 US/Eastern", False, ), - ("sms", "delivered", "Delivered 27 September at 08:31 US/Eastern", True), + ("sms", "delivered", "Delivered 27 September at 08:30 US/Eastern", True), ], ) def test_sending_status_hint_displays_correctly_on_notifications_page( diff --git a/tests/app/main/views/test_jobs.py b/tests/app/main/views/test_jobs.py index 42cd5ecbf..323a1fe00 100644 --- a/tests/app/main/views/test_jobs.py +++ b/tests/app/main/views/test_jobs.py @@ -90,7 +90,7 @@ def test_should_show_page_for_one_job( assert page.h1.text.strip() == "thisisatest.csv" assert " ".join(page.find("tbody").find("tr").text.split()) == ( - "2021234567 template content Delivered 1 January at 06:10 US/Eastern" + "2021234567 template content Delivered 1 January at 06:09 US/Eastern" ) assert page.find("div", {"data-key": "notifications"})["data-resource"] == url_for( "main.view_job_updates", @@ -109,7 +109,7 @@ def test_should_show_page_for_one_job( assert page.find("span", {"id": "time-left"}).text == "Data available for 7 days" assert normalize_spaces(page.select_one("tbody tr").text) == normalize_spaces( - "2021234567 " "template content " "Delivered 1 January at 06:10 US/Eastern" + "2021234567 " "template content " "Delivered 1 January at 06:09 US/Eastern" ) assert page.select_one("tbody tr a")["href"] == url_for( "main.view_notification", @@ -424,7 +424,7 @@ def test_should_show_updates_for_one_job_as_json( assert "2021234567" in content["notifications"] assert "Status" in content["notifications"] assert "Delivered" in content["notifications"] - assert "00:01" in content["notifications"] + assert "00:00" in content["notifications"] assert "Sent by Test User on 1 January at 00:00" in content["status"] @@ -466,7 +466,7 @@ def test_should_show_updates_for_scheduled_job_as_json( assert "2021234567" in content["notifications"] assert "Status" in content["notifications"] assert "Delivered" in content["notifications"] - assert "00:01" in content["notifications"] + assert "00:00" in content["notifications"] assert "Sent by Test User on 1 June at 16:00" in content["status"]